Drupal 8: Inyectar correctamente dependencias con DI
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Como estoy seguro de que ya sabe, la inyección de dependencia (DI) y el contenedor de servicios Symfony son importantes nuevas características de desarrollo de Drupal 8. Sin embargo, a pesar de que están empezando a ser mejor comprendidos en la comunidad de desarrollo de Drupal, todavía hay cierta falta de claridad sobre cómo exactamente inyectar servicios en Drupal 8 clases.
Muchos ejemplos hablan de servicios, pero la mayoría cubren sólo la forma estática de cargarlos:
1 |
$service = \Drupal::service('service_name'); |
Esto es comprensible, ya que el enfoque de la inyección adecuada es más detallado, y si ya lo sabes, más bien califica. Sin embargo, el enfoque estático en la vida real sólo debe utilizarse en dos casos:
- En el archivo .module (fuera de un contexto de clase)
- Esas raras ocasiones dentro de un contexto de clase en el que la clase se está cargando sin conocimiento del contenedor de servicio
Aparte de eso, inyectar servicios es la mejor práctica, ya que garantiza el código desacoplado y facilita las pruebas.
En Drupal 8 hay algunas especificidades acerca de la inyección de dependencia que no podrás entender únicamente a partir de un enfoque puro de Symfony. Así que 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 conceptos básicos, vamos a ver tres tipos de ejemplos, en orden de complejidad:
- Inyectar servicios en otro de sus propios servicios
- Inyectar servicios en clases sin servicio
- Inyección de servicios en clases de complemento
Avanzando, la suposición es que usted ya sabe lo que es DI, con qué propósito sirve y cómo el contenedor de servicio lo soporta. Si no, recomiendo el chequear hacia fuera este artículo primero.
Servicios
Inyectar servicios en su propio servicio es muy fácil. Puesto que usted es el que define el servicio, todo lo que tiene que hacer es pasarlo como un argumento al servicio que desea inyectar. Imagine las siguientes definiciones de servicio:
1 |
services: |
2 |
demo.demo_service: |
3 |
class: Drupal\demo\DemoService |
4 |
demo.another_demo_service: |
5 |
class: Drupal\demo\AnotherDemoService |
6 |
arguments: ['@demo.demo_service'] |
Aquí definimos dos servicios donde el segundo toma el primero como argumento constructor. Así que todo lo que tenemos que hacer ahora en la clase AnotherDemoService es almacenarlo como una variable local:
1 |
class AnotherDemoService { |
2 |
/**
|
3 |
* @var \Drupal\demo\DemoService
|
4 |
*/
|
5 |
private $demoService; |
6 |
|
7 |
public function __construct(DemoService $demoService) { |
8 |
$this->demoService = $demoService; |
9 |
}
|
10 |
|
11 |
// The rest of your methods
|
12 |
}
|
Y eso es más o menos. También es importante mencionar que este enfoque es exactamente el mismo que en Symfony, por lo que no hay cambio aquí.
Clases sin servicio
Ahora echemos un vistazo a las clases que a menudo interactuamos con pero que no son nuestros propios servicios. Para comprender cómo se lleva a cabo esta inyección, debe comprender cómo se resuelven las clases y cómo se instancian. Pero lo veremos pronto en la práctica.
Controladores
Las clases de controlador se utilizan principalmente para asignar las rutas de enrutamiento a la lógica empresarial. Se supone que deben permanecer delgados y delegar una lógica de negocios más pesada a los servicios. Muchos extienden la clase ControllerBase y obtienen algunos métodos auxiliares para recuperar servicios comunes del contenedor. Sin embargo, estos se devuelven estáticamente.
Cuando
se está creando un objeto controlador (ControllerResolver::createController), el ClassResolver se utiliza para obtener una
instancia de la definición de clase del controlador. El resolvedor es consciente del contenedor y devuelve una instancia del controlador si el contenedor ya lo tiene. Por el contrario, instancia una nueva y devuelve eso.
Y
aquí es donde tiene lugar nuestra inyección: si la clase que se
resuelve implementa ContainerAwareInterface, la instancia se lleva a
cabo utilizando el método estatico create() en esa clase que recibe todo
el contenedor. Y nuestra clase ControllerBase también implementa ContainerAwareInterface.
Así que echemos un vistazo a un controlador de ejemplo que correctamente inyecta servicios utilizando este enfoque (en lugar de solicitarlos de forma estática):
1 |
/**
|
2 |
* Defines a controller to list blocks.
|
3 |
*/
|
4 |
class BlockListController extends EntityListController { |
5 |
|
6 |
/**
|
7 |
* The theme handler.
|
8 |
*
|
9 |
* @var \Drupal\Core\Extension\ThemeHandlerInterface
|
10 |
*/
|
11 |
protected $themeHandler; |
12 |
|
13 |
/**
|
14 |
* Constructs the BlockListController.
|
15 |
*
|
16 |
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
|
17 |
* The theme handler.
|
18 |
*/
|
19 |
public function __construct(ThemeHandlerInterface $theme_handler) { |
20 |
$this->themeHandler = $theme_handler; |
21 |
}
|
22 |
|
23 |
/**
|
24 |
* {@inheritdoc}
|
25 |
*/
|
26 |
public static function create(ContainerInterface $container) { |
27 |
return new static( |
28 |
$container->get('theme_handler') |
29 |
);
|
30 |
}
|
31 |
}
|
La
clase EntityListController no hace nada para nuestros propósitos aquí,
así que imagínese que BlockListController extiende directamente la clase
ControllerBase, que a su vez implementa el ContainerInjectionInterface.
Como dijimos, cuando este controlador es instanciado, se llama al método estático create(). Su propósito es instanciar esta clase y pasar los parámetros que quiera al constructor de clase. Y dado que el contenedor se pasa a create(), puede elegir qué servicios solicitar y pasar al constructor.
Entonces, el constructor simplemente tiene que recibir los servicios y almacenarlos localmente. Tenga en cuenta que es una mala práctica inyectar el contenedor entero en su clase, y siempre debe limitar los servicios que usted inyecta a los que necesita. Y si usted necesita demasiados, usted es probable que este haciendo algo mal.
Utilizamos este ejemplo de controlador para ir un poco más profundo en el enfoque de 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 sean conscientes de los contenedores, pero no cubriremos esto aquí. Veamos otros ejemplos de clases con las que puedes interactuar y en las que deberías inyectar servicios.
Formularios
Las formas son otro gran ejemplo de las clases donde usted necesita inyectar servicios. Normalmente, puede extender las clases de FormBase o ConfigFormBase que ya implementan ContainerInjectionInterface. En este caso, si anula los métodos create() y constructor, puede inyectar lo que quiera. Si
no desea extender estas clases, todo lo que tiene que hacer es
implementar esta interfaz usted mismo y seguir los mismos pasos que
hemos visto anteriormente con el controlador.
Como
ejemplo, echemos un vistazo al SiteInformationForm que extiende el
ConfigFormBase y verá cómo inyecta servicios encima del config.factory
que su superior necesita:
1 |
class SiteInformationForm extends ConfigFormBase { |
2 |
|
3 |
...
|
4 |
public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) { |
5 |
parent::__construct($config_factory); |
6 |
|
7 |
$this->aliasManager = $alias_manager; |
8 |
$this->pathValidator = $path_validator; |
9 |
$this->requestContext = $request_context; |
10 |
}
|
11 |
|
12 |
/**
|
13 |
* {@inheritdoc}
|
14 |
*/
|
15 |
public static function create(ContainerInterface $container) { |
16 |
return new static( |
17 |
$container->get('config.factory'), |
18 |
$container->get('path.alias_manager'), |
19 |
$container->get('path.validator'), |
20 |
$container->get('router.request_context') |
21 |
);
|
22 |
}
|
23 |
|
24 |
...
|
25 |
}
|
Como
antes, el método create() se utiliza para la instancia, que pasa al
constructor el servicio requerido por la clase padre así como algunos
adicionales que necesita en la parte superior.
Y esto es más o menos cómo funciona la inyección de constructor básica en Drupal 8. Está disponible en casi todos los contextos de clase, excepto para algunos en los que la parte de instanciación todavía no se resolvió de esta manera (por ejemplo, los complementos FieldType). Además, hay un subsistema importante que tiene algunas diferencias, pero es crucialmente importante para entender: plugins.
Plugins
El sistema de complementos es un componente muy importante de Drupal 8 que proporciona mucha funcionalidad. Así que vamos a ver cómo funciona la inyección de dependencia con las clases de complemento.
La
diferencia más importante en cómo la inyección se maneja con los
enchufes es las clases del complemento de la interfaz necesitan
implementar: ContainerFactoryPluginInterface. La razón es que los complementos no se resuelven pero son administrados por un administrador de complementos. Así que cuando este gestor necesita instanciar uno de sus plugins, lo hará utilizando una fábrica. Y por lo general, esta fábrica es el 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 método usual create(), también se
pasan las variables $configuration, $plugin_id y $plugin_definition
(que son las tres variables básicas Parámetros de cada plugin viene con).
Así que veamos dos ejemplos de plugins que inyectan servicios. En primer lugar, el complemento principal de UserLoginBlock (@Block):
1 |
class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface { |
2 |
|
3 |
...
|
4 |
|
5 |
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) { |
6 |
parent::__construct($configuration, $plugin_id, $plugin_definition); |
7 |
|
8 |
$this->routeMatch = $route_match; |
9 |
}
|
10 |
|
11 |
/**
|
12 |
* {@inheritdoc}
|
13 |
*/
|
14 |
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
15 |
return new static( |
16 |
$configuration, |
17 |
$plugin_id, |
18 |
$plugin_definition, |
19 |
$container->get('current_route_match') |
20 |
);
|
21 |
}
|
22 |
|
23 |
...
|
24 |
}
|
Como
puede ver, implementa la ContainerFactoryPluginInterface y
el método create() recibe esos tres parámetros adicionales. Éstos
se pasan entonces en el orden correcto al constructor de clase, y desde
el contenedor se solicita y se pasa un servicio también. Este es el ejemplo más básico, pero de uso general, de inyectar servicios en clases de complemento.
Otro ejemplo interesante es el complemento FileWidget (@FieldWidget):
1 |
class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface { |
2 |
|
3 |
/**
|
4 |
* {@inheritdoc}
|
5 |
*/
|
6 |
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) { |
7 |
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); |
8 |
$this->elementInfo = $element_info; |
9 |
}
|
10 |
|
11 |
/**
|
12 |
* {@inheritdoc}
|
13 |
*/
|
14 |
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
15 |
return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('element_info')); |
16 |
}
|
17 |
...
|
18 |
}
|
Como
puede ver, el método create() recibe los mismos parámetros, pero el
constructor de la clase espera otros adicionales que son específicos
para este tipo de complemento. Esto no es un problema. Por lo general, se pueden encontrar dentro de la array $configuration de ese complemento en particular y se pasa desde allí.
Así que estas son las principales diferencias cuando se trata de inyectar servicios en clases de complemento. Hay una interfaz diferente para implementar y algunos parámetros adicionales en el método create().
Conclusión
Como hemos visto en este artículo, hay una serie de maneras en que podemos poner nuestros servicios en Drupal 8. A veces tenemos que solicitarlos estáticamente. Sin embargo, la mayoría de las veces no debemos hacerlo. Y hemos visto algunos ejemplos típicos de cuándo y cómo debemos inyectarlos en nuestras clases en su lugar. También hemos visto las dos interfaces principales que las clases necesitan implementar para ser instanciadas con el contenedor y estar listas para 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 en otras clases de ese tipo. Si son plugins, compruebe si alguno de los padres implementa ContainerFactoryPluginInterface. Si no, hágalo usted mismo para su clase y asegúrese de que el constructor reciba lo que espera. También echa un vistazo a la clase de administrador de plugins responsable y vea qué fábrica utiliza.
En otros casos, como con clases TypedData como el FieldType, eche un vistazo a otros ejemplos en el núcleo. Si
ve a otros usuarios utilizando servicios cargados estáticamente, lo más
probable es que aún no esté listo para la inyección, por lo que tendrá
que hacer lo mismo. Pero mantenga un ojo hacia fuera, porque esto podría cambiar en el futuro.



