Symfony: cómo resolver una dependencia circular

Técnicas para resolver una dependencia circular empleando el Event Dispatcher

Image for post
Image for post

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:

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.

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:

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 :

Image for post
Image for post

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

Image for post
Image for post

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:

Image for post
Image for post

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

Image for post
Image for post

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

Image for post
Image for post

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:

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store