Lorsque l'on se plonge dans le développement Drupal, l'un des premiers concepts que l'on apprend à connaître est celui des services, ainsi que d'autres concepts tels que l'injection de dépendances, qui est étroitement liée aux services.


Le concept de services a été introduit après la refonte et la réarchitecture de Drupal 7 en Drupal 8, où le paradigme est passé à la programmation orientée objet (POO). Les services sont devenus l'une des pièces maîtresses du noyau de Drupal, nous permettant d'utiliser les services déjà définis pour interagir avec Drupal.


Les services sont des objets gérés par le conteneur de services. Leur objectif principal est de découpler les fonctionnalités réutilisables en rendant ces objets enfichables et remplaçables en les enregistrant auprès d'un conteneur de services. 
Mais les services peuvent également être “décorés” pour offrir plus de fonctionnalités et de flexibilité grâce aux balises de service. Découvrons-les !
 

Les balises de service

Dans Drupal, les balises sont utilisées pour indiquer qu'un service doit être enregistré ou utilisé d'une manière particulière; ou qu'il appartient à une catégorie. Par exemple, lors de la création d'un abonné à un événement, vous marquerez votre service avec la balise event_subscriber. Cela indique que le service est un abonné d'événement qui doit être déclenché et traité par le collecteur de service Event Subscriber.


Les balises de service permettent de regrouper des services en se basant sur des fonctionnalités ou des caractéristiques communes. Cela permet à Drupal de retrouver et de rassembler dynamiquement des services appartenant à la même catégorie.


Drupal est suffisamment souple pour prendre en charge des balises de service personnalisées. Et heureusement pour nous, la création d'une balise personnalisée est assez simple !
 

Les balises de service personnalisées

Pour créer des balises de service personnalisées, vous avez besoin d'un collecteur de services et d'une interface commune que les services balisés doivent implémenter. Dans ce guide, nous utiliserons l'exemple de la création d'un module d’évaluation pour les produits alimentaires avec sa propre balise de service personnalisée.

L'interface commune

Tous les services qui utiliseront la balise de service personnalisé doivent implémenter une interface commune qui spécifie les méthodes que tous les services marqués doivent avoir. Cette interface commune garantit que tous les services regroupés sous la même catégorie de balise conservent des fonctionnalités et des comportements cohérents.
En ce qui concerne les services du module d’évaluation pour les produits alimentaires , ils doivent implémenter l'interface suivante :
 

<?php

namespace Drupal\food_review;

/**
* Interface for food reviewer services.
*/
interface FoodReviewerInterface {

 /**
  * Get the food host.
  *
  * @return string
  *   The host name.
  */
 public function getHost(): string;

 /**
  * Set the food host.
  *
  * @param string $host
  *   The host name.
  */
 public function setHost(string $host): void;

 /**
  * Reviews a dish.
  *
  * @param string $dish
  *   The dish id.
  *
  * @return int
  *   A review score from 0 to 10.
  */
 public function review(string $dish_id): int;

}

Collecteur de services

Un collecteur de services, comme son nom l'indique, collecte et traite tous les services d'une balise spécifique. Le collecteur de services est un objet PHP normal qui doit avoir au moins une méthode ; c'est cette méthode qui est chargée de traiter les services balisés, un par un.
 

La méthode doit accepter les paramètres suivants :

  1. Interface commune (obligatoire) - représente le service découvert lui-même et doit être associé à l'interface commune liée à la balise de service. 
    ID (optionnel) - l'ID du service balisé 
    Priorité (optionnel) - la priorité du service balisé
    Autres paramètres personnalisés (facultatif) - vous pouvez définir d'autres paramètres dont vous estimez que votre service balisé a besoin.

Pour notre cas d'utilisation, nous créons un collecteur d’évaluations sur les produits alimentaires :

<?php

namespace Drupal\food_review;

/**
* The food reviewer manager.
*/
class FoodReviewerManager {

 /**
  * The tagged reviewer services.
  *
  * The array is divided by category and priority.
  *
  * @var array
  */
 protected array $reviewers;

