12.15. Services et Injection de Dépendances.
Lorsque nous utilisons Drupal et que nous avons besoin d'utiliser le code d'un module contrib ou du noyau dans un module personnalisé, nous utilisons des hooks et des services. Nous avons déjà utilisé des hooks dans cet article :
12.11.3. Hooks pour travailler avec les Entités.
Voyons maintenant les services. Un service est un objet PHP. Donc, lorsque vous créez une nouvelle classe PHP dans votre module personnalisé, il est préférable de la concevoir dès le départ comme un service, afin que votre code puisse être utilisé dans un autre module de manière standard par la suite.
Drupal rassemble tous les services dans un objet PHP appelé Service Container, ainsi Drupal stocke toutes les informations sur les services disponibles et utilisés en un seul endroit. Vous pouvez accéder à cet objet et voir quels services sont utilisés :
<?php
$container = \Drupal::getContainer();
?>
Documentation de Drupal::getContainer()
Vous pouvez manipuler cet objet via les méthodes has/get/set, mais généralement nous ajoutons les services au container via des fichiers *.services.yml dans nos modules.
Voyons l'implémentation de la méthode getContainer()
:
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container is not initialized yet. \\Drupal::setContainer() must be called with a real container.');
}
return static::$container;
}
?>
La variable du Service Container est statique, ce qui signifie qu'après l'appel de index.php et jusqu'à la fin du traitement de la requête, on peut accéder à cette variable depuis n'importe quel fichier, que ce soit dans une classe, un hook de module ou même un fichier .theme de thème.
Comment utiliser les services dans Drupal ?
Passons maintenant à l'utilisation du Service Container dans Drupal. L'objet $container
contient les objets services, ce qui permet d'effectuer toute la logique nécessaire à la création de l'objet dans le constructeur, et de nous fournir un objet prêt à l'emploi dans notre module personnalisé. Par exemple, si nous devons écrire une requête SQL, nous appelons simplement l'objet de la base de données depuis le Service Container. Cet objet utilise déjà les credentials de notre fichier settings.php et établira la connexion MySQL lors de l'exécution de la requête :
$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 vous regardez l'implémentation de la méthode database()
, vous verrez que nous utilisons l'objet service database du Service Container :
Documentation de Drupal::database()
<?php
public static function database() {
return static::getContainer()
->get('database');
}
?>
Ainsi, nous ne chargeons que les classes nécessaires à notre code au moment opportun. C'est pour cela que nous utilisons un stockage unique d'objets : le Service Container.
Comment ajouter un service au Service Container ?
Quand nous créons un fichier *.services.yml, Drupal charge les services définis et stocke leurs objets dans le Service Container.
core/modules/syslog/syslog.services.yml :
services:
logger.syslog:
class: Drupal\syslog\Logger\SysLog
arguments: ['@config.factory', '@logger.log_message_parser']
tags:
- { name: logger }
Vous pouvez aussi ajouter un service au container via la méthode set()
, mais ceci est principalement utilisé pour mocker des dépendances dans les tests :
Qu'est-ce que l'Injection de Dépendances ?
Si vous exécutez Code Sniffer, il vous indiquera que l'appel à Drupal::database()
doit être corrigé en passant la base de données via le constructeur de la classe où le service est utilisé. Appeler un objet du Service Container dans le constructeur d'une classe s'appelle Injection de Dépendances (DI). Par exemple :
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
/**
* Contrôleur de formulaire WisenetConfigurationForm.
*
* Exemple simple avec un champ texte.
* Hérite de FormBase, la classe de base la plus simple.
*
* @see \Drupal\Core\Form\FormBase
*/
class WisenetGetCourseForm extends FormBase {
/**
* Connexion active à la base de données.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructeur.
*
* @param \Drupal\Core\Database\Connection $database
* La connexion à la base de données.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
...
/**
* Sauvegarde des données de cours.
*
* Fonction pour sauvegarder les données dans le type de contenu cours.
*/
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();
...
}
}
Dans cet exemple, la requête à la base de données est nécessaire dans le formulaire, donc nous avons ajouté la méthode create()
qui est utilisée pour créer l'instance de la classe. Cette méthode peut exister dans différentes classes et interfaces, mais elle prend toujours en paramètre une variable $container
de type ContainerInterface
. Si dans la méthode create()
on appelle un objet depuis le Service Container avec $container->get('myservice.name')
, alors l'objet retourné est passé en argument au constructeur __construct()
(ici $container->get('database')
est injecté dans Connection $database
).
Nous verrons dans les prochains articles comment appeler correctement les objets du Service Container dans les contrôleurs, blocs, formulaires (BaseForm), formulaires de configuration (ConfigForm) et classes/services personnalisés.
Une fois que nous aurons vu comment injecter et utiliser correctement les objets du Service Container, nous verrons comment créer nos propres services.
Nous aborderons également comment surcharger des classes pour les services afin d'utiliser une classe personnalisée à la place d'une classe d'un module contrib.
Pourquoi utiliser un Service Container et l’Injection de Dépendances ?
Nous pourrions utiliser les namespaces et inclure le code des modules directement, instancier des objets tiers partout où nécessaire sans utiliser de Service Container. Mais cela cause des problèmes lors des mises à jour. Par exemple, si nous devons remplacer la classe d’envoi d’emails appelée dans 200 endroits différents. Pour faciliter la maintenance, on crée un service au lieu d’inclure des fichiers directement. Ainsi, si l’on veut passer d’envoi par PHP mail() à SMTP, on change juste la classe du service, pas tous les appels dans le code.
L’Injection de Dépendances résout aussi le problème des appels multiples au service dans une même classe. Pas besoin d’appeler plusieurs fois le Service Container si on utilise un service dans plusieurs méthodes : on stocke l’objet service dans une propriété de la classe et on l’utilise via $this->serviceName
.
Bien sûr, on peut se passer du Service Container et de l’Injection de Dépendances, mais ces patterns uniformisent le code, le simplifient et facilitent sa maintenance.
Où puis-je trouver le nom d’un service ?
Dans notre exemple, le service s’appelle « database » :
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
// Ici on ajoute le nom du service.
$container->get('database'),
);
}
Mais si vous ajoutez un service depuis un module contrib ou personnalisé, il peut avoir un nom du type :
nom_module.nom_service
Vous pouvez vérifier le nom du service dans le fichier *.services.yml. Le nom ne commence pas forcément par module_name.
, mais c’est souvent le cas. Par exemple :
/**
* {@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')
);
}