Aller au contenu principal

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.

info

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 EventSubscriberInterface et définit une méthode statique getSubscribedEvents(). 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éristiqueEvent ListenerEvent Subscriber
Nombre d'événements écoutésUn seulUn ou plusieurs
DéclarationConfiguration ou attribut PHPToute la logique dans la classe elle-même
Où trouver la liste des événements écoutésDans le fichier de configurationDans la méthode getSubscribedEvents()
astuce

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.exception sans 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.

remarque

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
}
}
info

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


Quel pattern de conception le composant EventDispatcher de Symfony met-il en œuvre ?


Quel est l'intérêt principal du système d'événements dans Symfony ?


Quelle interface une classe doit-elle implémenter pour devenir un Event Subscriber ?


Quelle méthode statique doit définir un Event Subscriber ?


Quelle différence principale distingue un Event Listener d'un Event Subscriber ?


Quel événement du noyau Symfony est dispatché lorsqu'une exception non attrapée survient ?


Quel service Symfony permet de dispatcher un événement depuis son propre code ?


Pourquoi un Event Subscriber n'a-t-il pas besoin d'être déclaré manuellement dans la configuration des services ?



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.


Bonne pratique - Dispatcher après la persistance, pas avant

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.


Bonne pratique - Un Subscriber par responsabilité

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.

📌 Une solution