---
title: 'Events Symfony : créer un événement et enregistrer ses listeners'
description: 'Crée un événement métier Symfony, branche un listener avec AsEventListener ou un tag de service, vérifie les priorités et distingue le dispatch synchrone de Messenger.'
pubDate: 2022-10-25
updatedDate: 2026-10-18
family: 'php-symfony'
tags: []
heroImage: '/images/maitriser-les-events-et-les-event-listeners-avec-symfony.webp'
videoUrl: 'https://www.youtube.com/watch?v=Ze3oi8DwpVk'
---

Tu veux réagir à la publication d'un article sans ajouter chaque réaction dans le service qui le
publie ? Un événement porte l'information, un dispatcher la transmet et des listeners exécutent les
réactions. Tu peux ainsi ajouter un journal de publication sans que le producteur connaisse sa classe.

La vidéo d'octobre 2022 présente Symfony 6. Ce guide écrit utilise la
[documentation EventDispatcher de Symfony 7.4](https://symfony.com/doc/7.4/event_dispatcher.html) et des
exemples exécutés sur cette branche. Il ne s'agit pas d'une transcription de la vidéo. La mise à jour
apporte un événement métier, les deux modes d'enregistrement et des contrôles reproductibles.

## Prérequis et limite importante : le dispatch reste synchrone

Pars d'une application Symfony avec FrameworkBundle et des services autowirés. Symfony 7.4 requiert
[PHP 8.2 ou supérieur](https://symfony.com/releases/7.4). Le composant EventDispatcher est déjà une
dépendance du framework ; tu peux vérifier tes versions avec :

```bash
composer show symfony/framework-bundle
composer show symfony/event-dispatcher
```

Dans une application PHP sans framework, la
[procédure d'installation du composant](https://symfony.com/doc/7.4/event_dispatcher.html#installation)
est `composer require symfony/event-dispatcher`, mais l'autoconfiguration des services ne vient pas
avec le composant seul.

**Un listener n'est pas une tâche en arrière-plan.** Le dispatcher appelle les listeners dans le
processus courant avant de te rendre la main. Un listener lent ralentit donc l'appelant ; une exception
non interceptée interrompt le dispatch et remonte à l'appelant. Ce comportement est visible dans
[l'implémentation du dispatcher](https://github.com/symfony/event-dispatcher/blob/7.4/EventDispatcher.php).

Si tu veux une file d'attente, des workers et des reprises après échec, regarde
[Symfony Messenger](https://symfony.com/doc/7.4/messenger.html). Messenger n'est pas automatiquement
asynchrone non plus : il faut configurer un transport, router le message vers celui-ci et faire tourner
un worker. Ajouter `AsEventListener` ne réalise aucune de ces étapes.

## Créer un événement métier avec les données utiles

Place cette classe dans `src/Event/ArticlePublished.php` :

```php
<?php

namespace App\Event;

final class ArticlePublished
{
    public function __construct(public readonly int $articleId)
    {
    }
}
```

Un objet PHP suffit. Il n'a pas besoin d'étendre une classe du framework pour être dispatché. Le choix
de ne transmettre qu'un identifiant garde ici le contrat simple ; il n'implique pas que l'article est
chargé ni qu'une transaction a été validée.

Le [dispatch d'un événement personnalisé](https://symfony.com/doc/7.4/event_dispatcher.html#creating-and-dispatching-custom-events)
utilise par défaut le nom complet de sa classe. Tu peux aussi fournir un nom explicite au dispatcher,
mais tes listeners doivent alors écouter ce même nom.

## Déclarer un listener avec AsEventListener

Crée `src/EventListener/PublicationListener.php` :

```php
<?php

namespace App\EventListener;

use App\Event\ArticlePublished;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: ArticlePublished::class, priority: 20)]
final class PublicationListener
{
    public function __construct(private LoggerInterface $logger)
    {
    }

    public function __invoke(ArticlePublished $event): void
    {
        $this->logger->info('Article publié', [
            'article_id' => $event->articleId,
        ]);
    }
}
```

L'attribut identifie l'événement et la priorité ; `__invoke()` reçoit l'objet dispatché.
Le logger doit être disponible comme service, par exemple via l'intégration Monolog de ton application.
Le test utilise un logger en mémoire, sans fichier ni service distant.

L'attribut ne suffit pas si la classe n'est pas enregistrée comme service. Avec une configuration
standard, conserve `autoconfigure: true` et un import qui couvre le dossier `EventListener`.
Pour cet exemple, le fichier `config/services.yaml` contient :

```yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true

  App\:
    resource: '../src/'
    exclude:
      - '../src/Kernel.php'
      - '../src/Event/'
```

Adapte cet extrait à tes exclusions existantes, sans écraser la configuration de ton projet.
Les objets événement sont créés avec `new` : ils ne sont pas des services à autowirer.
La [documentation des attributs de listener](https://symfony.com/doc/7.4/event_dispatcher.html#defining-event-listeners-with-php-attributes)
décrit aussi l'inférence du type d'événement ; ici, le nom explicite rend le branchement lisible.

## Dispatcher l'événement depuis un service

Ajoute `src/Service/PublicationNotifier.php` :

```php
<?php

namespace App\Service;

use App\Event\ArticlePublished;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class PublicationNotifier
{
    public function __construct(private EventDispatcherInterface $dispatcher)
    {
    }

    public function notify(int $articleId): void
    {
        $this->dispatcher->dispatch(new ArticlePublished($articleId));
    }
}
```

Injecte ce service dans ton cas d'usage et appelle `notify(42)` au moment où l'article est réellement
publié. Cet exemple ne sauvegarde rien en base : il montre uniquement la notification.
Dans une application Doctrine, choisis ce moment selon ta transaction ; un événement PHP n'est pas une
garantie de commit ni de livraison durable.

La signature est `dispatch($event)` : l'objet vient en premier, le nom éventuel en second. Si tu envoies
`dispatch(new ArticlePublished(42), 'article.published')`, le listener de cet article, enregistré sur
la classe, ne sera pas appelé sous ce nom différent.

## Variante : enregistrer le listener par un tag de service

Si tu préfères la configuration YAML, garde la classe mais désactive son autoconfiguration sur cette
définition et ajoute le tag explicite dans le même bloc `services` :

```yaml
App\EventListener\PublicationListener:
  autowire: true
  autoconfigure: false
  tags:
    - name: kernel.event_listener
      event: App\Event\ArticlePublished
      method: __invoke
      priority: 20
```

C'est une **alternative** au branchement par attribut, pas un second enregistrement à cumuler. Ici,
`autoconfigure: false` empêche l'attribut d'ajouter un tag en plus. Cette variante a aussi été exécutée
séparément dans le test : un dispatch produit un seul appel du logger.
Les [tags des listeners](https://symfony.com/doc/7.4/event_dispatcher.html#creating-an-event-listener)
permettent de préciser l'événement, la méthode et la priorité au même endroit.

Sans FrameworkBundle, tu dois enregistrer le callable toi-même. L'équivalent autonome est :

```php
$dispatcher->addListener(ArticlePublished::class, $listener, 20);
$dispatcher->dispatch(new ArticlePublished(42));
```

Ce fragment suppose un `EventDispatcher` instancié et un objet `$listener` déjà construit. Il a été
exécuté séparément : le composant seul ne scanne pas les attributs de tes classes.

## Comprendre l'ordre des listeners et la propagation

Une priorité plus élevée s'exécute avant une priorité plus basse : **20 avant 0, puis -10**.
À priorité égale, l'ordre dépend de l'enregistrement ; évite d'en faire une dépendance métier implicite.
Dans le test, un second listener de priorité -10 a servi de sonde : le journal de priorité 20 était
bien rempli avant son appel, puis `notify()` a rendu la main.

Un [subscriber](https://symfony.com/doc/7.4/event_dispatcher.html#creating-an-event-subscriber) regroupe
les événements et priorités dans `getSubscribedEvents()` et implémente `EventSubscriberInterface`.
C'est utile pour rassembler plusieurs réactions cohérentes ; ce n'est pas un mode d'exécution
asynchrone. Inutile de transformer un listener unique uniquement pour obtenir une priorité.

Si tu as besoin d'arrêter les listeners suivants, Symfony fournit
[`Event::stopPropagation()`](https://symfony.com/doc/7.4/event_dispatcher.html#stopping-event-flow-propagation).
Cette API nécessite un événement stoppable, par exemple une classe qui étend
`Symfony\Contracts\EventDispatcher\Event`. **La classe minimale `ArticlePublished` ci-dessus ne
possède pas cette méthode.** L'arrêt de propagation n'annule pas les effets déjà produits par les
listeners précédents et ne constitue pas un rollback.

## Vérifier le branchement et diagnostiquer un silence

Cette commande a été exécutée dans l'application de test :

```bash
php bin/console debug:event-dispatcher 'App\Event\ArticlePublished'
```

Elle a listé `PublicationListener::__invoke()` avec la priorité 20, dans les deux modes de
configuration testés séparément. Le [diagnostic des listeners](https://symfony.com/doc/7.4/event_dispatcher.html#debugging-event-listeners)
permet de vérifier le branchement avant de soupçonner ton code métier.

Si rien ne se passe, contrôle dans cet ordre :

1. **L'appel est-il atteint ?** Créer une classe événement ne déclenche aucun dispatch.
2. **Le nom est-il identique ?** Compare la classe écoutée et le nom passé à `dispatch()`.
3. **Le listener est-il un service ?** Vérifie l'import du dossier, les exclusions et l'autoconfiguration.
4. **Apparaît-il une ou plusieurs fois ?** Évite le cumul du tag manuel et de l'attribut autoconfiguré.
5. **Un autre listener interrompt-il le traitement ?** Cherche une exception ou un arrêt de propagation.
6. **Le logger est-il observable ?** Dans ton application, vérifie les handlers et niveaux de log ; aucun
   fichier précis n'est garanti par cet exemple.

## Périmètre des tests réellement exécutés

Le 18 octobre 2026, les trois classes PHP et les configurations YAML ont été extraites de cet article.
Les contrôles ont tourné avec PHP **8.4.26**, FrameworkBundle **7.4.21** et EventDispatcher **7.4.17** :

- attribut et autoconfiguration : le logger en mémoire reçoit une seule entrée `Article publié` avec
  le contexte `article_id` égal à 42 ;
- tag YAML avec autoconfiguration désactivée : même résultat, sans double appel ;
- listener de priorité 20 avant la sonde de priorité -10, puis retour à l'appelant ;
- nom explicite différent de la classe : aucun appel du listener enregistré sur cette classe ;
- listener qui lève une exception : le listener suivant n'est pas appelé et l'exception remonte ;
- enregistrement manuel avec `addListener()` : réception de l'événement vérifiée.

Ces tests ne couvrent ni Doctrine, ni Monolog sur disque, ni une file Messenger, ni l'envoi d'un e-mail.
Pour relier cette mécanique à une entrée HTTP, consulte le
[guide du routing Symfony](/blog/tout-savoir-sur-le-routing-dans-symfony/), puis le
[parcours Symfony et backend](/blog/symfony-backend/). Tu peux regarder la vidéo associée pour retrouver
la présentation d'origine sur Symfony 6.
