Pimple service container on steroids

Siempre nos ha gustado Pimple como contenedor de servicios por su sencillez y fácil configuración e instanciación de servicios.

El problema surge cuando comenzamos un proyecto, inicialmente pequeño y simple, la cosa empieza a funcionar y requiere más y más servicios.

Lo primero que te pide el cuerpo, es dividir la carga de servicios mediante ServiceProviders, agrupando los servicios por dominios lógicos, a semejanza de los Bundles de Symfony.

Pero, que sucede, cuando nuestro número de ServiceProviders supera los 30 o 40, y encima con dependencias entre ellos, extendiendo unos los servicios de otros.

Como hemos sido programadores ordenados (es lo que normalmente nos caracteriza como informáticos 😜), todos los servicios de nuestros ServiceProviders tienen la nomenclatura con su propio espacio de nombres:

  • En OrderServiceProvider, los servicios comienzan por “order.*”:
  • En CartServiceProvider, los servicios empiezan como “cart.*”:
  • En TemplatingServiceProvider, como adivinaréis ya, los servicios se denominan “templating.*”:

Se nos ocurre, que cada ServiceProvider defina su propio ámbito y éste se identifique por el espacio de nombres común a sus servicios. De este modo, tendríamos los siguientes scopes: “order”, “cart” y “templating”.

Que tal si registramos los scopes en el contenedor de servicios, sin llegar a definir los servicios de cada ServiceProvider, y sólo cuando se requiera algún servicio del scope, éste se levante y registre todos sus servicios asociados.

Así, nuestros ServiceProviders quedarían del siguiente modo:

Ahora, se nos ocurre crear un CartHelper que nos sea útil a la hora de renderizar el carro de compra en las vistas. Para que fuera accesible en nuestros templates, deberíamos agregarlo al templating.helper.locator a través del mapa templating.helper.locator.map. Así que extendemos el servicio para agregar nuestro nuevo helper:

¿Es correcta esta implementación? No del todo; seguramente nos falle en alguna ocasión que otra. Recordemos que los servicios no están definidos en el contenedor de servicios hasta que se solicita un servicio del scope.

Imaginemos que alguien solicita cart.provider y por tanto se cargan todos los servicios del scope de cart. Cuando lleguemos al extend, como templating todavía no se ha cargado se levantará para dar de alta sus servicios y posteriormente se ejecutará el extend. Todo bien.

Si por el contrario, en un punto anterior de nuestro script, se levantó el scope de templating, y se solicitó el servicio templating.helper.locator.map, cuando se levante cart y se trate de realizar el extend, se lanzará una excepción puesto que el servicio ya está instanciado en el contenedor. Sólo es posible extender la definición de un servicio si todavía no se ha instanciado. Todo mal.

No podemos esperar a que se carguen los servicios de un scope para tratar de extender los de otro. Así que vamos a mover dicha instrucción al método register:

De este modo, cuando se registre CartServiceProvider se levantará el scope de templating y se extenderá el servicio templating.helper.locator.map. ¿Es realmente lo que queremos? ¿Y si recibimos una petición rest que maneja un controlador que hace uso del carro de compra y devuelve un json, sin hacer uso en ningún momento de templating? No, queremos solo cargar los recursos que realmente se vayan a utilizar. Lazy load a tope.

No hay problema, nuestro “Pimple on steroids” es suficientemente inteligente para darse cuenta que el servicio templating.helper.locator.map no está definido pero si su scope, de esta forma puede “esperarse” a que el scope de templating se levante para extender en ese momento el servicio, se vaya a instanciar o no.

Fantástico y maravilloso 👌.

Así, puede darse el caso, que lleguemos a nuestro template cart.summary.html.php y por tanto se haya levantado el scope de templating:

Se solicita el helper de cart y el ServiceLocator templating.helper.locator resuelve que el servicio necesario es cart.templating.helper y en ese preciso momento se levanta el scope de cart, se definen sus servicios en el contenedor y se instancia el servicio.

Otro ejemplo claro de disparador levanta-scopes es el caso de los alias de servicios nombrándolos con la interfaz que representan, al estilo del contenedor de servicios de Symfony 4.

Cuando en una acción de un controlador se requiera un servicio por su interfaz, en este caso Cart\Domain\CartProviderInterface:

el ArgumentResolver de HttpKernel tratará de buscarlo en el contenedor de servicios, lo encontrará y lo instanciará. Al tratarse de un alias, éste llamará a su vez al servicio real cart.provider que levantará el scope de cart.

En resumen, disponemos de nuestro contenedor de servicios con multitud de ServiceProviders, unos relacionados con otros, y que, en cada llamada a nuestro sistema, utiliza sólo los recursos que necesita.

Bien por nosotros 👏