Drupal Service Decorators : L'art de modifier l'existant sans tout casser

Surchargez proprement vos services avec le Drupal Service Decorator. Un guide technique pour développeurs avancés : Dependency Injection, services.yml et bonnes pratiques.

Illusration : Drupal Service Decorator

On a tous connu ce moment de solitude face à une demande client un peu trop spécifique.

Vous savez, ce moment où le service par défaut du cœur de Drupal fait 95% du travail, mais qu'il manque ce petit détail crucial pour le projet. La tentation est grande, surtout quand on débute, de copier-coller la classe entière dans un module custom, ou pire (ne mentez pas, on a les noms), de patcher directement le fichier source en se disant "je ferai ça proprement plus tard".

Soyons clairs : c'est la meilleure recette pour transformer une maintenance de routine en cauchemar lors de la prochaine mise à jour de sécurité.

Heureusement, si vous cherchez à surcharger un service Drupal proprement, l'architecture du CMS nous offre une solution élégante issue des design patterns orientés objet : le Drupal Service Decorator.

Au lieu de remplacer brutalement une classe ou de bidouiller des hooks à n'en plus finir, le décorateur nous permet d'envelopper un service existant pour en modifier la logique métier sans toucher à son intégrité. C’est propre, c’est pérenne, et c’est totalement intégré au conteneur de Drupal Dependency Injection.

Dans cet article, on va voir ensemble comment implémenter ce pattern pour altérer le comportement natif de Drupal sans jamais compromettre la stabilité de votre application. Vous allez voir, une fois qu'on a compris la mécanique du fichier services.yml, c'est un outil dont on ne peut plus se passer.

Partie 1 : Le concept (Promis, on fait court)

Avant d'ouvrir votre IDE, il faut bien visualiser ce que nous allons construire. Le Decorator Pattern n'est pas une invention spécifique à Drupal ; c'est un grand classique des Design Patterns (merci le Gang of Four) utilisé massivement dans le développement logiciel moderne.

Pour faire simple : imaginez des poupées russes.

Le service original de Drupal (celui que vous voulez modifier) est la petite poupée au centre. Votre Service Decorator, c'est la poupée plus grande qui vient englober la première.

Schéma UML simplifié du pattern Decorator

De l'extérieur, pour le reste de l'application, rien ne change. Votre décorateur "ressemble" au service original car il implémente exactement la même Interface. Mais techniquement, c'est votre classe qui intercepte les appels en premier.

Pourquoi ne pas simplement faire un extends ?

C'est la question que tout développeur PHP se pose au début. "Pourquoi s'embêter avec un décorateur alors que je pourrais juste étendre la classe originale et surcharger la méthode ?"