 /**
  * Adds a food reviewer service to the reviewers property.
  *
  * @param \Drupal\food_review\FoodReviewerInterface $food_reviewer_service
  *   The food reviewer service.
  * @param string $id
  *   The service ID.
  * @param int $priority
  *   The service priority.
  * @param string $category
  *   The food review category.
  */
 public function addReviewer(FoodReviewerInterface $food_reviewer_service, string $id, int $priority, string $category): void {
   $food_reviewer_service->setHost($this->getHostByCategory($id, $category));
   $this->reviewers[$category][$priority][] = $food_reviewer_service;
 }

 /**
  * Get the reviewer host by category.
  *
  * @param string $id
  *   The service ID.
  * @param string $category
  *   The service category.
  *
  * @return string
  *   The host name.
  */
 private function getHostByCategory(string $id, string $category): string {
   // ...
   return $host;
 }

}

Pour ce collecteur, nous ajoutons le service de révision lui-même, l'identifiant du service, la priorité et une propriété personnalisée nommée catégorie. Nous ajoutons également une nouvelle étape lors de la découverte d'un service balisé - dans ce cas, nous ajoutons l'hôte.


Une fois le collecteur de services créé, nous devons l'ajouter au conteneur de services afin qu'il puisse être découvert par Drupal. Un collecteur de services est un service simple, ce qui signifie que pour qu'il soit découvert, il suffit de l'ajouter au YAML des services et de le baliser.

services:
 food_review.food_reviewer_manager:
   class: Drupal\food_review\FoodReviewerManager
   tags:
 	- { name: service_collector, tag: food_review.food_reviewer, call: addReviewer }

Lorsque vous marquez un service avec la balise service_collector, en plus de l’établir comme un collecteur de services personnalisé, vous devez fournir les propriétés suivantes :

  1. Tag - le nom du tag du collecteur de service personnalisé ; s'il n'est pas fourni, l'ID du service par défaut sera utilisé comme tag.
  2. Call - la méthode qui doit être exécutée lors de la découverte d'un service balisé avec notre propre balise ; la valeur par défaut est "addHandler" si elle n'est pas fournie.
  3. Required - La valeur par défaut est false ; sinon, au moins un service avec la balise personnalisée doit exister.

Si votre collecteur de services dépend d'autres services, vous pouvez les injecter comme d'habitude. Rappelez-vous qu'un collecteur de services n'est qu'un simple service balisé.


Maintenant que nous disposons de la balise de service personnalisé, créons un service balisé - dans notre exemple, un critique gastronomique.
 

<?php

namespace Drupal\food_review;

/**
* A sea food reviewer service.
*/
class SeaFoodReviewer implements FoodReviewerInterface {

 /**
  * The sea food host.
  *
  * @var string
  */
 protected string $host;

 /**
  * {@inheritdoc}
  */
 public function getHost(): string {
   return $this->host;
 }

 /**
  * {@inheritdoc}
  */
 public function setHost(string $host): void {
   $this->host = $host;
 }

 /**
  * {@inheritdoc}
  */
 public function review(string $dish_id): float {
   // ...
   return $score;
 }

}

Comme vous pouvez le voir, le service a implémenté l'interface FoodReviewerInterface et toutes ses méthodes.


La dernière étape consiste à créer et à balisétiqueter le service dans Drupal :

services:
 food_review.food_reviewer_manager:
   class: Drupal\food_review\FoodReviewerManager
   tags:
 	- { name: service_collector, tag: food_review.food_reviewer, call: addReviewer }

 food_review.seafood_reviewer:
   class: Drupal\food_review\SeaFoodReviewer
   tags:
 	- { name: food_review.food_reviewer, priority: 1, category: seafood }

Le service a été créé ! Nous l'avons correctement étiqueté avec notre propre balise de service personnalisée, et nous avons ajouté la priorité ainsi que les propriétés de la catégorie.

Utilisation du collecteur de services et les balises de service

Maintenant que nous avons créé le collecteur de services et utilisé la balise de service pour étiqueter les services que nous voulons catégoriser, nous pouvons personnaliser et utiliser le collecteur de services.


