12.15. Servicios y Dependency Injection.
Cuando usamos Drupal y necesitamos utilizar el código de un módulo contrib o del núcleo en un módulo personalizado, empleamos hooks y servicios. Ya hemos usado hooks en este artículo:
12.11.3. Hooks para trabajar con Entity.
Ahora vamos a entender qué son los servicios. Un servicio es un objeto PHP. Por lo tanto, cuando creas una nueva clase PHP en tu módulo personalizado, es mejor definirla desde el principio como un servicio, para que luego puedas reutilizar ese código en otro módulo de forma estándar.
Drupal recopila todos los servicios en un objeto PHP llamado Service Container, así que Drupal almacena la información de todos los servicios disponibles en un solo lugar. Puedes acceder a este objeto y ver qué servicios estás utilizando:
<?php
$container = \Drupal::getContainer();
?>
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x
Puedes trabajar con este objeto usando los métodos has/get/set, pero lo más habitual es agregar servicios al contenedor a través de los archivos *.services.yml en nuestros módulos.
Veamos la implementación del método getContainer():
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container no ha sido inicializado. \\Drupal::setContainer() debe ser llamado con un contenedor válido.');
}
return static::$container;
}
?>
La variable del contenedor de servicios está definida como estática, lo que significa que después de que se llame a index.php y hasta el final del ciclo de ejecución, se puede acceder a esta variable desde cualquier lugar del código: una clase, un hook o incluso un archivo .theme.
¿Cómo utilizar servicios en Drupal?
Veamos ahora cómo usar el Service Container en Drupal. El objeto $container contiene objetos de servicios, lo que permite ejecutar toda la lógica necesaria para crear un objeto en su constructor y proporcionarnos el objeto ya listo para usar en nuestro módulo personalizado. Por ejemplo, si necesitamos hacer una consulta SQL, simplemente llamamos al objeto de base de datos desde el contenedor de servicios y este ya utilizará las credenciales del archivo settings.php y establecerá la conexión con MySQL al ejecutar la consulta:
$query = \Drupal::database()->select('node_field_data', 'n');
$query->addField('n', 'nid');
$query->condition('n.title', 'About Us');
$query->range(0, 1);
$nid = $query->execute()->fetchField();
Si observas la implementación del método database(), verás que usa el servicio 'database' desde el Service Container:
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3Adatabase/9.2.x
<?php
public static function database() {
return static::getContainer()
->get('database');
}
?>
Así conectamos solo las clases que necesitamos en ese momento. Para esto usamos el contenedor de servicios como almacén centralizado de objetos.
¿Cómo agregar un servicio al Service Container?
Cuando creamos un archivo *.services.yml, Drupal carga los servicios definidos en ese archivo y los guarda en el contenedor.
https://api.drupal.org/api/drupal/core%21modules%21syslog%21syslog.services.yml/9.2.x
core/modules/syslog/syslog.services.yml:
services:
logger.syslog:
class: Drupal\syslog\Logger\SysLog
arguments: ['@config.factory', '@logger.log_message_parser']
tags:
- { name: logger }
También puedes agregar un servicio a la variable $container usando el método set(), aunque esto suele usarse para simular (mock) dependencias en pruebas:
¿Qué es Dependency Injection?
Si ejecutas Code Sniffer, te mostrará un error indicando que deberías reemplazar \Drupal::database() y usar el servicio database en el constructor de la clase. Cuando llamas a un objeto del Service Container desde el constructor de una clase, eso se llama Dependency Injection (DI). Por ejemplo:
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
class WisenetGetCourseForm extends FormBase {
protected $database;
public function __construct(Connection $database) {
$this->database = $database;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
public function saveCourse($courses) {
$query = $this->database->select('node__field_course_wisenet_id', 'nc');
$query->addField('n', 'nid');
$query->join('node_field_data', 'n', 'nc.entity_id = n.nid');
$query->condition('nc.field_course_wisenet_id_value', $course['CourseOfferId']);
$query->range(0, 1);
$nid = $query->execute()->fetchField();
}
En este ejemplo, se necesita hacer una consulta a la base de datos desde un formulario, así que se añadió un método create(), que Drupal usa para instanciar la clase. Este método siempre recibe como parámetro una variable $container del tipo ContainerInterface. Si dentro del método create() se llama a un servicio con $container->get('nombre_del_servicio'), el objeto devuelto se pasa al constructor como argumento (en este caso, $container->get('database') se pasa como Connection $database).
Más adelante veremos cómo inyectar correctamente objetos del contenedor en controladores, bloques, formularios base y formularios de configuración, así como en clases y servicios personalizados.
Después de comprender cómo conectar e inyectar objetos desde el contenedor, aprenderemos a crear nuestros propios servicios.
También veremos cómo sobrescribir clases de servicios, para que podamos usar una clase de un módulo personalizado en lugar de la de un módulo contrib.
¿Por qué necesitamos Service Container y Dependency Injection?
Podemos usar namespaces y llamar a clases directamente desde otros módulos, sin contenedor. Pero esto trae problemas de mantenimiento. Por ejemplo, si necesitamos reemplazar la clase para enviar emails y esa clase se usa en 200 lugares, sería muy difícil actualizar todo. En cambio, si usamos un servicio, solo necesitamos cambiar la clase del servicio en un lugar.
Dependency Injection también evita llamadas duplicadas al contenedor dentro de una misma clase. Podemos guardar el servicio en una propiedad de la clase y usarlo como $this->serviceName.
Por supuesto, podríamos prescindir del Service Container y de DI, pero estos patrones unifican el código, lo hacen más limpio y fácil de mantener.
¿Dónde puedo ver el nombre de un servicio?
En nuestro ejemplo, el servicio es "database":
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
// Aquí agregamos el nombre del servicio.
$container->get('database'),
);
}
Pero si agregas un servicio desde un módulo contrib o personalizado, podría tener un nombre como:
nombre_del_módulo.nombre_del_servicio
Puedes verificar el nombre del servicio en el archivo *.services.yml. No siempre empieza con el nombre del módulo, pero normalmente sí. Por ejemplo:
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('commerce_cart.cart_provider'),
$container->get('entity_type.manager')
);
}