La réponse tient en un principe clé : Composition over Inheritance (La composition plutôt que l'héritage).

Si vous étendez directement une classe du cœur de Drupal (extends CoreClass), vous créez un couplage fort. Le jour où une mise à jour mineure de Drupal modifie le constructeur de cette classe parente ou marque une méthode comme final, votre site plante (WSOD).

Avec un Décorateur, c'est différent :

  1. Isolation : Vous injectez le service original à l'intérieur de votre service (Composition).
  2. Sécurité : Si le code interne du service original change, votre décorateur continue souvent de fonctionner tant que l'Interface (le contrat) est respectée.
  3. Cumulable : Le plus beau ? On peut empiler plusieurs décorateurs sur un même service. Module A décore le service, puis Module B décore le résultat du Module A. C'est infiniment plus souple que l'héritage linéaire.

En résumé, décorer un service, c'est appliquer une couche de vernis custom sur une logique existante, sans jamais rayer la peinture d'origine.

Partie 2 : La mise en place technique (Le "Hard" Skill)

La beauté du décorateur sous Drupal, c'est qu'il ne nécessite aucune librairie externe ou hack obscur. Tout se passe entre votre déclaration de service (YAML) et votre classe (PHP).

1. La déclaration : services.yml

C'est ici que tout se joue. Pour dire à Drupal "Hé, n'utilise pas le service standard, utilise le mien à la place", nous allons utiliser la propriété magique decorates.

Voici à quoi ressemble une déclaration typique dans votre fichier mon_module.services.yml :

services:
  mon_module.mon_super_decorator:
    class: Drupal\mon_module\MyServiceDecorator
    # C'est cette ligne qui fait le lien
    decorates: system.service_original
    # La priorité définit l'ordre d'exécution si plusieurs décorateurs existent
    decoration_priority: 10
    # On masque ce service car il n'a pas vocation à être appelé directement
    public: false
    arguments:
      # L'argument magique : on injecte le service qu'on est en train de remplacer !
      - '@mon_module.mon_super_decorator.inner'
      - '@current_user' # On peut aussi injecter d'autres services si besoin

Les points clés pour le SEO technique (et pour que ça marche) :

  • decorates : L'ID du service que vous voulez surcharger (ex: entity_type.manager ou logger.factory).
  • .inner : Remarquez l'argument '@mon_module.mon_super_decorator.inner'. Drupal renomme automatiquement l'ancien service en lui ajoutant le suffixe .inner. C'est crucial : c'est ce qui nous permettra d'appeler la méthode originale à la fin de notre traitement.
  • decoration_priority : Plus le chiffre est élevé, plus votre décorateur sera exécuté "tôt" (c'est-à-dire qu'il sera la couche la plus externe de l'oignon).

2. La Classe PHP : L'art de la délégation

Maintenant, créons notre classe. La règle d'or ici est le Type Hinting. Votre classe doit implémenter la même Interface que le service original. Sinon, Drupal rejettera votre décorateur lors de la compilation du conteneur.

Voici une structure robuste que j'utilise sur mes projets :

namespace Drupal\mon_module;

// On importe l'interface du service original
use Drupal\Core\SomeComponent\OriginalServiceInterface;

class MyServiceDecorator implements OriginalServiceInterface {

  /**
   * Le service original décoré (la poupée interne).
   *
   * @var \Drupal\Core\SomeComponent\OriginalServiceInterface
   */
  protected $innerService;

  /**
   * Constructeur.
   *
   * @param \Drupal\Core\SomeComponent\OriginalServiceInterface $inner_service
   * Le service original renommé.
   */
  public function __construct(OriginalServiceInterface $inner_service) {
    $this->innerService = $inner_service;
  }

  /**
   * Méthode que l'on souhaite modifier.
   */
  public function laMethodeCible($arg1) {
    // 1. Logique AVANT (Ex: Vérification de permission, Log)
    // ... votre code custom ...

    // 2. Appel du service original (Délégation)
    $result = $this->innerService->laMethodeCible($arg1);

    // 3. Logique APRÈS (Ex: Modification du résultat)
    return $result;
  }

  /**
   * L'astuce de Senior : La méthode magique __call.
   *
   * Elle permet de déléguer automatiquement toutes les autres méthodes
   * de l'interface sans avoir à les réécrire une par une.
   */
  public function __call($method, $args) {
    return call_user_func_array([$this->innerService, $method], $args);
  }
}

L'astuce __call : C'est souvent ce qui différencie un code verbeux d'un code maintenable. Si l'interface du service original contient 20 méthodes et que vous ne voulez en modifier qu'une seule, utiliser __call vous évite d'écrire 19 méthodes "passe-plats" inutiles.

Partie 3 : Cas concret – "Le Gardien des Emails"

Passons à la pratique. Imaginons que le cahier des charges impose une restriction métier stricte : aucun email provenant du domaine @spambot.com ne doit être considéré comme valide par Drupal.

Si vous étiez un développeur junior, vous auriez probablement cherché à utiliser un hook_form_alter sur le formulaire d'inscription. Mais que se passe-t-il si l'utilisateur est créé via une API REST ? Ou via une commande Drush ? Votre hook est contourné.

C'est là que le Service Decorator brille. En surchargeant le validateur d'email au niveau le plus bas, on sécurise toutes les portes d'entrée simultanément.

1. Le fichier my_security.services.yml

On cible le service core email.validator.

services:
  my_security.email_validator_decorator:
    class: Drupal\my_security\EmailValidatorDecorator
    # On cible le service qui gère la validation des emails dans le Core
    decorates: email.validator
    # Une priorité haute pour passer avant d'autres potentiels validateurs
    decoration_priority: 10
    public: false
    arguments:
      # Injection de l'instance originale (le service natif)
      - '@my_security.email_validator_decorator.inner'

2. La Classe EmailValidatorDecorator.php

C'est ici qu'on implémente notre logique métier. Notez l'importance de respecter l'interface EmailValidatorInterface.

namespace Drupal\my_security;

use Drupal\Component\Utility\EmailValidatorInterface;

/**
 * Décore le validateur d'email pour bloquer les domaines interdits.
 */
class EmailValidatorDecorator implements EmailValidatorInterface {

  /**
   * Le service original (le coeur de Drupal).
   *
   * @var \Drupal\Component\Utility\EmailValidatorInterface
   */
  protected $innerService;

  /**
   * Liste des domaines blacklistés (pourrait venir de la config).
   */
  protected $blacklistedDomains = ['spambot.com', 'evil-corp.com'];

  /**
   * Constructeur.
   */
  public function __construct(EmailValidatorInterface $inner_service) {
    $this->innerService = $inner_service;
  }

  /**
   * {@inheritdoc}
   */
  public function isValid($email, $checkDNS = FALSE) {
    // 1. Notre logique custom "AVANT"
    // On extrait le domaine de l'email
    $parts = explode('@', $email);
    $domain = array_pop($parts);

    if (in_array($domain, $this->blacklistedDomains)) {
      // Si le domaine est interdit, on refuse immédiatement
      // sans même déranger le service parent.
      return FALSE;
    }

    // 2. Délégation "PENDANT"
    // Si notre check passe, on laisse le service natif faire son travail
    // (vérifier la syntaxe RFC, le DNS, etc.)
    return $this->innerService->isValid($email, $checkDNS);
  }
}

Pourquoi c'est élégant ?

Regardez bien ce qui se passe dans la méthode isValid. Si le domaine est interdit, je retourne FALSE directement. Je n'appelle même pas $this->innerService->isValid(). J'ai court-circuité la logique standard pour imposer ma règle métier, économisant au passage des ressources (pourquoi faire une vérification DNS coûteuse sur un domaine qu'on sait interdit ?).

À l'inverse, si le domaine est autorisé, je rends la main au Core. Je ne réinvente pas la roue de la validation RFC des emails (ce qui serait complexe et source d'erreurs), je me contente d'ajouter ma petite couche de sécurité par-dessus.

C'est ça, la puissance du décorateur : Intercepter, Filtrer, Déléguer.

Partie 4 : Les pièges à éviter (Parce qu'on est entre pros)

C'est bien beau de tout décorer, mais comme tout outil puissant, il est possible de se tirer une balle dans le pied si on manque de rigueur. Voici trois points de vigilance que j'ai relevés au fil de mes projets.

1. La guerre des priorités (decoration_priority)

Si vous utilisez plusieurs modules contribs qui décorent tous le même service (par exemple entity_type.manager), l'ordre d'exécution devient capital. Rappelez-vous de l'image de l'oignon ou des poupées russes :

  • Priorité haute = Couche extérieure (s'exécute en premier).
  • Priorité basse = Couche intérieure (s'exécute juste avant le service original).

Si votre décorateur doit absolument passer avant un autre (pour bloquer une action par exemple), assurez-vous de définir une priorité supérieure dans votre YAML.

2. Le débogage peut devenir... intéressant

Quand vous débuggez un service décoré, get_class($service) ne va plus vous retourner Drupal\Core\...\OriginalClass, mais Drupal\mon_module\MonDecorateur. Si vous avez empilé 3 décorateurs, la stack trace peut devenir intimidante.

Mon conseil : Utilisez la commande Drush pour inspecter votre conteneur et vérifier qui décore quoi : drush devel:services | grep mon_service (si vous avez le module Devel) ou vérifiez simplement votre compilation dans le dossier sites/default/files/php.

3. Ne faites pas de la "Sur-ingénierie"

Le décorateur est l'outil ultime quand on doit remplacer ou bloquer une logique. Cependant, si Drupal propose déjà un Event (via EventSubscriber) ou un Hook pour faire ce que vous voulez, utilisez-les.

  • Besoin d'agir après qu'une entité soit sauvée ? -> hook_entity_insert.
  • Besoin de changer la façon dont l'entité est sauvée ? -> Decorateur.

Choisir le bon outil, c'est ce qui différencie le senior du bricoleur.

Conclusion

Le Drupal Service Decorator est probablement l'un des patterns les plus sous-estimés par les développeurs intermédiaires. Il offre une propreté de code et une stabilité face aux mises à jour que les "hacks" à l'ancienne ne pourront jamais égaler.

En maîtrisant ce concept, vous ne subissez plus l'architecture de Drupal : vous la pliez à vos besoins, proprement. C'est exactement le genre de dette technique négative (ou d'investissement technique) qui fait la différence sur la longévité d'un projet complexe.

Alors la prochaine fois que vous vous apprêtez à dire à votre client "Ce n'est pas possible, c'est le fonctionnement natif de Drupal", relisez votre fichier services.yml. Il y a sûrement un décorateur pour ça.


🚀 Besoin d'une architecture Drupal sur-mesure ?

Vous avez un projet complexe qui nécessite de sortir des sentiers battus sans casser le Core ? Je suis développeur Drupal spécialisé dans les architectures techniques avancées et le développement de modules custom.

Contactez-moi pour discuter de votre projet