← Tous les cours / EventDispatcher avancé Cours
00 — ARCHITECTURE PUB/SUB

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.

Patron Observateur (Observer)
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-14RôleImplé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
EventDispatcherInterface Symfony vs PSR-14
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 :

Flux d'un dispatch

Quand tu appelles $dispatcher->dispatch($event), voici ce qui se passe dans l'ordre :

dispatch($event)
callListeners()
$listener($event, …)
isPropagationStopped() ?

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).

💡 Signature complète : même si tu déclares 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é.

01 — ADDLISTENER ET PRIORITÉS

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 :

Structure de stockage
$listeners[eventName][priority][] = callable
Un 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.

priorité 100
→ appelé 1er
priorité 50
→ 2e
priorité 0
→ 3e
priorité -100
→ dernier
🔑 FIFO pour priorité égale : quand deux listeners ont la même priorité, celui enregistré en premier est appelé en premier. L'ordre d'insertion est préservé à l'intérieur d'un même niveau de priorité.

Les méthodes de consultation

MéthodeSignatureDescription
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().

⚠️ Enregistrement tardif : tu peux ajouter un listener après que le dispatcher a déjà traité d'autres events — le cache sera simplement recalculé au prochain dispatch. Mais enregistrer des listeners depuis l'intérieur d'un listener en cours d'exécution est déconseillé : le listener ajouté ne sera pas appelé pour l'event courant, seulement pour les suivants.

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.

02 — EVENTSUBSCRIBERINTERFACE

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 :

FormeSyntaxeRé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);
        }
    }
}
💡 Autoconfiguration Symfony : dans une application Symfony, toute classe implémentant 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

03 — CRÉER SES ÉVÉNEMENTS

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,
    ) {}
}
Symfony\Contracts\EventDispatcher\Event
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() avec nom explicite : si tu passes un deuxième argument à 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

Classe Events constantes
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.
04 — STOPPER LA PROPAGATION

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().

StoppableEventInterface
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

⚠️ Anti-pattern : n'utilise pas 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.

💡 Traçabilité : quand 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.

05 — LISTENER VS SUBSCRIBER

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èreListener (#[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] est IS_REPEATABLE : tu peux mettre plusieurs #[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.
06 — TRACEABLEEVENTDISPATCHER

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é.

TraceableEventDispatcher
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 :

dispatch()
preProcess()
callListeners() réels
postProcess()

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éthodeRetourContenu
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() est ton meilleur ami : si tu dispatches un event et que rien ne se passe, vérifie 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
);
07 — IMMUTABLEEVENTDISPATCHER

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.

ImmutableEventDispatcher
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éthodeComportement 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

ImmutableEventDispatcher vs EventDispatcher après compilation
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.
💡 Composition, pas héritage : 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).
08 — INTÉGRATION DIC & TAGS

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 { /* ... */ }
}
Paramètres de #[AsEventListener]
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.

implements
EventSubscriberInterface
→ autoconfigure
tag kernel.event_subscriber
RegisterListeners
AndSubscribersPass
addSubscriber() au boot

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
🔑 Service event_dispatcher : dans le conteneur Symfony, 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.
09 — SYNTHÈSE

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

BesoinOutil 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
💡 Les deux ensembles : un pattern courant est d'utiliser EventDispatcher pour les hooks synchrones (enregistrement en log, invalidation de cache) et Messenger pour les effets de bord lents (envoi d'email, notification push). Ils sont complémentaires.

4 anti-patterns à éviter

⚠️ Anti-pattern 1 — Logique métier dans les events : un event est un DTO. Si tu injectes des services dans ton event pour qu'il "fasse quelque chose", c'est le signe que la logique devrait être dans un listener ou un service métier dédié.
⚠️ Anti-pattern 2 — stopPropagation() comme if/else : si tu stoppe la propagation pour "choisir" quel listener s'exécute selon une condition, tu simules un branchement conditionnel avec un mécanisme conçu pour autre chose. Écris le if dans le listener.
⚠️ Anti-pattern 3 — Listeners qui modifient l'event mutuellement : si deux listeners modifient les mêmes propriétés d'un event, tu crées un couplage invisible par l'ordre de priorité. Préfère des events immuables (readonly). Si une modification est nécessaire, documente explicitement le contrat de mutation.
⚠️ Anti-pattern 4 — Dispatch dans un listener : dispatcher un nouvel event depuis l'intérieur d'un listener est possible mais dangereux si mal contrôlé. Attention aux boucles infinies (event A dispatch event B qui dispatch event A) et aux effets de bord difficiles à tracer. Si tu dois le faire, documente clairement la cascade.

Récapitulatif des classes et interfaces

Classe / InterfaceNamespaceRô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