En Symfony2, ¿por qué es una mala idea inyectar el contenedor de servicios, en lugar de servicios individuales?

No puedo encontrar la respuesta a esto …

Si inyecto el contenedor de servicio, como:

// config.yml my_listener: class: MyListener arguments: [@service_container] my_service: class: MyService // MyListener.php class MyListener { protected $container; public function __construct(ContainerInterface $container) { $this->container = $container; } public function myFunction() { $my_service = $this->container->get('my_service'); $my_service->doSomething(); } } 

entonces funciona tan bien como si lo hiciera:

 // config.yml my_listener: class: MyListener arguments: [@my_service] my_service: class: MyService // MyListener.php class MyListener { protected $my_service; public function __construct(MyService $my_service) { $this->my_service = $my_service; } public function myFunction() { $this->my_service->doSomething(); } } 

Entonces, ¿por qué no debería simplemente inyectar el contenedor de servicios y obtener los servicios de eso dentro de mi clase?

Mi lista de razones por las que deberías preferir los servicios de inyección:

  1. Su clase depende solo de los servicios que necesita, no del contenedor de servicios. Esto significa que el servicio se puede usar en un entorno que no utiliza el contenedor de servicios de Symfony. Por ejemplo, puede convertir su servicio en una biblioteca que se pueda usar en Laravel, Phalcon, etc. – su clase no tiene idea de cómo se están inyectando las dependencias.

  2. Al definir las dependencias en el nivel de configuración, puede usar el descargador de configuración para saber qué servicios están usando qué otros servicios. Por ejemplo, al inyectar @mailer , es bastante fácil trabajar desde el contenedor de servicio donde se ha inyectado el progtwig de correo. Por otro lado, si haces $container->get('mailer') , entonces, prácticamente la única forma de averiguar dónde se está usando el mailer es hacer un find .

  3. Se le notificará acerca de las dependencias faltantes cuando se compile el contenedor, en lugar de hacerlo en tiempo de ejecución. Por ejemplo, imagine que ha definido un servicio, que está inyectando en un oyente. Unos meses más tarde, elimina accidentalmente la configuración del servicio. Si está inyectando el servicio, se le notificará tan pronto como borre la caché. Si se inyecta el contenedor de servicio, solo descubrirá el error cuando el oyente falle debido a que el contenedor no puede obtener el servicio. Claro, podría tomar esto si tiene pruebas de integración exhaustivas, pero … tiene pruebas de integración exhaustivas, ¿no es así? 😉

  4. Sabrá antes si está inyectando el servicio incorrecto. Por ejemplo, si tiene:

     public function __construct(MyService $my_service) { $this->my_service = $my_service; } 

    Pero has definido al oyente como:

     my_listener: class: Whatever arguments: [@my_other_service] 

    Cuando el oyente recibe MyOtherService , PHP lanzará un error, diciéndole que está recibiendo la clase incorrecta. Si está haciendo $container->get('my_service') está asumiendo que el contenedor está devolviendo la clase correcta, y puede llevar mucho tiempo descubrir que no es así.

  5. Si está utilizando un IDE, entonces escriba sugerencias para agregar una carga completa de ayuda adicional. Si está utilizando $service = $container->get('service'); entonces su IDE no tiene idea de qué $service es. Si se inyecta con

     public function __construct(MyService $my_service) { $this->my_service = $my_service; } 

    entonces su IDE sabe que $this->my_service es una instancia de MyService , y puede ofrecer ayuda con nombres de métodos, parámetros, documentación, etc.

  6. Tu código es más fácil de leer. Todas sus dependencias se definen allí mismo en la parte superior de la clase. Si están dispersos en toda la clase con $container->get('service') entonces puede ser mucho más difícil de entender.

  7. Su código es más fácil de probar por unidad. Si está inyectando el contenedor de servicios, debe burlarse del contenedor de servicios y configurar el simulacro para devolver los simulacros de los servicios pertinentes. Al inyectar los servicios directamente, solo se burla de los servicios y los inyecta, se salta toda una capa de complicaciones.

  8. No te dejes engañar por la falacia de “permite carga lenta”. Puede configurar la carga diferida en el nivel de configuración , simplemente marcando el servicio como lazy: true .

