Architecture Pub/Sub et rôle d'EventDispatcher
EventDispatcher est l'implémentation Symfony du patron Observateur — un mécanisme de découplage entre celui qui émet un signal et ceux qui y réagissent.
Quand tu ouvres un fichier de conf Symfony et que tu vois kernel.request,
kernel.response ou security.interactive_login, tu regardes
un bus d'événements à l'œuvre. Chaque composant émet des signaux ;
n'importe quel autre composant peut y répondre — sans que l'émetteur sache qui écoute.
Un sujet (le dispatcher) émet des événements. Des observateurs (les listeners) s'y abonnent. Le sujet ignore qui écoute — il diffuse, les autres réagissent. C'est le découplage par excellence.
PSR-14 — la spec commune
Symfony implémente PSR-14, la spécification PHP pour les bus d'événements. Trois interfaces la composent :
| Interface PSR-14 | Rôle | Implémentation Symfony |
|---|---|---|
EventDispatcherInterface |
Dispatch un event, gère l'enregistrement des listeners | EventDispatcher |
StoppableEventInterface |
Permet à un event de signaler l'arrêt de la propagation | Symfony\Contracts\EventDispatcher\Event |
ListenerProviderInterface |
Fournit les listeners pour un event donné | Intégré dans EventDispatcher directement |
Symfony\Contracts\EventDispatcher\EventDispatcherInterface étend
Psr\EventDispatcher\EventDispatcherInterface. Elle ajoute
addListener(), addSubscriber(), getListeners()
et les méthodes de suppression — PSR-14 ne définit que dispatch().
EventDispatcher dans l'écosystème Symfony
Tu ne l'utilises pas directement dans le code métier, mais il est partout :
- HttpKernel — cycle requête/réponse entier basé sur des events (
KernelEvents) - Security —
security.interactive_login,security.logout - Form —
form.pre_submit,form.submit,form.post_submit - Workflow — transitions, entrées, sorties d'états
- Doctrine — lifecycle callbacks convertis en events
Flux d'un dispatch
Quand tu appelles $dispatcher->dispatch($event), voici ce qui se passe dans l'ordre :
Chaque listener reçoit trois arguments dans cet ordre : l'event lui-même (modifiable), le nom de l'event (string), et le dispatcher (pour re-dispatcher si besoin).
function (MyEvent $e): void
en listener, PHP ne passe que ce dont la fonction a besoin. La signature interne est
$listener($event, …) — les arguments supplémentaires sont
ignorés si la closure n'en déclare pas.
Lance la simulation pour visualiser ce flux :
Clique sur Dispatcher pour voir les 3 listeners s'activer dans l'ordre de priorité.
addListener et la gestion des priorités
addListener() est la brique de base — elle place un callable dans la file d'un événement avec une priorité.
addListener() est la méthode fondamentale pour enregistrer un listener.
Elle prend trois paramètres : le nom de l'événement, le callable à appeler, et la priorité.
$dispatcher->addListener(
OrderPlacedEvent::class, // nom de l'event (FQCN recommandé)
function (OrderPlacedEvent $event): void {
// traitement
},
50 // priorité, défaut = 0
);
Stockage interne et tri
En interne, EventDispatcher stocke les listeners dans un tableau à deux niveaux :
$listeners[eventName][priority][] = callableUn tableau d'événements → tableau de priorités → tableau de callables.
Avant d'appeler les listeners, le dispatcher trie par priorité décroissante
via krsort() : la priorité 100 est appelée avant 50, avant 0, avant -100.
Les méthodes de consultation
| Méthode | Signature | Description |
|---|---|---|
getListeners() |
(?string $eventName): array |
Tous les listeners (triés) pour un event, ou tous events si null |
getListenerPriority() |
(string $eventName, callable $listener): ?int |
Priorité d'un listener donné, null s'il n'est pas enregistré |
hasListeners() |
(?string $eventName): bool |
Vrai si au moins un listener pour cet event (ou tout event si null) |
removeListener() |
(string $eventName, callable $listener): void |
Désenregistre un listener précis |
removeSubscriber() |
(EventSubscriberInterface $subscriber): void |
Désenregistre tous les listeners d'un subscriber |
Invalidation du cache interne
EventDispatcher maintient deux caches internes ($sorted et $optimized)
pour éviter de retrier à chaque dispatch. Ces caches sont invalidés automatiquement
à chaque appel à addListener() ou removeListener().
Signature complète d'un listener
Le dispatcher appelle chaque listener avec trois arguments :
function (
$event, // l'objet event (type dépend de l'event)
string $eventName, // le nom de l'event tel qu'enregistré
EventDispatcherInterface $dispatcher // le dispatcher lui-même
): void
En pratique, la plupart des listeners ne déclarent que le premier argument. PHP transmet les arguments supplémentaires mais ils sont ignorés si la signature ne les déclare pas.
EventSubscriberInterface — les 3 formes
Un subscriber déclare ses abonnements en une seule méthode statique — c'est le listener centralisé.
Un subscriber est une classe qui regroupe plusieurs listeners liés
en un seul endroit. Elle implémente EventSubscriberInterface et déclare
ses abonnements via une méthode statique.
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class OrderSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
OrderPlacedEvent::class => 'onOrderPlaced',
OrderShippedEvent::class => ['onOrderShipped', 20],
OrderCancelledEvent::class => [
['notifyCustomer', 100],
['releaseStock', 50],
],
];
}
public function onOrderPlaced(OrderPlacedEvent $event): void { /* ... */ }
public function onOrderShipped(OrderShippedEvent $event): void { /* ... */ }
public function notifyCustomer(OrderCancelledEvent $event): void { /* ... */ }
public function releaseStock(OrderCancelledEvent $event): void { /* ... */ }
}
Les 3 formes de getSubscribedEvents()
La valeur associée à chaque nom d'event peut prendre trois formes :
| Forme | Syntaxe | Résultat |
|---|---|---|
| Forme 1 | 'eventName' => 'method' |
Listener sur la méthode, priorité 0 |
| Forme 2 | 'eventName' => ['method', $priority] |
Listener sur la méthode, priorité explicite |
| Forme 3 | 'eventName' => [['method1', $p1], ['method2', $p2]] |
Plusieurs listeners sur le même event |
Ce que fait addSubscriber() en interne
Quand tu appelles $dispatcher->addSubscriber($sub), le dispatcher itère sur
getSubscribedEvents() et appelle addListener() pour chaque entrée.
C'est du sucre syntaxique — rien de magique :
// Pseudo-code simplifié de addSubscriber()
foreach ($subscriber::getSubscribedEvents() as $eventName => $params) {
if (is_string($params)) {
$this->addListener($eventName, [$subscriber, $params]);
} elseif (is_string($params[0])) {
$this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0);
} else {
foreach ($params as $listener) {
$this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0);
}
}
}
EventSubscriberInterface est automatiquement tagguée
kernel.event_subscriber et enregistrée sans configuration YAML.
C'est l'autoconfigure du DI Container qui fait ça.
Utilise la simulation ci-dessous pour voir comment chaque forme de
getSubscribedEvents() est traduite en appels addListener() :
Sélectionne une forme pour voir son équivalent en addListener().
getSubscribedEvents()
→ addListener() équivalent
Créer ses propres événements
Un événement bien conçu est un DTO immutable avec un nom stable — il porte les données dont les listeners ont besoin.
Un event Symfony n'est qu'un objet PHP ordinaire. Mais sa conception influe directement sur la maintenabilité de ton code. Voyons comment créer des events robustes.
La classe de base Event
Symfony fournit Symfony\Contracts\EventDispatcher\Event comme classe de base.
Elle implémente StoppableEventInterface et expose deux méthodes :
use Symfony\Contracts\EventDispatcher\Event;
final class OrderPlacedEvent extends Event
{
public function __construct(
public readonly Order $order,
public readonly \DateTimeImmutable $placedAt,
) {}
}
Classe de base légère. Expose
stopPropagation(): void et
isPropagationStopped(): bool. Hériter d'elle n'est pas obligatoire
(PSR-14 accepte n'importe quel objet), mais c'est la convention Symfony.
Event typé vs GenericEvent
Symfony fournit aussi GenericEvent — un event polyvalent pour les cas simples :
use Symfony\Component\EventDispatcher\GenericEvent;
// GenericEvent : subject + arguments
$event = new GenericEvent($order, ['channel' => 'web']);
$event->getSubject(); // l'objet Order
$event['channel']; // 'web' (ArrayAccess)
$event->getArgument('channel'); // 'web'
GenericEvent implémente ArrayAccess et IteratorAggregate.
C'est pratique pour des scripts ou du prototypage, mais pour du code métier on préfère un event typé :
Event typé — recommandé
- Type safety à la compilation
- IDE autocompletion
- Contrat explicite entre émetteur et listeners
- Refactoring sûr
GenericEvent — usage limité
- Rapide à écrire
- Adapté aux scripts et migrations
- Pas de contrat, pas de type check
- Difficile à refactorer
Nommage des événements
Depuis Symfony 5, la convention recommandée est d'utiliser le FQCN de la classe event comme nom :
// Recommandé (Symfony 5+)
$dispatcher->dispatch(new OrderPlacedEvent($order));
// Le nom est automatiquement OrderPlacedEvent::class
// Ancienne convention (encore valide, moins idiomatique)
const ORDER_PLACED = 'order.placed';
$dispatcher->dispatch(new OrderPlacedEvent($order), self::ORDER_PLACED);
dispatch(), c'est ce nom qui est utilisé pour trouver les listeners —
pas le FQCN de la classe. C'est utile pour les events génériques mais source de confusion
pour les events typés. Utilise le FQCN implicite par défaut.
Bonnes pratiques de conception
- Immutabilité — propriétés
readonly, pas de setters. Les listeners ne doivent pas modifier l'event, sauf si c'est explicitement leur rôle (ex.GetResponseEventde HttpKernel). - Données minimalistes — ne passe que ce dont les listeners ont besoin. Pas besoin de passer tout l'agrégat si un ID suffit.
- Pas de logique — l'event est un DTO, pas un service. Aucune dépendance injectée.
- Nommage au passé —
OrderPlacedEvent(quelque chose s'est passé), pasPlaceOrderEvent(qui ressemble à une commande).
Pour les projets qui utilisent encore les string constants, centralise-les dans une classe
Events ou utilise des constantes de classe sur l'event lui-même :OrderEvents::PLACED, OrderEvents::SHIPPED, etc.
StoppableEventInterface — arrêter la propagation
stopPropagation() permet à un listener d'interrompre la chaîne — les listeners suivants ne seront pas appelés.
Imagine une chaîne de contrôle de sécurité à l'aéroport : si un listener détecte un problème,
il peut lever la main et dire "je gère ça, pas la peine que les autres continuent".
C'est exactement ce que fait stopPropagation().
Interface PSR-14. Une seule méthode :
isPropagationStopped(): bool.
Quand elle retourne true, le dispatcher arrête d'appeler les listeners suivants.
Symfony\Contracts\EventDispatcher\Event l'implémente avec un flag interne.
Comment ça fonctionne dans callListeners()
Dans le code interne de EventDispatcher::callListeners(), après chaque appel
à un listener, le dispatcher vérifie :
// Pseudo-code de callListeners()
foreach ($listeners as $listener) {
$listener($event, $eventName, $this);
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
break; // on s'arrête ici, les suivants ne sont pas appelés
}
}
Appeler stopPropagation()
final class AccessCheckListener
{
public function onRequest(RequestEvent $event): void
{
if (!$this->isAuthorized($event->getRequest())) {
$event->setResponse(new Response('Forbidden', 403));
$event->stopPropagation(); // les listeners suivants sont ignorés
}
}
}
Cas d'usage légitimes
- Validation en chaîne — premier listener qui détecte une erreur stoppe les validations suivantes
- Résolution de réponse — le premier listener qui peut répondre prend la main (pattern chain of responsibility)
- Override de comportement — un listener prioritaire remplace le comportement par défaut des listeners de base priorité
stopPropagation() comme un
if/else déguisé. Si tu veux un comportement conditionnel, écris-le
dans le listener. stopPropagation() est réservé aux cas où tu veux
vraiment empêcher d'autres listeners de réagir à cet event.
Interaction avec la priorité
La priorité et stopPropagation() interagissent directement.
Un listener de priorité 100 peut stopper les listeners de priorité 50, 0, -100.
L'inverse est impossible — un listener de priorité 0 ne peut pas stopper ceux de priorité 100
car ils ont déjà été appelés.
TraceableEventDispatcher est actif (env dev),
les listeners non appelés suite à un stopPropagation() apparaissent dans
getNotCalledListeners() avec le statut stopped. Pratique pour debugger.
Utilise la simulation pour visualiser l'effet de stopPropagation() :
Clique sur Lancer pour appeler les listeners en séquence. Active le toggle pour voir ce qui se passe quand le listener 2 stoppe la propagation.
Listener vs Subscriber — guide de choix
Listener et Subscriber ne font pas la même chose — ils répondent à des besoins d'organisation différents.
Listener et Subscriber coexistent dans Symfony. Ils ont des forces différentes — choisir entre les deux est une question d'organisation, pas de performance.
Le listener — simple et ciblé
Un listener est n'importe quel callable enregistré sur un seul event :
// Listener via closure (idéal pour le prototypage)
$dispatcher->addListener(
OrderPlacedEvent::class,
static function (OrderPlacedEvent $event): void {
// logique ici
},
50
);
// Listener via attribut PHP (recommandé dans Symfony)
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: OrderPlacedEvent::class, priority: 50)]
final class SendConfirmationEmailListener
{
public function __invoke(OrderPlacedEvent $event): void
{
// logique ici
}
}
Le subscriber — centralisé et auto-découvert
Un subscriber regroupe la gestion de plusieurs events liés dans une seule classe.
Il est auto-découvert par Symfony via autoconfigure :
final class OrderSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
OrderPlacedEvent::class => ['onPlaced', 100],
OrderShippedEvent::class => ['onShipped', 50],
OrderCancelledEvent::class => 'onCancelled',
];
}
// méthodes...
}
Tableau de comparaison
| Critère | Listener (#[AsEventListener]) | Subscriber |
|---|---|---|
| Nombre d'events | Un seul par classe (convention) | Plusieurs, déclarés dans getSubscribedEvents() |
| Déclaration | Attribut PHP sur la classe | Méthode statique, tag automatique via autoconfigure |
| Testabilité | Classe seule, __invoke() facile à tester |
Plusieurs méthodes publiques, teste chacune séparément |
| Logique partagée | Injection de dépendances dans le constructeur | Même injection, partagée entre handlers du même subscriber |
| Découvrabilité | Un fichier = un comportement = facile à localiser | Un fichier gère plusieurs events — pratique ou difficile à naviguer |
| Priorité multiple | Un attribut #[AsEventListener] par event (IS_REPEATABLE) |
Native dans getSubscribedEvents() |
Guide de choix
Préfère le Listener quand…
- Tu gères un seul event
- La logique est simple et autonome
- Tu veux une classe très facile à tester
- Tu travailles sur un comportement one-shot (migration, script)
Préfère le Subscriber quand…
- Tu gères plusieurs events du même domaine
- Des méthodes partagent la même logique ou dépendances
- Tu veux regrouper toute la réaction d'un composant en un endroit
- Tu migres d'une ancienne config YAML vers des classes
#[AsEventListener] sur la même classe pour l'enregistrer sur plusieurs
events tout en gardant l'approche "une classe = un comportement" si les méthodes
sont distinctes.
TraceableEventDispatcher — le débugger
En environnement dev, Symfony remplace EventDispatcher par TraceableEventDispatcher — il enregistre tout pour le Profiler.
En environnement de développement, Symfony ne te donne pas l'EventDispatcher
standard — il l'enveloppe dans un TraceableEventDispatcher. Chaque dispatch est
observé, mesuré et enregistré.
Décorateur autour d'un
EventDispatcherInterface. Il injecte un Stopwatch
et un LoggerInterface, remplace temporairement chaque listener par un
WrappedListener, collecte les données de timing et de statut après chaque dispatch.
Construction
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;
$traceable = new TraceableEventDispatcher(
$realDispatcher, // le vrai EventDispatcher
new Stopwatch(), // pour mesurer le timing
$logger, // PSR-3 Logger, optionnel
);
Le mécanisme de wrapping
Quand un dispatch est déclenché, TraceableEventDispatcher exécute en deux temps :
Chaque WrappedListener mesure son propre temps d'exécution
via Stopwatch et enregistre s'il a été appelé et si la propagation a été stoppée.
Les méthodes d'inspection
| Méthode | Retour | Contenu |
|---|---|---|
getCalledListeners(?Request) |
array[] |
Listeners effectivement appelés : name, event, pretty_name, time (ms) |
getNotCalledListeners(?Request) |
array[] |
Listeners enregistrés mais non appelés (event non dispatché ou propagation stoppée) |
getOrphanedEvents(?Request) |
string[] |
Events dispatchés sans aucun listener enregistré |
reset() |
void |
Efface toutes les données collectées (utile entre tests) |
Utilisation dans le Profiler
Ces données alimentent l'onglet Events du Symfony Profiler. Tu y vois la liste de tous les events dispatchés pendant la requête, les listeners appelés pour chacun, leur temps d'exécution, et les listeners non déclenchés. C'est l'outil de debugging principal pour tout comportement bizarre lié aux events.
getOrphanedEvents() dans le Profiler.
Si ton event y apparaît, aucun listener n'est enregistré pour lui — problème
d'autoconfigure, de tag manquant, ou de faute de frappe dans le nom d'event.
Désactivation conditionnelle
Pour les tests de performance ou les benchmarks, tu peux désactiver temporairement
le traçage via une closure $disabled passée au constructeur.
Quand la closure retourne true, le dispatcher se comporte comme un EventDispatcher standard.
$disabled = static fn() => $_SERVER['APP_ENV'] === 'bench';
$traceable = new TraceableEventDispatcher(
$realDispatcher,
new Stopwatch(),
null,
$disabled, // 4e paramètre optionnel
);
ImmutableEventDispatcher
ImmutableEventDispatcher est un proxy read-only — il expose le catalogue d'événements sans permettre l'enregistrement de nouveaux listeners.
Parfois tu veux exposer un EventDispatcher à du code qui doit pouvoir
écouter des events, mais pas en ajouter de nouveaux. C'est le rôle
de ImmutableEventDispatcher : un proxy read-only.
Décorateur autour d'un
EventDispatcherInterface. Toutes les méthodes
de lecture (dispatch, getListeners, hasListeners,
getListenerPriority) sont déléguées au dispatcher wrappé.
Toutes les méthodes d'écriture (addListener, addSubscriber,
removeListener, removeSubscriber) lèvent une BadMethodCallException.
Construction et usage
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
// Construire le dispatcher complet
$dispatcher = new EventDispatcher();
$dispatcher->addListener(OrderPlacedEvent::class, $listener);
// Exposer une vue read-only
$immutable = new ImmutableEventDispatcher($dispatcher);
// OK — lire le catalogue
$immutable->hasListeners(OrderPlacedEvent::class); // true
$immutable->getListeners(OrderPlacedEvent::class); // [...listeners...]
$immutable->dispatch(new OrderPlacedEvent($order)); // dispatch fonctionne
// KO — écrire lève BadMethodCallException
$immutable->addListener(OrderPlacedEvent::class, $newListener);
// BadMethodCallException: Unmodifiable event dispatchers must not be modified.
Méthodes d'écriture — return type never
Les méthodes d'écriture de ImmutableEventDispatcher ont le return type never :
elles lèvent toujours une exception et ne retournent jamais normalement.
| Méthode | Comportement dans ImmutableEventDispatcher |
|---|---|
addListener() |
Lance BadMethodCallException |
addSubscriber() |
Lance BadMethodCallException |
removeListener() |
Lance BadMethodCallException |
removeSubscriber() |
Lance BadMethodCallException |
dispatch() |
Délégué au dispatcher wrappé (fonctionne) |
getListeners() |
Délégué au dispatcher wrappé (fonctionne) |
Cas d'usage
- Injecter dans du code client non-trusté — une extension tierce peut écouter tes events mais ne peut pas en hijacker d'autres
- Finaliser une configuration — after kernel boot, le conteneur peut exposer un ImmutableEventDispatcher pour garantir que plus aucun listener ne sera ajouté
- Tests — passer un ImmutableEventDispatcher à un composant pour vérifier qu'il ne tente pas d'enregistrer des listeners
Dans le conteneur compilé, les listeners sont "figés" dans le code PHP généré.
ImmutableEventDispatcher est différent : c'est un guard runtime.
Il n'empêche pas l'ajout de listeners via le dispatcher wrappé — seulement via le proxy immutable lui-même.
ImmutableEventDispatcher est un bon
exemple du principe Open/Closed — il étend le comportement sans modifier le dispatcher sous-jacent.
La même instance de dispatcher peut être partagée en version mutable (pour la configuration) et
en version immutable (pour l'exposition publique).
Intégration Symfony — DIC, tags, autoconfigure
Dans une application Symfony, EventDispatcher est un service du conteneur — les listeners et subscribers se déclarent via attributs PHP.
Dans une application Symfony, tu n'instancies jamais EventDispatcher
manuellement — il vit dans le conteneur. Les listeners et subscribers sont des services
comme les autres, enregistrés via des attributs PHP ou des tags YAML.
#[AsEventListener] — la voie moderne
Depuis Symfony 6.3, l'attribut #[AsEventListener] est la façon la plus
concise d'enregistrer un listener. Il remplace le tag kernel.event_listener :
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: OrderPlacedEvent::class, priority: 50)]
final class SendConfirmationEmailListener
{
public function __invoke(OrderPlacedEvent $event): void
{
// envoi email...
}
}
// Plusieurs events sur la même classe (IS_REPEATABLE)
#[AsEventListener(event: OrderPlacedEvent::class, method: 'onPlaced')]
#[AsEventListener(event: OrderShippedEvent::class, method: 'onShipped', priority: 30)]
final class OrderNotificationListener
{
public function onPlaced(OrderPlacedEvent $event): void { /* ... */ }
public function onShipped(OrderShippedEvent $event): void { /* ... */ }
}
event — FQCN ou string de l'event (requis)method — méthode à appeler (défaut : __invoke)priority — priorité entière (défaut : 0)dispatcher — nom du service dispatcher (si plusieurs dispatchers)
kernel.event_listener tag YAML
La déclaration YAML reste valide. Utile pour des listeners dans des bundles ou des projets qui préfèrent la configuration séparée :
# config/services.yaml
App\EventListener\SendConfirmationEmailListener:
tags:
- name: kernel.event_listener
event: App\Event\OrderPlacedEvent
method: __invoke
priority: 50
EventSubscriberInterface et autoconfigure
Toute classe implémentant EventSubscriberInterface est automatiquement
tagguée kernel.event_subscriber grâce à autoconfigure: true
(activé par défaut dans Symfony). Aucune déclaration manuelle nécessaire.
EventSubscriberInterface
AndSubscribersPass
Plusieurs dispatchers dans le même projet
Symfony permet d'avoir plusieurs instances de EventDispatcher dans le conteneur
(par exemple pour isoler des events métier des events framework). Dans ce cas :
# Déclarer un second dispatcher
app.order_dispatcher:
class: Symfony\Component\EventDispatcher\EventDispatcher
# Cibler ce dispatcher dans un listener
App\Listener\OrderListener:
tags:
- name: kernel.event_listener
event: App\Event\OrderPlacedEvent
dispatcher: app.order_dispatcher
event_dispatcher (ou son alias Symfony\Component\EventDispatcher\EventDispatcherInterface)
est le dispatcher principal — celui d'HttpKernel. Injecte-le dans tes services
via EventDispatcherInterface $dispatcher dans le constructeur.
Synthèse — le guide de décision
EventDispatcher ou Messenger ? Listener ou Subscriber ? Quand stopper ? Le guide de décision.
Tu maîtrises maintenant les pièces du puzzle : listeners, subscribers, priorités, propagation, traceur, proxy read-only, intégration DIC. Il est temps d'assembler un guide de décision clair.
Guide de choix : besoin → outil
| Besoin | Outil recommandé | Pourquoi |
|---|---|---|
| Réagir à un seul event, logique simple | #[AsEventListener] |
Classe autonome, facile à tester, nommage explicite |
| Gérer plusieurs events liés d'un même domaine | EventSubscriberInterface |
Centralise l'abonnement, auto-découvert par Symfony |
| Arrêter la chaîne de traitement conditionnellement | stopPropagation() |
Natif dans Event, contrôlé par la priorité |
| Debugger pourquoi un listener ne se déclenche pas | TraceableEventDispatcher + Profiler |
getOrphanedEvents(), getNotCalledListeners() |
| Exposer le dispatcher à du code non-trusté | ImmutableEventDispatcher |
Proxy read-only, BadMethodCallException sur écriture |
| Traitement asynchrone ou découplé | Symfony Messenger | Messages en queue, workers, retry — EventDispatcher est synchrone |
EventDispatcher vs Symfony Messenger
C'est la question qui revient souvent. La frontière est la suivante :
EventDispatcher — synchrone
- Exécution immédiate dans la même requête
- L'émetteur attend la fin des listeners
- Idéal pour les hooks de cycle de vie (before/after save, requête HTTP)
- Un event, plusieurs listeners en séquence
Messenger — asynchrone
- Messages mis en file, traités par des workers
- L'émetteur n'attend pas la réponse
- Idéal pour les side-effects lents (emails, exports, webhooks)
- Retry, dead-letter queue, monitoring
4 anti-patterns à éviter
if dans le listener.
Récapitulatif des classes et interfaces
| Classe / Interface | Namespace | Rôle |
|---|---|---|
EventDispatcherInterface |
Symfony\Contracts\EventDispatcher |
Interface principale — dispatch + gestion listeners |
EventDispatcher |
Symfony\Component\EventDispatcher |
Implémentation standard |
Event |
Symfony\Contracts\EventDispatcher |
Classe de base pour les events, stopPropagation() |
EventSubscriberInterface |
Symfony\Component\EventDispatcher |
Contrat subscriber, getSubscribedEvents() |
GenericEvent |
Symfony\Component\EventDispatcher |
Event générique subject + arguments |
TraceableEventDispatcher |
Symfony\Component\EventDispatcher\Debug |
Décorateur de debug — Stopwatch + collecte |
ImmutableEventDispatcher |
Symfony\Component\EventDispatcher |
Proxy read-only |
#[AsEventListener] |
Symfony\Component\EventDispatcher\Attribute |
Attribut PHP pour déclarer un listener dans le DIC |