Chapitre 5.3 — Tags, autoconfiguration & service locators
Partie 5 : Services & DI — Chapitre 3/5 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Un tag marque un service comme membre d’une famille (ex. « tous les canaux de notification »). Un autre service peut alors collecter tous les services d’un tag.
- L’autoconfiguration taggue automatiquement selon les interfaces implémentées (une commande, un subscriber, un voter… sont tagués sans que vous le fassiez).
- Pour collecter une famille, on injecte un iterable taggé :
#[AutowireIterator('app.notification_channel')]ou, plus simple,#[AutowireIterator]sur un type d’interface via l’autoconfiguration.- C’est le socle du pattern stratégie : N implémentations d’une interface, sélectionnées à l’exécution.
- Un service locator (
#[AutowireLocator]) charge les services paresseusement et à la demande (par clé), quand instancier toute la famille serait du gaspillage.- Pour un dev Next.js : c’est « injecter un tableau de tous les plugins qui implémentent telle interface », résolu automatiquement par le framework.
🎯 Objectifs
- Comprendre tags et autoconfiguration.
- Collecter une famille de services et implémenter un pattern stratégie.
- Utiliser un service locator pour un chargement paresseux par clé.
- Prioriser les services d’un tag.
1. Le besoin : une famille de services interchangeables
Beaucoup de problèmes prennent la forme « N implémentations d’un même contrat, choisies selon le contexte » :
- des canaux de notification : e-mail, SMS, push, Slack ;
- des moyens de paiement : carte, PayPal, virement ;
- des exportateurs : CSV, XLSX, PDF ;
- des règles de validation métier empilables.
On veut : (1) ajouter une implémentation sans modifier le code qui les utilise, (2) les collecter toutes automatiquement. C’est le rôle des tags.
2. Autoconfiguration : les tags gratuits
Grâce à autoconfigure: true (activé par défaut), Symfony taggue automatiquement un service selon ce qu’il implémente ou étend :
| Vous écrivez… | Symfony taggue automatiquement comme… |
|---|---|
une classe #[AsCommand] | commande console |
une classe implémentant EventSubscriberInterface | subscriber d’événements |
un Voter (sécurité) | voter |
un ConstraintValidator | validateur |
un TwigExtension | extension Twig |
C’est pourquoi vous n’avez jamais eu à « enregistrer » une commande ou un subscriber : l’autoconfiguration l’a taggé, et le composant concerné collecte tous les services de ce tag. Vous pouvez faire pareil pour vos familles.
3. Créer et collecter une famille : le pattern stratégie
Prenons les canaux de notification de BookLab. On définit une interface, plusieurs implémentations, et un « dispatcher » qui les collecte toutes.
// src/Notification/NotificationChannelInterface.php
namespace App\Notification;
interface NotificationChannelInterface
{
public function supports(string $type): bool;
public function send(string $to, string $message): void;
}On dit à Symfony : « toute implémentation de cette interface est un service taggé ». Le plus propre : l’attribut #[AutoconfigureTag] sur l’interface.
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.notification_channel')]
interface NotificationChannelInterface { /* ... */ }Chaque implémentation est automatiquement taggée app.notification_channel :
final class EmailChannel implements NotificationChannelInterface { /* ... */ }
final class SmsChannel implements NotificationChannelInterface { /* ... */ }
final class SlackChannel implements NotificationChannelInterface { /* ... */ }Le dispatcher collecte tous les canaux via un iterable taggé :
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
final class Notifier
{
/** @param iterable<NotificationChannelInterface> $channels */
public function __construct(
#[AutowireIterator('app.notification_channel')]
private readonly iterable $channels,
) {}
public function notify(string $type, string $to, string $message): void
{
foreach ($this->channels as $channel) {
if ($channel->supports($type)) {
$channel->send($to, $message);
return;
}
}
throw new \RuntimeException("Aucun canal pour le type : $type");
}
}Ajouter un canal (ex. PushChannel) = créer une classe. Aucune modification du Notifier, aucune config : l’autoconfiguration le taggue, l’iterator le collecte. C’est la puissance du système.
💡 Pour un dev Next.js : c’est le mécanisme de plugins que vous bricoleriez à la main en JS (un registre où chaque plugin s’enregistre). Ici, l’enregistrement est automatique (implémenter l’interface suffit) et l’injection de « tous les plugins » est native. Très proche de la découverte de providers multi-token de NestJS.
4. Prioriser les services d’un tag
Quand l’ordre compte (ex. tester les canaux dans un ordre précis), on priorise avec #[AsTaggedItem] :
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(priority: 10)] // testé avant les priorités plus basses
final class SlackChannel implements NotificationChannelInterface { /* ... */ }L’iterable itère alors de la priorité la plus haute à la plus basse.
5. Service locator : charger à la demande, par clé
Parfois, on ne veut pas instancier toute la famille (coûteux), mais choisir une implémentation par clé, paresseusement. C’est le rôle d’un service locator (#[AutowireLocator]), qui n’instancie que le service réellement demandé.
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Psr\Container\ContainerInterface;
final class ExporterRegistry
{
public function __construct(
#[AutowireLocator('app.exporter')] // un locator, pas un iterable
private readonly ContainerInterface $exporters,
) {}
public function export(string $format, array $data): string
{
if (!$this->exporters->has($format)) {
throw new \InvalidArgumentException("Format inconnu : $format");
}
// seul l'exporteur demandé est instancié (lazy)
return $this->exporters->get($format)->export($data);
}
}Différence clé avec l’iterator :
#[AutowireIterator] | #[AutowireLocator] | |
|---|---|---|
| Instancie | tous les services du tag | seulement celui demandé (lazy) |
| Accès | itération (foreach) | par clé (get('csv')) |
| Cas d’usage | traiter/tester toute la famille | choisir une implémentation coûteuse |
⚠️ Piège : utiliser un iterable là où une seule implémentation coûteuse sera choisie instancie tout pour rien. Si vous sélectionnez une stratégie par clé (et que les instancier coûte cher), préférez le locator (lazy). Si vous devez parcourir toute la famille (comme le
Notifier), l’iterable convient.
6. Diagnostiquer les tags
php bin/console debug:container --tag=app.notification_channel
# liste tous les services portant ce tagPratique pour vérifier qu’une nouvelle implémentation a bien été collectée (si elle n’apparaît pas, c’est souvent qu’elle n’implémente pas l’interface taguée, ou que l’autoconfiguration est désactivée).
✏️ Exercices
Exercice 1. Vous ajoutez un PushChannel à BookLab. Quelles étapes faut-il pour qu’il soit pris en compte par le Notifier ?
✅ Solution
Une seule : créer la classe PushChannel implements NotificationChannelInterface. L’autoconfiguration la taggue app.notification_channel (via #[AutoconfigureTag] sur l’interface), et le Notifier la collecte automatiquement grâce à #[AutowireIterator('app.notification_channel')]. Aucune modification du Notifier ni de config. (Éventuellement #[AsTaggedItem(priority: ...)] pour l’ordre.)
Exercice 2. Vous avez 6 exportateurs lourds (chacun charge une grosse dépendance), mais une requête n’en utilise qu’un. Iterator ou locator ? Pourquoi ?
✅ Solution
Locator (#[AutowireLocator]) : il n’instancie que l’exportateur réellement demandé (par clé, ex. get('pdf')), évitant de charger les 6 dépendances lourdes à chaque requête. Un iterator instancierait tous les exportateurs, gaspillant mémoire et temps. Le locator est fait pour « choisir un parmi N, paresseusement ».
Exercice 3. Comment vérifier en ligne de commande que SlackChannel est bien collecté dans la famille ?
✅ Solution
php bin/console debug:container --tag=app.notification_channel liste tous les services portant ce tag ; SlackChannel doit y figurer. S’il manque, vérifier qu’il implémente bien NotificationChannelInterface (taguée) et que l’autoconfiguration est active.
🧠 Quiz de révision
1. À quoi sert un tag ?
À marquer un service comme membre d’une famille, pour qu’un autre service collecte tous les services de cette famille.
2. Que fait l’autoconfiguration vis-à-vis des tags ?
Elle taggue automatiquement les services selon les interfaces/classes qu’ils implémentent (commandes, subscribers, voters…), et #[AutoconfigureTag] permet de faire pareil pour vos propres interfaces.
3. Comment collecte-t-on tous les services d’un tag ?
En injectant un iterable taggé avec #[AutowireIterator('mon.tag')] (un iterable des services).
4. Différence entre iterator et service locator ?
L’iterator instancie tous les services du tag (pour les parcourir) ; le locator n’instancie que celui demandé par clé (paresseux) — idéal pour choisir une implémentation coûteuse parmi plusieurs.
5. Comment ordonner les services d’un tag ?
Avec l’attribut #[AsTaggedItem(priority: N)] (priorité haute = itéré en premier).
Prochain chapitre : 5.4 — Compiler passes & bundles — modifier le conteneur à la compilation, et l’anatomie d’un bundle.