Personalmente, la única vez que inyecté el contenedor de servicio fue la mejor solución posible cuando intenté inyectar el contexto de seguridad en un detector de doctrines. Esto arrojaba una excepción de referencia circular, porque los usuarios estaban almacenados en la base de datos. El resultado fue que la doctrine y el contexto de seguridad dependían unos de otros en el momento de la comstackción. Al inyectar el contenedor de servicio, pude esquivar la dependencia circular. Sin embargo, esto puede ser un olor a código, y hay formas de evitarlo (por ejemplo, al usar el despachador de eventos ), pero admito que la complicación adicional puede superar los beneficios.

No es una buena idea porque haces que tu clase dependa de la DI. ¿Qué sucede cuando algún día decides sacar tu clase y usarla en un proyecto completamente diferente? Ahora no estoy hablando de Symfony o incluso PHP, estoy hablando en general. Entonces, en ese caso, debe asegurarse de que el nuevo proyecto utilice el mismo tipo de mecanismo DI con los mismos métodos compatibles u obtenga excepciones. ¿Y qué sucede si el proyecto no usa DI en absoluto, o usa alguna nueva implementación DI interesante? Tienes que pasar por toda la base de código y cambiar las cosas para admitir el nuevo DI. En proyectos grandes, esto puede ser problemático y costoso, especialmente cuando está sacando más de una clase.

Lo mejor es hacer que tus clases sean lo más independientes posible. Esto significa mantener el DI fuera de su código habitual, algo así como una tercera persona que decide qué va a dónde, puntos a donde debe ir el material, pero no va allí y lo hace él mismo. Así es como lo entiendo.

Aunque, como dijo tomazahlin, estoy de acuerdo en que en proyectos de Symfony en raras ocasiones ayuda a prevenir dependencias circulares. Ese es el único ejemplo donde lo usaría y me aseguraría que esa es la única opción.

Además de todas las desventajas explicadas por otros (sin control sobre servicios usados, comstackción de tiempo de ejecución, dependencias perdidas, etc.)

Hay una razón principal, que rompe la principal ventaja de usar el reemplazo de DIC – Dependencias .

Si el servicio se define en la biblioteca, no podrá reemplazar sus dependencias con las locales que satisfagan sus necesidades.

Solo esta razón es lo suficientemente fuerte como para no inyectar un DIC completo. ¡Acabas de romper toda la idea de reemplazar dependencias ya que son HARDCODED! en servicio;)

Por cierto. No se olvide de requerir interfaces en el constructor del servicio en lugar de clases específicas tanto como pueda, de nuevo una buena sustitución de dependencias.

EDITAR: ejemplo de reemplazo de dependencias

Definición del servicio en algún proveedor:

    

Reemplazo en tu aplicación:

    

Esto le permite reemplazar la lógica del proveedor con la personalizada, pero no olvide implementar la interfaz de clase requerida. Con las dependencias codificadas, no puede reemplazar la dependencia en un solo lugar.

También puede anular el servicio vendor_dependency , pero esto lo reemplazará en todos los lugares, no solo en vendor_service .

Inyectar todo el contenedor no es una buena idea en general. Bueno, funciona, pero ¿por qué inyectar todo el contenedor, mientras que solo necesita algunos otros servicios o parámetros?

En ocasiones, desea inyectar todo el contenedor para evitar referencias circulares, ya que si se inyecta todo el contenedor, se obtiene una “carga diferida” de los servicios que necesita. Un ejemplo sería oyentes de entidades de doctrine.

Puede obtener el contenedor de cada clase que sea “Container Aware” o que tenga acceso al kernel.