Creando aplicaciones de Symfony a través de sus componentes: Dependency Injection

Pablo Lozano Munárriz
The Cocktail Engineering
4 min readJun 6, 2019

--

En este artículo nos centraremos en el componente Dependency Injection uno de los componentes más usados de Symfony por su potencia y versatilidad.

Dependency Injection (DI) es un patrón de diseño orientado a objetos, en el que se suministran objetos a una clase en lugar de ser la propia clase la que cree dichos objetos. Nuestras clases no crean los objetos, sino que se los suministra otra clase ‘contenedora’ que inyectará la implementación deseada. (Wikipedia)

Comenzamos instalando el componente con composer:

composer require symfony/dependency-injection

ContainerBuilder

ContainerBuilder es un contenedor DI que proporciona una API para definir fácilmente los servicios.

$container = new ContainerBuilder();

En nuestra aplicación tendremos una serie de procesadores de archivos que individualmente harán unas tareas definidas. Crearemos una interfaz para definir los métodos que tienen en común:

Implementaremos un par de clases con esta interfaz (ImageFileProcessor y VideoFileProcessor). Añadiremos una clase ProcessorChain en la que inyectaremos, mediante el constructor, cada uno de dichos procesadores. Esta clase recorrerá cada uno ellos para ver si es responsabilidad suya hacer algo con el archivo.

Ahora modificamos nuestra aplicación para usar el contenedor y registrar los servicios que hemos definido. Añadimos también como servicio nuestro comando ‘ProcessorCommand’ al cual debemos pasarle como argumentos los objetos con el método ‘addArgument’.

El contenedor nos permite añadir tags a las definiciones de servicios los cuales nos ayudarán más adelante en la inyección de dependencias.

Ahora si lanzamos la aplicación nos encontraremos con el siguiente error:

The “processor_command” service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.

Acceder directamente a un servicio desde el contenedor no es una buena práctica, por eso por defecto todos los servicios son privados y no son accesibles mediante el método get. En este caso haremos público ‘ProcessorCommand’ para poder acceder a él.

$container->register('processor_command', ProcessorCommand::class)
->addArgument($container->get('processor_chain'))
->setPublic(true)
->addTag('console.command', ['command' => 'process']);

Hasta aquí hemos visto como crear un contenedor y registrar nuestros servicios pero realmente no hemos obtenido ninguna mejora en la lógica de nuestra aplicación.

Compiler Passes

Los Compiler Passes nos permiten modificar las definiciones del contenedor durante la compilación. Antes hemos visto que nos daba un error en nuestro comando el cual no era público. Symfony provee de una serie de compiler passes los cuales se ejecutan al compilar el contenedor:

$container->compile();

Estos compiladores nos permiten entre otras cosas detectar referencias circulares, definir alias, eliminar servicios no usados, decoradores, etc…

Es interesante echarle un ojo a cada uno de ellos: https://github.com/symfony/symfony/tree/master/src/Symfony/Component/DependencyInjection/Compiler

También podemos crear nuestros propios compiler passes para redefinir los servicios que hemos registrado anteriormente. Crearemos un compiler pass para añadir a nuestro ProcessorChain todos y cada uno de los procesadores que tengamos.

Primero registramos nuestros procesadores añadiéndoles un tag que nos permita diferenciarlos.

$container->register(ImageFileProcessor::class)
->setPublic(false)
->addTag('app.processor');

Y creamos el compiler pass:

Al compilar el contenedor buscará los servicios definidos con el tag ‘app.processor’ e inyectará un objeto RewindableGenerator el cual nos permitirá iterar sobre cada uno de ellos.

Debemos decirle al contenedor que al compilar tenga en cuenta nuestro compiler pass y así tengamos todos nuestros procesadores en la clase ProcessorChain.

$container->addCompilerPass(new FileProcessorCompilerPass());

“Autowire & Autoconfigure”

Teniendo todos nuestros servicios en el contenedor, y sin necesidad de argumentos más allá de los propios del contenedor, podemos dejar que Symfony se encargue de las definiciones de los mismos, para ello tan solo hemos de registrar nuestros servicios con autowire.

$container->autowire(ProcessorChain::class);

Con Autoconfigure establecemos que las instancias de nuestras clases puedan ser configuradas globalmente. Esto es tarea de ResolveInstanceofConditionalsPass

Esto nos es útil sobre todo con los servicios que tengamos con tags definidos, ya que podemos directamente aplicar una configuración global para todos.

Nuestras clases implementan la interfaz FileProcessor con lo que mediante el método ‘registerForAutoconfiguration’ establecemos un tag a todos.

$container->registerForAutoconfiguration(FileProcessor::class)
->addTag('app.processor');
$container->autowire(ImageFileProcessor::class)
->setAutoconfigured(true);

Nuestra aplicación quedaría de la siguiente manera:

Para añadir un nuevo procesador tan solo hemos de añadirlo al contenedor como autoconfigure, pero podemos añadirlos automáticamente con la ayuda del componente Config, el cual veremos en otro artículo.

Dumpers

En aplicaciones grandes, si existen muchos archivos de configuración, el rendimiento se verá afectado ya que debe leer cada uno de estos archivos, para evitarlo Symfony genera un archivo de cache del contenedor mediante la clase PhpDumper.

Aunque todavía no hemos definido archivos de configuración podemos usar los dumpers para ver el container que se genera al compilar todos nuestros servicios.

En nuestro caso tendríamos que borrar manualmente el archivo ‘cached_container.php’ para volver a construir la cache, pero ¿Cómo detecta Symfony que hemos hecho cambios en nuestros servicios? Para ello volvemos a referirnos al componente Config. Symfony encapsula cada una de nuestras configuraciones en una clase ConfigCache que comprueba la fecha de modificación. Si alguna configuración ha cambiado vuelve a compilar todo.

Existen otros dumpers para mostrar el contenedor en diferentes formatos: Yaml, Xml, Graphviz ..

Añadiendo el dumper Graphviz obtenemos la siguiente gráfica:

Como vemos, nuestra aplicación, solo tiene un comando de entrada y dependiendo del tipo de archivo se procesará en uno de ellos o en ambos. Con la inyección de dependencias hemos logrado crear los objetos y despreocuparnos.

Conclusiones

El componente DI, como hemos visto, nos facilita la creación de nuestros servicios, pero va mucho más allá. Nos permite trabajar con parámetros, resolver variables de entorno definidas en los argumentos, etc..

Como veremos en el próximo artículo tiene una simbiosis perfecta con el componente Config, permitiéndonos trabajar con archivos de configuración y por lo tanto más fácilmente.

--

--