Symfony: cómo resolver una dependencia circular
Técnicas para resolver una dependencia circular empleando el Event Dispatcher

Hoy quiero hablaros de un error muy común con el que os podéis topar al montar aplicaciones relativamente complejas con Symfony: la dependencia circular entre servicios.
Este error, cuyo aspecto es similar al siguiente:
Circular reference detected for service "security.authorization_checker", path: "sensio_framework_extra.security.listener -> security.authorization_checker -> security.authentication.manager -> security.user.provider.concrete.entity_provider -> doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection -> app.listener.user -> app.mail_service -> templating -> twig -> security.context".
sucede en el momento en el que cerramos un círculo de dependencias en el que el servicio A depende del B, que a su vez depende de C y éste último de A.
A -> B -> C -> A
Una de las situaciones más comunes en las que nos puede aparecer es la siguiente:
- Declarar un
DoctrineEntityListener
para que cada vez que se genere un eventopostPersist
enviemos un email. - Este email se envía por medio de una clase (por ejemplo
AppMailer
) que inyectaremos en elEntityListener
anterior. - La clase
AppMailer
tiene además como dependencia el servicioTwig
para generar las plantillas. - Y finalmente en otra parte tenemos una
TwigExtension
que emplea el serviciodoctrine
para obtener el repositorio de una determinada entidad.
Y ya tenemos el lío montado debido a que:
Doctrine -> AppMailer -> Twig -> Doctrine
Para resolverlo, la mejor forma es inyectar el servicio container
en cualquiera de los puntos intermedios de modo que podamos romper el círculo de dependencias. Por ejemplo, puesto que desde el container
podemos acceder a cualquier servicio, bastará con inyectarlo como dependencia de AppMailer
para desde ahí obtener el servicio twig
y resolver el problema.
Lo repetiré por si acaso:
Inyectar el servicio container
para solucionar este tipo de errores (y en general, inyectar el container
en cualquier sitio) es una práctica horrible debido a que:
- Una clase sólo debería recibir las dependencias que necesita, no todas.
- Hace el código mucho más difícil de leer, dado que las dependencias de la clase no están definidas.
- Realizar tests unitarios requiere pasar un
container
con la dependencia, añadiendo un nivel más de abstracción. - La ausencia de un servicio será notificada en tiempo de ejecución y no cuando se compile el
container
. - Rompe el principio de diseño de programar contra interfaces, lo cual provoca que las dependencias no puedan reemplazarse de forma directa, pues están hardcodeadas en la case.
- Y si todo esto no te convence piensa en el dicho “matar moscas a cañonazos”
Ahora una posible solución válida
Ahora que ya tenemos claro que inyectar el container
no es una buena práctica, vamos con una de las posibles soluciones que involucrará al EventDispatcher
.
En primer lugar, veamos el aspecto de la clase AppMailer
:

así como de la clase que recibe en el método __invoke
que contiene las variables necesarias para generar el email:

Lo que haremos para evitar la dependencia circular será crear unEventSubscriber
que tendrá al servicio AppMailer
como dependencia y que cada vez que reciba un cierto evento se encargue de llamar al servicio AppMailer
para enviar el email.
De este modo, ya no inyectaremos directamente el servicio AppMailer
en el DoctrineEntityListener
sino que inyectaremos el EventDispatcher
para enviar el evento rompiendo de este modo la dependencia.
Es decir, primero definimos una clase que contendrá el evento:

Definiremos a continuación un EventSubscriber
que escuche dicho evento y que, al recibirlo, llame a la clase AppMailer
con el contenido del evento:

Y finalmente, en el servicio donde antes estábamos inyectando el servicio AppMailer
inyectaremos el EventDispatcher
para enviar el evento:

De este modo evitaremos la dependencia circular a la vez que ganamos la posibilidad de registrar en qué momento se está enviando un email pues estos serán enviados mediante un evento al que podremos suscribirnos con nuevos EventSubscribers
.
¿Quieres recibir más artículos como este?
Suscríbete a nuestra newsletter: