Drupal 8 Inyectando adecuadamente las dependencias usando DI

Como estoy seguro de que ya saben, la inyección de dependencia (DI) y el contenedor del servicio de Symfony son características nuevas e importantes del desarrollo de Drupal 8. Sin embargo, a pesar de que están empezando a entenderse mejor en la comunidad de desarrollo de Drupal, todavía falta algo. de claridad sobre cómo inyectar servicios exactamente en las clases de Drupal 8.

Muchos ejemplos hablan de servicios, pero la mayoría cubre solo la forma estática de cargarlos:

$ service = \ Drupal :: service ('service_name');

Esto es comprensible, ya que el enfoque de inyección adecuado es más detallado y, si ya lo sabe, es mejor dicho. Sin embargo, el enfoque estático en vida real Solo debe utilizarse en dos casos:

  • en el .módulo archivo (fuera de un contexto de clase)
  • esas raras ocasiones dentro de un contexto de clase donde la clase se está cargando sin conocimiento del contenedor de servicio

Aparte de eso, los servicios de inyección son la mejor práctica, ya que aseguran el código desacoplado y facilitan las pruebas.

En Drupal 8 hay algunas especificidades acerca de la inyección de dependencia que no podrás entender únicamente desde un enfoque de Symfony puro. Entonces, en este artículo vamos a ver algunos ejemplos de inyección de constructor adecuada en Drupal 8. Para este fin, pero también para cubrir todos los aspectos básicos, veremos tres tipos de ejemplos, en orden de complejidad:

  • Inyectando servicios en otro de tus propios servicios.
  • Inyectando servicios en clases no-servicio.
  • servicios de inyección en clases de plugin

En el futuro, se supone que ya sabe qué es DI, a qué sirve y cómo lo soporta el contenedor de servicios. Si no, recomiendo revisar este artículo primero.

Servicios

Inyectar servicios en su propio servicio es muy fácil. Ya que usted es quien define el servicio, todo lo que tiene que hacer es pasarlo como un argumento al servicio que desea inyectar. Imagina las siguientes definiciones de servicio:

servicios: demo.demo_service: clase: Drupal \ demo \ DemoService demo.another_demo_service: clase: Drupal \ demo \ AnotherDemoService argumentos: ['@ demo.demo_service']

Aquí definimos dos servicios donde el segundo toma el primero como un argumento constructor. Así que todo lo que tenemos que hacer ahora en el OtroDemoServicio clase es almacenarlo como una variable local:

class AnotherDemoService / ** * @var \ Drupal \ demo \ DemoService * / private $ demoService; función pública __construct (DemoService $ demoService) $ this-> demoService = $ demoService;  // El resto de tus métodos 

Y eso es prácticamente todo. También es importante mencionar que este enfoque es exactamente el mismo que en Symfony, por lo que no hay cambios aquí.

Clases sin servicio

Ahora echemos un vistazo a las clases con las que a menudo interactuamos, pero que no son nuestros propios servicios. Para comprender cómo se realiza esta inyección, debe comprender cómo se resuelven las clases y cómo se crean instancias. Pero eso lo veremos en la práctica pronto..

Controladores

Las clases de controladores se utilizan principalmente para asignar rutas de enrutamiento a la lógica empresarial. Se supone que deben permanecer delgados y delegar la lógica empresarial más pesada a los servicios. Muchos extienden el ControllerBase Clase y obtenga algunos métodos de ayuda para recuperar servicios comunes del contenedor. Sin embargo, estos son devueltos estáticamente.

Cuando se crea un objeto controlador (ControllerResolver :: createController), la ClassResolver Se utiliza para obtener una instancia de la definición de clase de controlador. El resolvedor es consciente del contenedor y devuelve una instancia del controlador si el contenedor ya lo tiene. A la inversa, crea una instancia de una nueva y devuelve. 