L'un des avantages de l'utilisation des balises de service est que le collecteur de services peut recevoir plusieurs services de processeur ou leurs identifiants lors de l'instanciation. Cela permet d'établir une architecture extensible dans laquelle le collecteur de services peut interagir avec plusieurs processeurs (comme les services balisés) sans être étroitement lié à eux.


En outre, le collecteur de services reçoit des instances de tous les processeurs enregistrés lors de son instanciation, ce qui les rend immédiatement disponibles. Ces processeurs sont ajoutés dynamiquement au moment de l'exécution. Cela permet une plus grande flexibilité lors de la modification ou de l'extension du comportement du service collecteur ou des processeurs eux-mêmes.


Dans notre exemple ci-dessus, lorsqu'un processeur est ajouté, nous définissons l'hôte du processeur par le biais d'un mécanisme qui appartient au collecteur de services lui-même, ce qui enlève la responsabilité au processeur et permet au collecteur de services de faire le gros du travail. Ainsi, lorsque de nouveaux évaluateurs de produits alimentaires sont ajoutés, il suffit de les baliser et les autres processus (tels que la définition de l'hôte) peuvent être délégués au collecteur de services.


Mais l'exemple ci-dessus est très simple et ne prend pas en compte l'avantage d'avoir tous les services regroupés. 
Supposons donc que nous ayons besoin d'évaluer un plat en fonction d'une catégorie et que nous devions récupérer toutes les évaluations de tous les évaluateurs disponibles. Ce que nous pouvons faire, c'est ajouter cette fonctionnalité dans le collecteur de services :
 

<?php

namespace Drupal\food_review;

/**
* The food reviewer manager.
*/
class FoodReviewerManager {

 /**
  * The tagged reviewer services.
  *
  * The array is divided by category and priority.
  *
  * @var array
  */
 protected array $reviewers;

 // ...

 // ...

 /**
  * Processes a dish review by category.
  *
  * All the available food reviewers for the category will be processed.
  *
  * @param string $category
  *   The food category.
  * @param string $dish_id
  *   The dish to review ID.
  *
  * @return array
  *   The reviews of the dish are split by priority.
  */
 public function processReviewsByCategory(string $category, string $dish_id): array {
   $reviews = [];

   foreach ($this->reviewers[$category] as $priority => $reviewers) {
 	/** @var \Drupal\food_review\FoodReviewerInterface $reviewer */
 	foreach ($reviewers as $reviewer) {
   	$reviews[$priority][] = [
     	'host' => $reviewer->getHost(),
     	'review_score' => $reviewer->review($dish_id),
   	];
 	}
   }

   return $reviews;
 }

}

Dans la méthode ci-dessus, nous parcourons les évaluateurs d'aliments de la catégorie choisie et récupérons l'évaluation de chacun des évaluateurs disponibles. Nous pouvons parcourir les évaluateurs parce que le collecteur de services les possède.


Si, par exemple, nous devons exécuter le code ci-dessus pour calculer la note moyenne d'un plat, nous pouvons utiliser le collecteur de services :

/**
 * Calculates the average of a dish.
 */
function _food_review_calculate_average(string $category, string $dish_id) {
 /** @var \Drupal\food_review\FoodReviewerManager */
  $food_manager = \Drupal::service('food_review.food_reviewer_manager');
  $reviews = $food_manager->processReviewsByCategory($category, $dish_id);
  // ...
  // ...
  return $average;
}

Bien sûr, il serait plus logique d'ajouter la méthode de calcul de la moyenne dans le collecteur de services, mais il ne s'agit que d'un simple exemple. 
Comme vous pouvez le voir, nous utilisons le collecteur de services comme un service normal, puis nous appelons la méthode pour obtenir les avis. Le collecteur de services peut obtenir tous les avis parce qu'il possède tous les services balisés. 


La plupart des collecteurs de services Drupal Core fonctionnent généralement en itérant à travers les services balisés et en leur fournissant les données dont ils peuvent avoir besoin pour effectuer des tâches spécifiques. 


