Il y a un paradoxe dans beaucoup de bases de code Drupal : le framework repose sur Symfony depuis la version 8, mais les modules custom ignorent souvent les composants Symfony les plus utiles. On réécrit un gestionnaire de file d'attente maison, on modélise un workflow dans des hooks imbriqués, on valide des données d'entrée à la main — alors que Symfony Messenger, Symfony Workflow et le Serializer font déjà tout ça, de manière testable et documentée.
Ce n'est pas un manque de compétence. C'est un angle mort d'apprentissage : les développeurs Drupal apprennent les API Drupal, pas les composants Symfony que Drupal embarque. Résultat, le code custom s'accumule — difficile à tester, difficile à transmettre à une nouvelle équipe, coûteux à maintenir.
Pourquoi le code custom Drupal réinvente des briques Symfony
Drupal n'est pas construit sur Symfony : il est construit avec Symfony. Depuis la version 8, le conteneur de services, le routing, la gestion des événements et les requêtes HTTP reposent sur des composants Symfony. Drupal a cependant ses propres API de haut niveau — hook_, EntityTypeManager, FormBase — et les développeurs travaillent naturellement à ce niveau.
Le problème apparaît quand un besoin sort du cadre natif. Besoin d'une file d'attente asynchrone ? On écrit une implémentation maison. Besoin de sérialiser un objet métier complexe ? On boucle sur les champs. Besoin de limiter un endpoint API à N appels par minute ? On stocke un compteur en base de données.
Ces solutions fonctionnent jusqu'à un certain seuil. Puis elles deviennent des pièges : non testables unitairement, dépendantes de l'état global de Drupal, impossibles à documenter sans l'auteur original.
Les cinq composants Symfony à connaître dans un contexte Drupal
1. HttpClient — sortir des appels HTTP bricolés
Drupal embarque \Symfony\Component\HttpClient. Si votre module custom appelle une API externe via un GuzzleHttp\Client configuré manuellement, HttpClient offre une alternative plus propre : un client mockable dans les tests via MockHttpClient et MockResponse, une gestion native du retry avec backoff exponentiel, et une configuration par service déclarée dans la définition du service Drupal.
2. Serializer — structurer la transformation des données
Le composant Serializer de Symfony normalise et dénormalise des objets en JSON, XML ou tout autre format. Écrire la transformation dans des Normalizer dédiés plutôt qu'en code procédural dans un contrôleur produit du code testable, extensible, et lisible par quelqu'un qui ne connaît pas le contexte métier du projet.
La règle d'intégration sans couplage excessif : un Normalizer métier ne doit pas avoir de dépendance directe sur EntityTypeManager. Si c'est le cas, la logique de récupération des données et la logique de transformation sont mélangées — elles méritent d'être séparées.
3. Messenger — déléguer ce qui ne doit pas bloquer la requête
Symfony Messenger est le composant de messagerie asynchrone de Symfony. Drupal l'embarque depuis la version 9.2. Tout ce qui n'exige pas de réponse immédiate peut être envoyé dans un bus de messages et traité en arrière-plan : import de données volumineuses, notifications, synchronisation CRM.
Ce qui change : la requête HTTP se termine vite, et le traitement lourd est délégué à un worker. La gestion des erreurs et les tentatives de retry deviennent explicites dans les MessageHandler — plutôt que noyées dans des hooks.
4. Workflow — modéliser les états explicitement
Symfony Workflow modélise les transitions d'état d'un objet. Pour les entités métier non-Drupal ou les processus qui dépassent le cycle de vie éditorial, c'est une option plus légère que content_moderation. Un workflow déclaré en YAML est auditable par un non-développeur, ce qui réduit la dépendance à la mémoire institutionnelle d'un développeur qui aurait quitté le projet.
5. RateLimiter — protéger les endpoints sans plomberie manuelle
Symfony RateLimiter implémente les stratégies classiques de limitation de débit : token bucket, fixed window, sliding window. Sans lui, on trouve souvent des implémentations avec des requêtes SQL à chaque appel, des caches Drupal détournés pour stocker des compteurs, ou aucune limitation. Intégré dans un service injecté dans un contrôleur custom, il produit un code que n'importe quel développeur backend peut lire sans connaître les internals de Drupal.
Quand intégrer, quand s'abstenir
Trois signaux qui indiquent qu'un composant Symfony vaut la peine d'être introduit :
- Le code n'a pas de dépendance fonctionnelle à Drupal — transformation de données, validation de schéma, orchestration de messages. Un service Symfony pur sera plus testable.
- Le besoin correspond à un problème générique déjà résolu. Réimplémenter un composant mature crée de la dette sans avantage.
- L'équipe doit pouvoir faire évoluer le comportement sans passer par la couche Drupal.
À l'inverse, quand le besoin est profondément lié au cycle de vie des entités Drupal, aux permissions, au cache ou à la recherche, les API Drupal sont le bon niveau d'abstraction.
Ce que ça change sur la maintenance à long terme
Réduire la dette technique d'une base Drupal ne demande pas de refonte complète. Ça demande d'introduire les bons composants dans les nouveaux développements, de limiter le code custom aux cas où ni Drupal ni Symfony ne couvrent le besoin, et de vérifier systématiquement si un composant Symfony embarqué dans Drupal résout déjà ce qu'on s'apprête à coder.
Les bénéfices sont cumulatifs : tests plus rapides, documentation implicite dans la configuration des workflows et des bus de messages, base de code qu'une équipe peut prendre en main sans mémoriser l'historique du projet.
C'est l'approche que DrupaLabs applique dans ses missions de TMA et de refonte Drupal : identifier le code custom qui réimplémente une brique Symfony, le remplacer par le composant, et réduire le périmètre à maintenir dans la durée.