Y aquí es donde tiene lugar nuestra inyección: si la clase que se resuelve implementa la ContainerAwareInterface, La instanciación se lleva a cabo mediante el uso de la estática crear() Método en esa clase que recibe el contenedor completo. Y nuestro ControllerBase clase también implementa el ContainerAwareInterface.

Así que echemos un vistazo a un controlador de ejemplo que inyecta los servicios correctamente utilizando este enfoque (en lugar de solicitarlos de forma estática):

/ ** * Define un controlador para listar bloques. * / class BlockListController extiende EntityListController / ** * El controlador del tema. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / protected $ themeHandler; / ** * Construye el BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * El controlador del tema. * / public function __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container) return new static ($ container-> get ('theme_handler')); 

los EntityListController La clase no hace nada para nuestros propósitos aquí, así que imagina que BlockListController extiende directamente el ControllerBase clase, que a su vez implementa la ContainerInjectionInterface.

Como dijimos, cuando este controlador es instanciado, la estática crear() se llama metodo Su propósito es crear una instancia de esta clase y pasar los parámetros que quiera al constructor de la clase. Y como el contenedor pasa a crear(), Puede elegir qué servicios solicitar y transmitir al constructor.. 

Entonces, el constructor simplemente tiene que recibir los servicios y almacenarlos localmente. Tenga en cuenta que es una mala práctica inyectar todo el contenedor en su clase, y siempre debe limitar los servicios que inyecte a los que necesita. Y si necesita demasiados, es probable que esté haciendo algo mal..

Utilizamos este ejemplo de controlador para profundizar un poco más en el enfoque de la inyección de dependencia de Drupal y entender cómo funciona la inyección de constructor. También hay posibilidades de inyección de setter haciendo que las clases estén al tanto del contenedor, pero no lo cubriremos aquí. En su lugar, veamos otros ejemplos de clases con las que puede interactuar y en las que debe inyectar servicios..

Formas

Las formas son otro gran ejemplo de clases en las que necesita inyectar servicios. Por lo general, o bien se extiende la FormBase o ConfigFormBase Clases que ya implementan el ContainerInjectionInterface. En este caso, si anula la crear() Y los métodos de construcción, puedes inyectar lo que quieras. Si no desea extender estas clases, todo lo que tiene que hacer es implementar esta interfaz usted mismo y seguir los mismos pasos que vimos anteriormente con el controlador..

Como ejemplo, echemos un vistazo a la SiteInformationForm que extiende el ConfigFormBase y ver cómo inyecta servicios encima del config.factory sus padres necesitan:

clase SiteInformationForm extiende ConfigFormBase … public function __construct (ConfigFactoryInterface $ config_factory, AliasManagerInterface $ alias_manager, PathValidatorInterface $ path_validator, RequestContext $ request_context) parent :: __ construct ($ config_factory); $ this-> aliasManager = $ alias_manager; $ this-> pathValidator = $ path_validator; $ this-> requestContext = $ request_context;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container) devolver nueva estática ($ container-> get ('config.factory'), $ container-> get ('path.alias_manager') , $ container-> get ('path.validator'), $ container-> get ('router.request_context')); …

Como antes, el crear() Se utiliza un método para la creación de instancias, que pasa al constructor el servicio requerido por la clase principal, así como algunos adicionales que necesita en la parte superior..

Y así es como funciona la inyección del constructor básico en Drupal 8. Está disponible en casi todos los contextos de clase, a excepción de unos pocos en los que la parte de creación de instancias aún no se resolvió de esta manera (por ejemplo, los complementos FieldType). Además, hay un subsistema importante que tiene algunas diferencias pero que es crucial entender: los complementos.

Complementos

El sistema de complementos es un componente muy importante de Drupal 8 que alimenta una gran cantidad de funcionalidad. Así que veamos cómo funciona la inyección de dependencia con las clases de plugin..

