Drupal 10 : L'injection de dépendance

Comment exploiter la puissance des services en fonction du contexte dans lequel on est en train de développer.

Cet article a été initialement rédigé pour Drupal 8 mais son contenu est toujours d'actualité pour Drupal 9 et Drupal 10.

N'hésitez pas à nous contacter ou à vous inscrire à notre formation «Drupal pour les développeurs et développeuses» pour en savoir plus !

Définition

L’injection de dépendance par constructeur

Nous avons vu grâce au Conteneur de Services qu’il était possible de réutiliser les objets et d’interchanger leur implémentation. Toute application ayant un minimum de valeur ajoutée, il est probable que tous ces objets aient des liens entre eux. Bien souvent, on parle de dépendance(s). L’Injection de dépendance est donc un gros mot pour désigner une façon de créer les instances des objets et de lier les objets entre eux. L’Injection de dépendance est l’un des nombreux design patterns utilisés dans Drupal.

Si l’on reprend notre exemple de Service utilisé dans le chapitre précédent, nous avons vu un Service simple, prenons maintenant l’exemple d’un Service plus compliqué, qui, pour fonctionner, doit utiliser un autre Service.

L’exemple est celui du Service flood qui permet de limiter le nombre d’actions d’un utilisateur.

Voici la définition du Service, nous allons le détailler juste après.

# core.services.yml
flood:
 class: Drupal\Core\Flood\DatabaseBackend
 arguments: ['@database', '@request_stack']
 tags:
   - { name: backend_overridable }

Et voici le constructeur ainsi que la méthode register() de la classe Flood\DatabaseBackend.

# DatabaseBackend.php
class DatabaseBackend implements FloodInterface {

/**
 * The database connection used to store flood event information.
 *
 * @var \Drupal\Core\Database\Connection
 */
protected $connection;

/**
 * The request stack.
 *
 * @var \Symfony\Component\HttpFoundation\RequestStack
 */
protected $requestStack;

/**
 * Construct the DatabaseBackend.
 *
 * @param \Drupal\Core\Database\Connection $connection
 *   The database connection which will be used to store the flood event
 *   information.
 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
 *   The request stack used to retrieve the current request.
 */
public function __construct(Connection $connection, RequestStack $request_stack) {
  $this->connection = $connection;
  $this->requestStack = $request_stack;
}

/**
 * Implements Drupal\Core\Flood\FloodInterface::register().
 */
public function register($name, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = $this->requestStack->getCurrentRequest()->getClientIp();
  }
  $this->connection->insert('flood')
    ->fields(array(
      'event' => $name,
      'identifier' => $identifier,
      'timestamp' => REQUEST_TIME,
      'expiration' => REQUEST_TIME + $window,
    ))
    ->execute();
 }
}

Dans cet exemple la méthode register() du Service flood sauve en base de données l’événement réalisé par un utilisateur (l’utilisateur étant identifié par son IP).

On peut voir que pour sauver l’action de l’utilisateur en base de données nous avons besoin d’utiliser la connexion à la base de données et de récupérer l’IP de l’utilisateur.

Toujours dans l’esprit de garder du code facilement interchangeable, nous ne voulons pas écrire au sein d’une méthode du code qui récupérerait la connexion à la base de données directement, nous voulons que cette connexion nous soit envoyée lorsque nous instancions notre objet.
De cette façon, si la connexion se fait sur une base MySQL ou Cassandra ou est un faux objet retournant des valeurs en dur pour les tests, cela ne fait aucune différence pour nous (et il en va de même pour l’IP de l’utilisateur).

Dans le constructeur de la classe, nous récupérons la connexion à la base de données et la requête de l’utilisateur. Ces deux informations étant indispensables, on peut donc dire qu’il y a deux dépendances sur le Service flood, et ces dépendances sont injectées via le constructeur.

Lors de la définition du Service, on indique les dépendances via une arobase @ suivie du nom du service (donc la dépendance) dans les arguments du constructeur. Dans notre cas la base de données (@database) et la requête qui vient d’être effectuée (@request_stack).

# core.services.yml
flood:
 class: Drupal\Core\Flood\DatabaseBackend
 arguments: ['@database', '@request_stack']
 tags:
   - { name: backend_overridable }

