Le composant EventDispatcher
Comment Symfony permet à plusieurs parties du code de réagir à un même événement, sans se connaître entre elles
Notions théoriques
Le pattern Observateur
Imaginez une sonnette d'entrée. La personne qui appuie sur le bouton ne sait pas qui va réagir : peut-être personne n'est là, peut-être plusieurs personnes se lèvent en même temps. Elle se contente de déclencher un signal. C'est exactement le principe du pattern Observateur (en anglais Observer pattern) : un objet notifie qu'un fait s'est produit, sans savoir qui va réagir, ni combien de code va s'exécuter en réponse.
Symfony implémente ce pattern à travers son composant EventDispatcher. Le noyau du framework (et votre propre code applicatif) dispatche des événements tout au long du traitement d'une requête. N'importe quelle partie de l'application peut s'abonner à ces événements pour y réagir, sans jamais modifier le code qui les déclenche.
On appelle cela du découplage : le code qui dispatche un événement (par exemple « un article vient d'être publié ») n'a aucune idée de ce qui va se passer ensuite (envoyer un e-mail, écrire un log, notifier un webhook...). Il ne fait qu'annoncer le fait.
Event Listener et Event Subscriber
Symfony propose deux manières de « s'abonner » à un événement :
- L'Event Listener : une classe simple (souvent avec une seule méthode) déclar ée en configuration
(ou via l'attribut PHP
#[AsEventListener]) pour écouter un seul événement précis. - L'Event Subscriber : une classe qui implémente l'interface
EventSubscriberInterfaceet définit une méthode statiquegetSubscribedEvents(). Cette méthode retourne un tableau associatif qui indique quels événements la classe écoute et quelle méthode appeler pour chacun. Un même Subscriber peut donc réagir à plusieurs événements différents.
| Caractéristique | Event Listener | Event Subscriber |
|---|---|---|
| Nombre d'événements écoutés | Un seul | Un ou plusieurs |
| Déclaration | Configuration ou attribut PHP | Toute la logique dans la classe elle-même |
| Où trouver la liste des événements écoutés | Dans le fichier de configuration | Dans la méthode getSubscribedEvents() |
Dans la pratique, l'Event Subscriber est la forme la plus utilisée dans les projets Symfony, car toute la logique d'abonnement reste dans la classe elle-même, ce qui la rend plus facile à retrouver et à maintenir.
Les événements du noyau Symfony
Le noyau HTTP de Symfony (le HttpKernel) dispatche automatiquement une série d'événements
pendant le traitement de chaque requête. En voici trois parmi les plus courants :
kernel.request: dispatché tout au début, dès qu'une requête arrive, avant même que le contrôleur soit trouvé.kernel.response: dispatché juste avant l'envoi de la réponse HTTP au navigateur.kernel.exception: dispatché dès qu'une exception non attrapée survient pendant le traitement de la requête.
Vous avez déjà croisé
kernel.exceptionsans le savoir ! Lors de la séance sur la gestion des erreurs, Symfony transforme automatiquement chaque exception en réponse HTTP (page 404, page 500...) grâce à un mécanisme d'écoute sur cet événement précis. Ce que vous appreniez alors comme un comportement « magique » du framework est en réalité un simple Event Subscriber, comme ceux que vous allez créer aujourd'hui.
Créer et dispatcher son propre événement métier
Rien n'empêche de créer vos propres événements, propres à la logique métier de votre application. Un événement est une classe simple qui transporte les données utiles à ceux qui vont l'écouter.
Pour le dispatcher, on utilise le service EventDispatcherInterface, injecté comme n'importe quel autre service.
Le lien avec la séance sur les services est direct : un Event Subscriber est lui-même un service.
Grâce à l'auto-configuration de Symfony, toute classe qui implémente EventSubscriberInterface
est automatiquement enregistrée et abonnée aux événements qu'elle déclare, sans aucune configuration manuelle.
Exemple de mise en application
Reprenons l'exemple d'un blog. Après la publication d'un article, on souhaite déclencher une action (par exemple, préparer une notification) sans alourdir le contrôleur ni le service qui gère la publication.
D'abord, la classe d'événement :
// src/Event/ArticlePublieEvent.php
namespace App\Event;
use App\Entity\Article;
use Symfony\Contracts\EventDispatcher\Event;
class ArticlePublieEvent extends Event
{
public function __construct(
private Article $article
) {}
public function getArticle(): Article
{
return $this->article;
}
}
Ensuite, on dispatche cet événement depuis le service qui publie l'article :
// src/Service/ArticleManager.php
namespace App\Service;
use App\Entity\Article;
use App\Event\ArticlePublieEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class ArticleManager
{
public function __construct(
private EntityManagerInterface $entityManager,
private EventDispatcherInterface $eventDispatcher
) {}
public function publier(Article $article): void
{
$article->setPublie(true);
$this->entityManager->persist($article);
$this->entityManager->flush();
// On annonce que l'article vient d'être publié
$this->eventDispatcher->dispatch(new ArticlePublieEvent($article));
}
}
Enfin, un Subscriber réagit à cet événement :
// src/EventSubscriber/ArticlePublieSubscriber.php
namespace App\EventSubscriber;
use App\Event\ArticlePublieEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ArticlePublieSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger
) {}
public static function getSubscribedEvents(): array
{
return [
ArticlePublieEvent::class => 'onArticlePublie',
];
}
public function onArticlePublie(ArticlePublieEvent $event): void
{
$article = $event->getArticle();
$this->logger->info(sprintf(
'Nouvel article publié : "%s"',
$article->getTitre()
));
// Ici, on pourrait par exemple envoyer un e-mail de notification aux abonnés
}
}
Le service ArticleManager ne connaît rien de ArticlePublieSubscriber. On pourrait ajouter
dix autres Subscribers qui écoutent ArticlePublieEvent sans jamais toucher à ArticleManager.
C'est exactement le découplage recherché.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Votre défi pour aujourd'hui consiste à créer un événement métier
CommentaireAjouteEvent, dispatché à chaque fois qu'un commentaire est ajouté à un article, et un Subscriber qui y réagit.
Étape 1 — Créer la classe d'événement
La classe CommentaireAjouteEvent doit transporter le commentaire concerné.
Étape 2 — Dispatcher l'événement depuis le service
Dans CommentaireManager, après l'enregistrement du commentaire en base, il faut annoncer l'événement.
Un événement métier doit être dispatché une fois le fait accompli, c'est-à-dire après
que le flush() a réussi. Si on le dispatche avant, un Subscriber pourrait réagir à un
commentaire qui, finalement, n'a jamais été enregistré en base (erreur de validation, exception...).
Étape 3 — Créer le Subscriber
Le Subscriber doit écouter CommentaireAjouteEvent et journaliser l'ajout.
Résistez à la tentation de mettre toute la logique de notification (log, e-mail, mise à jour
d'un compteur...) dans un seul et même Subscriber. Préférez créer un Subscriber par responsabilité
(CommentaireAjouteSubscriber pour le log, CommentaireNotificationSubscriber pour l'e-mail...).
Chacun écoute le même événement, sans se connaître, et reste facile à tester isolément.