La diferencia más importante en la forma en que se maneja la inyección con los complementos es la clase de interfaz que las clases de complementos deben implementar: ContainerFactoryPluginInterface. La razón es que los complementos no se resuelven, pero son administrados por un administrador de complementos. Entonces, cuando este administrador necesite crear una instancia de uno de sus complementos, lo hará usando una fábrica. Y por lo general, esta fábrica es la ContainerFactory (o una variación similar de la misma). 

Así que si nos fijamos en ContainerFactory :: createInstance (), Vemos que aparte del contenedor que se pasa al habitual crear() método, el $ configuracion, $ plugin_id, y $ plugin_definition las variables también se pasan (que son los tres parámetros básicos con los que viene cada complemento).

Así que veamos dos ejemplos de tales complementos que inyectan servicios. Primero, el núcleo. UserLoginBlock enchufar (@Bloquear):

la clase UserLoginBlock amplía los implementos BlockBase ContainerFactoryPluginInterface … public function __construct (array $ configuration, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) parent :: __ construct ($ configuration, $ plugin_id, $ plugin_definition); $ this-> routeMatch = $ route_match;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) return new static ($ configuration, $ plugin_id, $ plugin_definition, $ container-> get ( 'current_route_match')); …

Como puedes ver, implementa el ContainerFactoryPluginInterface y el crear() El método recibe esos tres parámetros extra. Estos se pasan luego en el orden correcto al constructor de la clase, y desde el contenedor también se solicita y pasa un servicio. Este es el ejemplo más básico, aunque comúnmente utilizado, de inyectar servicios en clases de complementos.

Otro ejemplo interesante es el FileWidget enchufar (@ FieldWidget):

la clase FileWidget amplía los implementos WidgetBase ContainerFactoryPluginInterface / ** * @inheritdoc * / public function __construct ($ plugin_id, $ plugin_definition, FieldDefinitionEntrentaciónLetroneraControl de la Válvula en el interior de la mente plugin_id, $ plugin_definition, $ field_definition, $ settings, $ third_party_settings); $ this-> elementInfo = $ element_info;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) devolver nueva static ($ plugin_id, $ plugin_definition, $ configuration ['field_definition'], $ configuración ['configuración'], $ configuración ['third_party_settings'], $ container-> get ('element_info')); …

Como puedes ver, la crear() El método recibe los mismos parámetros, pero el constructor de la clase espera otros adicionales que son específicos de este tipo de complemento. Esto no es un problema. Por lo general, se pueden encontrar dentro de la $ configuracion matriz de ese plugin en particular y pasado de allí.

Así que estas son las principales diferencias cuando se trata de inyectar servicios en clases de complementos. Hay una interfaz diferente para implementar y algunos parámetros adicionales en el crear() método.

Conclusión

Como hemos visto en este artículo, hay varias formas en que podemos obtener servicios en Drupal 8. A veces tenemos que solicitarlos de forma estática. Sin embargo, la mayoría de las veces no deberíamos. Y hemos visto algunos ejemplos típicos de cuándo y cómo deberíamos inyectarlos en nuestras clases. También hemos visto las dos interfaces principales que las clases necesitan implementar para poder crear una instancia con el contenedor y estar listas para la inyección, así como la diferencia entre ellas..

Si está trabajando en un contexto de clase y no está seguro de cómo inyectar servicios, comience a buscar otras clases de ese tipo. Si son complementos, verifique si alguno de los padres implementa el ContainerFactoryPluginInterface. Si no es así, hágalo usted mismo para su clase y asegúrese de que el constructor reciba lo que espera. También puedes ver la clase de administrador de complementos responsable y ver qué fábrica utiliza. 

En otros casos, como con las clases TypedData como la Tipo de campo, Eche un vistazo a otros ejemplos en el núcleo. Si ve que otros utilizan servicios cargados estáticamente, es muy probable que todavía no esté listo para la inyección, por lo que tendrá que hacer lo mismo. Pero mantente atento, porque esto podría cambiar en el futuro..