L’Injection de dépendance se fait dans le constructeur de la classe Flood\DatabaseBackend, de cette façon :

public function __construct(Connection $connection, RequestStack $request_stack) {
 $this->connection = $connection;
 $this->requestStack = $request_stack;
}

Les deux arguments du constructeur indiquent que notre classe a besoin de ces deux objets sous peine de ne pas pouvoir fonctionner. On garde la référence à nos deux arguments en les stockant comme attributs de la classe, ce qui permet de les utiliser par la suite au sein de nos méthodes.

public function register($name, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
  // Récupération de l'adresse IP du client depuis l'objet "requestStack".
  $identifier = $this->requestStack->getCurrentRequest()->getClientIp();
  }
  // Utilisation de l'objet "connection" pour requêter la base de données.
  $this->connection->insert('flood')
    ->fields(array(
      'event' => $name,
      'identifier' => $identifier,
      'timestamp' => REQUEST_TIME,
      'expiration' => REQUEST_TIME + $window,
    ))
    ->execute();
 }
}

Notre exemple nous permet d’illustrer la méthode la plus classique pour injecter des dépendances entre nos Services. Il s’agit d’une Injection de dépendance par constructeur car notre objet ne peut pas fonctionner si l’on ne lui fourni pas ses dépendances. Les dépendances ne pourront pas non plus être modifiées durant la vie de l’objet (le constructeur n’étant appelé qu’une seule fois).

Il existe deux autres façons d’injecter les dépendances vers les objets, on qualifiera ces formes d’injection comme “injection par setter”.

L’injection de dépendance par setter

L’autre possibilité pour définir une dépendance est de passer les objets utilisés par ce que l’on appelle un setter. Il s’agit d’une méthode d’une classe qui définit (“to set” en anglais) la valeur d’un attribut. Elle est accompagnée de sa méthode inverse, le getter, qui permet de retourner la valeur de l’attribut.

Exemple avec la classe FormBase au sein de laquelle il est possible, entre autres, de définir / récupérer le chemin vers le formulaire.

# FormBase.php
abstract class FormBase implements FormInterface, ContainerInjectionInterface {
  // The request stack.
  protected $requestStack; // \Symfony\Component\HttpFoundation\RequestStack.
  // Gets the request object.
  protected function getRequest() {
    if (!$this->requestStack) {
      $this->requestStack = \Drupal::service('request_stack');
    }
    return $this->requestStack->getCurrentRequest();
  }
  // Sets the request stack object to use.
  public function setRequestStack(RequestStack $request_stack) {
    $this->requestStack = $request_stack;
    return $this;
  }
}

Quel est l’intérêt de cette méthode comparée à l’Injection de dépendance par constructeur ?

Avec cette méthode, il devient possible d’avoir des dépendances optionnelles vers d’autres objets.
Si dans le code de votre classe il est possible de faire appel à un autre objet mais que la classe n’en a pas absolument besoin pour fonctionner, vous pouvez utilisez cette méthode pour injecter votre dépendance. Il est également possible d’utiliser cette méthode si, au cours de la vie de l’objet, la valeur de la dépendance doit changer.
Dans notre exemple, un même formulaire peut être appelé de différents endroits, on utilise donc une injection par setter pour spécifier le chemin d’où est appelé le formulaire.

Il existe une troisième méthode pour injecter des dépendances qui consiste à définir directement la valeur d’un attribut public. Nous ne détaillerons pas cette méthode car c’est une pratique peu recommandée, aucun contrôle sur les données ne pouvant être fait facilement.

L’injection de dépendances appliquée aux Services

Nous l’avons vu dans le chapitre précédent, on peut manipuler les Services via le Conteneur de Services. Dans Drupal 10, pour accéder à un Service, il va falloir passer par le Conteneur de Services.

Les choses se complexifient légèrement car, selon ce que vous implémentez, il ne sera pas possible d’accéder au Conteneur de Services de la même façon.

Cas 1 : Je développe une classe de Service