Prenons l'exemple de \Drupal\Core\Cache\CacheTagsInvalidator, qui est un collecteur de service balisé avec le nom cache_tags_invalidator. Ce collecteur est responsable de la gestion de l'invalidation des balises de cache dans Drupal. Il itère à travers tous les processeurs d'invalidation de balises disponibles associés à la balise cache_tags_invalidator.


Au cours de cette itération, le service CacheTagsInvalidator appelle la méthode invalidateTags de l'interface CacheTagsInvalidatorInterface. Cette méthode nécessite généralement des informations sur les balises de cache qui sont transmises au collecteur de service lui-même.


Par conséquent, le tableau des balises de cache fourni au collecteur de services est ensuite transmis à chaque processeur d'invalidation des balises par l'intermédiaire de la méthode invalidateTags. Cela permet à ces processeurs d'effectuer leurs tâches spécifiques liées à l'invalidation des balises de cache en utilisant les données fournies.
 

Mises en garde à prendre en compte

En reprenant l'exemple ci-dessus, que se passerait-il si vous utilisiez un service d'évaluation des produits alimentaires ? Dans notre cas, le service food_review.seafood_reviewer.


Vous pourriez l'injecter ou le récupérer dans le conteneur du service. Mais la propriété de l'hôte serait-elle disponible ?


Si le collecteur de services n'a pas été instancié au moment où le service de fruits de mer est instancié, alors tout le code qui est exécuté lorsqu'un service balisé est ajouté au collecteur de services n'a pas encore été exécuté. Une erreur serait donc générée si vous appeliez la fonction \Drupal::service('food_review.seafood_reviewer')->getHost().


L'erreur est due au fait que la propriété $host n'est pas encore instanciée, car la propriété est instanciée lorsque le collecteur de service est instancié. Naturellement, le code pourrait être remanié pour tenir compte de ces cas (par exemple, en vérifiant d'abord si la propriété est déjà définie, etc.) Le scénario inverse pourrait cependant fonctionner.


Rappelez-vous que le code du collecteur de services n'est pas exécuté lors de la découverte du service, mais jusqu'à ce que le collecteur de services lui-même ait été instancié (c'est-à-dire lorsque nous l'utilisons).

Bien qu'il n'y ait pas de meilleures pratiques concernant les balises de service, je pense que les services balisés ne devraient pas être utilisés (ou du moins pas invoqués) directement à travers le conteneur de service. Au lieu de cela, l'interaction devrait se faire à travers le collecteur de balises de service. De cette façon, nous pouvons minimiser les effets de bord. Si nous avons besoin d'utiliser un service balisé, le fait de le récupérer à partir du collecteur garantit que toutes transformations devant être effectuées sont déjà faites par le collecteur. Les éléments de preuve numérique de Drupal attestent du fait que les services balisés ne sont pas invoqués à travers le conteneur; seulement à travers le collecteur de services.
 

Le dernier mot sur les balises de service

Les balises de service offrent un moyen puissant de catégoriser les services dans Drupal sur la base de fonctionnalités ou de caractéristiques partagées. En attribuant des balises aux services, les développeurs peuvent regrouper des services apparentés en vue d'une recherche dynamique, ce qui permet un comportement plus souple et plus adaptatif du système.


Ceci est particulièrement utile dans les scénarios où le système doit s'adapter en fonction de fonctionnalités ou de caractéristiques spécifiques. Par exemple, les balises de service permettent une catégorisation efficace et un traitement collectif lorsque plusieurs services partagent une fonctionnalité ou un comportement commun, mais diffèrent dans leur mise en œuvre. 


Ce mécanisme permet aux développeurs d'effectuer des actions ou des tâches basées sur ces services groupés, ce qui contribue à créer des flux de travail rationalisés et adaptables au sein de l'écosystème Drupal.


Bien entendu, les balises de service ont leurs inconvénients. La décision de les utiliser doit correspondre aux exigences architecturales du projet, de la même manière que le couplage doit être évité, en raison de la nature du fonctionnement des collecteurs de services. Si nécessaire, Drupal propose d'autres approches architecturales qui prennent en compte ces inconvénients.


J'espère que vous avez trouvé ce guide instructif et utile. Bon codage !