Chapitre 5.5 — L’EventDispatcher
Partie 5 : Services & DI — Chapitre 5/5 Version de référence : Symfony 7.4.
⏱️ TL;DR
- L’EventDispatcher découple « il s’est passé quelque chose » de « réagir à ça ». Un service dispatche un événement ; des listeners/subscribers y réagissent, sans que l’émetteur les connaisse.
- Le pipeline du noyau (Partie 4) EST un EventDispatcher. Vous pouvez créer vos propres événements métier (
BookingCreated).- Un listener = une classe (ou méthode) annotée
#[AsEventListener]. Un subscriber = une classeEventSubscriberInterfacequi déclaregetSubscribedEvents().- Plusieurs listeners réagissent au même événement, avec des priorités, chacun indépendant (envoyer un e-mail, journaliser, mettre à jour des stats).
- Synchrone : les listeners s’exécutent dans la même requête. Pour du différé/lent, on utilise Messenger (Partie 12), pas l’EventDispatcher.
- Pour un dev Next.js : c’est un EventEmitter typé et intégré au conteneur — mais on l’emploie pour découpler, pas pour l’asynchrone.
🎯 Objectifs
- Comprendre le pattern « dispatch / listen » et son intérêt de découplage.
- Créer un événement métier, le dispatcher, et y brancher des listeners/subscribers.
- Utiliser priorités et
stopPropagation. - Choisir entre EventDispatcher (synchrone) et Messenger (asynchrone).
1. Le problème : le couplage des « effets de bord »
Imaginez BookingCreator::create(). Après avoir créé une réservation, il faut : envoyer un e-mail de confirmation, journaliser, incrémenter des statistiques, notifier le prestataire… Si on met tout dans create(), le service devient un fourre-tout couplé à l’e-mail, aux logs, aux stats.
// ❌ create() connaît tous les effets de bord
public function create(CreateBookingInput $input): Booking
{
$booking = /* ... */;
$this->mailer->send(/* confirmation */);
$this->logger->info(/* ... */);
$this->stats->increment('bookings');
$this->providerNotifier->notify(/* ... */);
return $booking;
}L’EventDispatcher inverse ça : create() annonce simplement « une réservation a été créée » ; d’autres services réagissent, chacun de son côté.
// ✅ create() annonce un fait ; il ignore qui écoute
public function create(CreateBookingInput $input): Booking
{
$booking = /* ... */;
$this->eventDispatcher->dispatch(new BookingCreated($booking));
return $booking;
}2. Créer et dispatcher un événement
Un événement est une simple classe qui transporte les données du fait :
// src/Event/BookingCreated.php
declare(strict_types=1);
namespace App\Event;
use App\Entity\Booking;
final class BookingCreated
{
public function __construct(
public readonly Booking $booking,
) {}
}On le dispatche en injectant EventDispatcherInterface :
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class BookingCreator
{
public function __construct(
private readonly EventDispatcherInterface $dispatcher,
// ... autres dépendances
) {}
public function create(CreateBookingInput $input): Booking
{
$booking = /* ... construire + persister ... */;
$this->dispatcher->dispatch(new BookingCreated($booking));
return $booking;
}
}⚡ Depuis Symfony 5 : les événements « custom » ne sont plus obligés d’étendre une classe
Eventde base, ni de passer par un « nom » d’événement (chaîne). Un événement est simplement identifié par sa classe. On dispatchenew BookingCreated($booking)et on écoute le typeBookingCreated. Plus simple et typé.
3. Réagir : listeners et subscribers
Deux façons d’écouter. La plus concise : l’attribut #[AsEventListener] sur une méthode.
// src/EventListener/SendConfirmationOnBookingCreated.php
use App\Event\BookingCreated;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener]
final class SendConfirmationOnBookingCreated
{
public function __construct(private readonly MailerInterface $mailer) {}
// le type de l'argument dit à quel événement on réagit
public function __invoke(BookingCreated $event): void
{
$booking = $event->booking;
// ... envoyer l'e-mail de confirmation
}
}Chaque effet de bord devient un listener indépendant : un pour l’e-mail, un pour les stats, un pour la notification prestataire. Ajouter un effet = ajouter une classe, sans toucher BookingCreator.
L’autre forme, le subscriber, regroupe plusieurs écoutes dans une classe et les déclare :
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class BookingMetricsSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
BookingCreated::class => 'onCreated',
BookingCancelled::class => ['onCancelled', 10], // avec priorité
];
}
public function onCreated(BookingCreated $event): void { /* ... */ }
public function onCancelled(BookingCancelled $event): void { /* ... */ }
}Listener (#[AsEventListener]) | Subscriber (EventSubscriberInterface) | |
|---|---|---|
| Déclaration | Attribut sur la classe/méthode | Méthode getSubscribedEvents() |
| Portée | Souvent un événement | Plusieurs événements regroupés |
| Visibilité | Config dispersée près du code | Liste centralisée dans la classe |
Les deux sont autoconfigurés (Partie 5.3) : aucune config à écrire.
4. Priorités et arrêt de propagation
Quand plusieurs listeners écoutent le même événement, l’ordre peut compter :
#[AsEventListener(priority: 100)] // s'exécute avant les priorités plus bassesUn listener peut stopper la chaîne (les suivants ne s’exécuteront pas) — utile pour les événements « annulables » (comme dans le pipeline du noyau, où poser une Response arrête la suite) :
public function __invoke(SomeEvent $event): void
{
if (/* condition */) {
$event->stopPropagation(); // si l'événement le supporte (StoppableEventInterface)
}
}5. EventDispatcher vs Messenger : synchrone ou différé ?
C’est une distinction cruciale :
| EventDispatcher | Messenger (Partie 12) | |
|---|---|---|
| Exécution | Synchrone, dans la requête courante | Asynchrone possible (worker séparé) |
| Usage | Découpler des réactions immédiates | Différer un travail lent/lourd |
| Échec d’un listener | Impacte la requête | Retries, files, isolation |
| Exemple | « au moment de créer, mettre à jour un compteur en mémoire » | « envoyer l’e-mail sans faire attendre l’utilisateur » |
Règle pratique : si la réaction est rapide et doit se produire maintenant (cohérence, log léger), un listener convient. Si elle est lente ou différable (e-mail, appel externe, génération de PDF), il vaut mieux que le listener dispatche un message Messenger (traité par un worker) plutôt que de faire le travail lui-même.
⚠️ Piège : mettre un envoi d’e-mail lourd ou un appel HTTP externe directement dans un listener synchrone ralentit la requête et la fait échouer si le service tiers est indisponible. Le bon combo : un listener léger sur
BookingCreatedqui se contente dedispatch(new SendBookingConfirmation($id))vers Messenger (Partie 12). L’EventDispatcher découple dans la requête ; Messenger sort le travail de la requête.
💡 Pour un dev Next.js : l’EventDispatcher ≈ un
EventEmittersynchrone, typé, injecté. Il ne remplace pas une file de jobs. Le duo EventDispatcher (découplage in-process) + Messenger (travail hors requête) correspond, côté Node, à « émettre un event » + « pousser un job dans BullMQ ». Ne confondez pas les deux rôles.
✏️ Exercices
Exercice 1. BookingCreator::create() envoie actuellement lui-même l’e-mail, écrit un log et incrémente des stats. Décrivez comment le refactorer avec l’EventDispatcher.
✅ Solution
Créer un événement BookingCreated (transportant le Booking). Dans create(), remplacer les trois effets par ->dispatch(new BookingCreated($booking)). Puis créer trois listeners indépendants (#[AsEventListener]) : un pour l’e-mail, un pour le log, un pour les stats — chacun réagissant à BookingCreated. BookingCreator ne connaît plus aucun de ces effets ; on peut en ajouter/retirer sans le modifier. (Idéalement, le listener e-mail dispatche un message Messenger plutôt que d’envoyer l’e-mail en synchrone.)
Exercice 2. Vous devez envoyer un e-mail et appeler une API externe lente à la création d’une réservation. Où placez-vous ce travail, et pourquoi pas dans un listener synchrone ?
✅ Solution
Dans un handler Messenger (Partie 12), déclenché par un message dispatché depuis un listener léger sur BookingCreated. Un listener synchrone ferait attendre l’utilisateur (e-mail + API lente dans la requête) et ferait échouer la requête si l’API tierce est down. Messenger sort ce travail de la requête (réponse immédiate), avec retries et isolation en cas d’échec.
Exercice 3. Quand préférer un subscriber à un listener ?
✅ Solution
Quand une même classe doit réagir à plusieurs événements et qu’on veut une vue centralisée de ses écoutes (via getSubscribedEvents()). Pour un seul événement, un listener avec #[AsEventListener] est plus concis et local. Les deux sont autoconfigurés ; c’est surtout une question de lisibilité/organisation.
🧠 Quiz de révision
1. Que découple l’EventDispatcher ?
« Il s’est passé quelque chose » (l’émetteur qui dispatche) de « réagir à ça » (les listeners). L’émetteur ignore qui écoute.
2. Comment identifie-t-on un événement custom en Symfony moderne ?
Par sa classe (new BookingCreated($booking) ; on écoute le type BookingCreated), sans nom-chaîne ni classe de base obligatoire.
3. Différence entre listener et subscriber ?
Un listener (#[AsEventListener]) réagit généralement à un événement, config locale. Un subscriber (EventSubscriberInterface) regroupe plusieurs écoutes déclarées dans getSubscribedEvents().
4. Les listeners s’exécutent-ils de façon synchrone ou asynchrone ?
Synchrone, dans la requête courante. Pour du travail différé/lent, il faut Messenger (Partie 12).
5. Que doit faire un listener sur BookingCreated qui doit envoyer un e-mail lourd ?
BookingCreated qui doit envoyer un e-mail lourd ?Dispatcher un message Messenger (SendBookingConfirmation) traité par un worker, plutôt que d’envoyer l’e-mail en synchrone dans la requête.
Fin de la Partie 5. Vous maîtrisez le système nerveux de Symfony : conteneur, autowiring, paramètres, tags/stratégies, compiler passes, bundles et EventDispatcher. Vos contrôleurs fins peuvent désormais déléguer à des services propres et découplés.
Prochaine étape : Partie 6 — Doctrine ORM & base de données — le modèle de données de BookLab : entités, relations, requêtes, migrations.