C’est le cas le plus simple qui est celui que nous avons vu précédemment avec le Service flood, vous implémentez un Service qui a des dépendances obligatoires sur d’autres Services.
Dans ce cas là, pas besoin de manipuler le conteneur directement, Drupal se charge de l’instanciation des objets pour vous, il vous suffit de déclarer le
Service et de stocker les dépendances passées au constructeur.

Déclaration du Service :

# core.services.yml
flood:
 class: Drupal\Core\Flood\DatabaseBackend
 arguments: ['@database', '@request_stack']
 tags:
   - { name: backend_overridable }

Stockage des dépendances envoyées au constructeur.

public function __construct(Connection $connection, RequestStack $request_stack) {
  $this->connection = $connection;
  $this->requestStack = $request_stack;
}

L’utilisation du Conteneur de Services vous est transparente.

Cas 2 : J’ai besoin de passer un Service au constructeur d’une Factory

Autre cas, vous implémentez des plugins, étendez des contrôleurs d’entités ou toute autre classe faisant appel à une Factory nécessitant le Conteneur de Services.
Dans ce cas là, vous aurez à respecter le contrat des interfaces des
Factories qui implémentent l’une des méthodes create() ou createInstance().
Dans la signature de ces méthodes, vous retrouverez la présence d’un argument
$container de type \Symfony\Component\DependencyInjection\ContainerInterface.
Cet argument vous permettra alors de récupérer les
Services à transmettre au constructeur de la classe. (Nous verrons comment savoir quelle Factory appeler dans l’implémentation d’un Service de récupération des couvertures.)

Exemple de l’utilisation du Conteneur de Services.

# CommentStorage.php
class CommentStorage extends SqlContentEntityStorage implements CommentStorageInterface {
 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) {
  return new static(
   $entity_info,
   $container->get('database'),
   $container->get('entity.manager'),
   $container->get('current_user'),
   $container->get('cache.entity'),
   $container->get('language_manager')
  );
 }
}

Cas 3 : Le Conteneur de Services ne m’est pas directement transmis

Il se peut que vous vous retrouviez à implémenter une classe qui n’est ni un Service ni l’implémentation d’un Plugin, contrôleur d’Entité, etc. Dans ce cas là vous n’avez aucune méthode appelée par le système à laquelle est transmis le Conteneur de Services. Dans cette situation, la seule façon d’accéder à un Service est de passer par la méthode statique service() de la classe Drupal.

Exemple d’utilisation :

# MyController.php

// Récupérer le service tour depuis mon contrôleur.
$tour_service = \Drupal::service('tour');

C’est la solution à utiliser en dernier recours et qu’il faut tenter d’éviter aux maximum pour garder votre application découplée et donc facilement testable, refactorable.

Commentaires

Romain J Jeudi 31 mars 2016 - 10:13
Très bon aperçu de l'injection de dépendance dans Drupal 8. La dernière façon de récupérer un service est malheureusement la seule disponible lorsque l'on utilise les fonctions de hook. On ne peut donc pas l'éviter. C'est l'une des raisons pour laquelle les hooks vont disparaitre totalement de Drupal.
En réponse à par Romain J
DuaelFr Jeudi 31 mars 2016 - 10:18
Tu as totalement raison. En ce qui concerne la disparition des hooks, on a un peu de marge devant nous. Ils ne feront probablement qu'être remplacés par une version plus "objet" et pas avant Drupal 9 pour conserver la rétro-compatibilité.

Votre commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.
Votre adresse servira à afficher un Gravatar et à vous notifier des réponses. Votre commentaire sera anonymisé si ce billet est dépublié pendant plus de 3 mois.
Pour lutter contre le spam notre système enregistre votre adresse IP et votre adresse e-mail si vous la partagez.
Nous vous invitons à consulter notre politique de confidentialité pour comprendre les traitements faits de ces données et comment les rectifier.

À propos de Julien

Co-fondateur - Scrum master & Expert technique

Utilisateur de Drupal depuis 2008, j’ai fait mes armes comme développeur chez Commerce Guys puis me suis mis à encadrer les nouveaux arrivants avant de donner des formations, participer aux avant ventes et accompagner les équipes au passage à Scrum.

Je suis impliqué dans la communauté française de Drupal depuis 2009, j’ai été tour à tour président puis vice-président de l’association Drupal France et francophonie entre 2011 et 2013.