You are on page 1of 5

Drupal 8 : L'injection de dépendance

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 de la
dépendance dans les arguments. 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 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 8, 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.

You might also like