Événements et EventDispatcher

Listeners, priorités, événements métier et limites du découplage événementiel.

Créé le 12 juin 2026·Mis à jour le 12 juin 2026
Voir 2 références

Introduction


L'EventDispatcher est le composant qui implémente le pattern observateur dans Symfony. Il permet de découpler l'émetteur d'un événement de ses conséquences : le code qui crée une commande n'a pas à connaître l'envoi d'email, la mise à jour du stock ou la notification analytics qui en découlent.

C'est aussi le mécanisme d'extension central du framework : le cycle de vie du kernel, les workflows, la sécurité et de nombreux bundles exposent leurs points d'extension sous forme d'événements.

Listener ou subscriber ?


Deux écritures coexistent pour réagir à un événement.

Le listener moderne s'appuie sur l'attribut #[AsEventListener] :

#[AsEventListener]
final class SendOrderConfirmationListener
{
    public function __invoke(OrderPlacedEvent $event): void
    {
        // le type de l'argument détermine l'événement écouté
    }
}

Le subscriber regroupe plusieurs écoutes dans une classe et déclare lui-même ce qu'il écoute :

final class OrderSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderPlacedEvent::class => 'onOrderPlaced',
            OrderCancelledEvent::class => ['onOrderCancelled', 10],
        ];
    }
}

En pratique, l'attribut #[AsEventListener] couvre tous les cas, y compris plusieurs écoutes sur la même classe, avec moins de cérémonie. Les subscribers restent utiles dans les bundles réutilisables, où la configuration doit voyager avec la classe.

Créer un événement métier


Un événement est un simple objet immuable qui transporte un contexte :

final class OrderPlacedEvent extends Event
{
    public function __construct(
        public readonly Order $order,
    ) {
    }
}
final class PlaceOrderHandler
{
    public function __construct(
        private readonly EventDispatcherInterface $dispatcher,
    ) {
    }

    public function handle(PlaceOrder $command): void
    {
        $order = // ... logique métier

        $this->dispatcher->dispatch(new OrderPlacedEvent($order));
    }
}

Le service émetteur ne connaît aucun des écouteurs. Ajouter une conséquence (programme de fidélité, webhook partenaire) ne modifie pas le code existant.

Priorités et propagation


Les écouteurs d'un même événement s'exécutent par priorité décroissante (défaut : 0). À priorité égale, l'ordre d'enregistrement s'applique.

#[AsEventListener(priority: 100)]

Un écouteur peut interrompre la chaîne :

$event->stopPropagation();

Les écouteurs suivants ne seront pas appelés. À utiliser avec parcimonie : un stopPropagation() enfoui rend le comportement de l'application difficile à suivre.

php bin/console debug:event-dispatcher kernel.request

Cette commande affiche tous les écouteurs d'un événement dans leur ordre réel d'exécution, ce qui est indispensable pour diagnostiquer un problème d'ordre.

Événements du framework


Au-delà des événements métier, plusieurs familles d'événements sont dispatchées par le framework et ses bundles :

FamilleExemplesUsage typique
Kernelkernel.request, kernel.response, kernel.exceptionComportements transverses HTTP
Consoleconsole.command, console.errorInstrumentation des commandes CLI
SécuritéLoginSuccessEvent, LogoutEventAudit, mise à jour de last_login
Workflowworkflow.transition, workflow.guardBloquer ou tracer les transitions
DoctrinepostPersist, preUpdateRéactions au cycle de vie des entités

Les événements Doctrine passent par le dispatcher de Doctrine, pas celui de Symfony. On y réagit avec #[AsEntityListener] ou #[AsDoctrineListener], et ils s'exécutent au cœur du flush() : on n'y fait jamais d'opération lourde ou faillible.

Quand ne pas utiliser d'événements


L'EventDispatcher est synchrone : tous les écouteurs s'exécutent dans la requête, et une exception dans un écouteur fait échouer l'ensemble. C'est un outil de découplage, pas d'asynchronisme.

Quelques repères de conception :

  • La conséquence fait partie du contrat métier (débiter le stock lors d'une commande) : un appel explicite dans le service est plus honnête qu'un événement. Le code dit ce qu'il fait.
  • La conséquence est annexe et synchrone (logging, audit) : événement.
  • La conséquence est lourde ou faillible (email, appel API externe) : message Messenger asynchrone, pas un écouteur.

L'abus d'événements produit un spaghetti événementiel : pour comprendre ce que fait une action, il faut chasser les écouteurs dans toute la base de code. Un événement se justifie quand le découplage apporte une vraie valeur d'extension.

Résumé


ConceptÀ retenir
#[AsEventListener]Forme moderne, l'événement est déduit du type-hint
SubscriberUtile dans les bundles, configuration embarquée
PrioritéDécroissante, défaut 0, visible via debug:event-dispatcher
stopPropagation()Interrompt la chaîne, à utiliser avec parcimonie
SynchroneTout écouteur s'exécute dans la requête courante

Les événements découplent l'action de ses conséquences annexes. Pour les conséquences essentielles, préférer un appel explicite ; pour les conséquences lourdes, préférer Messenger. L'événement synchrone occupe la place du milieu.