Symfony: cómo resolver una dependencia circular

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 evento postPersist enviemos un email.
  • Este email se envía por medio de una clase (por ejemplo AppMailer ) que inyectaremos en el EntityListener anterior.
  • La clase AppMailer tiene además como dependencia el servicio Twig para generar las plantillas.
  • Y finalmente en otra parte tenemos una TwigExtension que emplea el servicio doctrine 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 .