Retour au cours

backend / php-laravel

Events et listeners

Leçon 191 exercice

Explication

Ce que vous allez apprendre

  • Comprendre comment les events découplent "ce qui se passe" de "ce qu'on en fait"
  • Déclencher un event et lui faire porter uniquement les données nécessaires
  • Enregistrer plusieurs listeners indépendants pour un même event
  • Rendre un listener asynchrone en une seule interface (ShouldQueue), sans toucher au code du déclencheur
  • Diffuser un event en temps réel côté client avec ShouldBroadcast

Dans quel contexte ?

Quand une commande est validée, trois choses distinctes doivent se produire : envoyer un email de confirmation, décrémenter le stock des produits commandés, et notifier en temps réel un tableau de bord administrateur. Sans events, ces trois responsabilités finiraient toutes entassées dans la même méthode valider(), la rendant difficile à faire évoluer sans risquer de casser autre chose.

D'abord, séparer le déclencheur de ses conséquences

Un event (CommandeValidee) est une simple classe qui transporte des données (ici, la commande concernée), sans aucune logique de traitement. CommandeService::valider() se contente de déclencher l'event avec event(new CommandeValidee($commande)), sans jamais savoir ni se soucier de ce qui va réagir à cet événement.

Ce découplage change fondamentalement l'évolutivité du code : ajouter une quatrième réaction à la validation d'une commande ne nécessite de toucher ni au service, ni aux listeners existants — juste d'ajouter un nouveau listener à la liste.

Prérequis

Il faut avoir compris les jobs et les queues (leçon précédente) : un listener peut lui aussi devenir asynchrone via ShouldQueue, exactement comme un job.

Ensuite, plusieurs réactions indépendantes au même déclencheur

EventServiceProvider associe un event à une liste de listeners : EnvoyerConfirmationCommande et DecrementerStock réagissent tous deux à CommandeValidee, sans se connaître mutuellement ni connaître le code qui a déclenché l'event. Si l'un des deux échoue, ça n'affecte pas nécessairement l'autre.

ÉlémentRôleSait-il ce qui l'a déclenché ?
EventPorte les données du fait survenuNon, juste les données transmises
ListenerRéagit à l'event avec une action préciseNon, ignore les autres listeners
Dispatcher (event(...))Déclenche l'eventIgnore qui va réagir

Piège fréquent

Empiler trop de logique métier critique directement dans des listeners synchrones peut ralentir l'action initiale sans qu'on s'en rende compte, exactement comme un traitement lourd placé directement dans un contrôleur. Dès qu'un listener effectue un appel réseau ou un traitement lent, ShouldQueue doit être envisagé.

Il reste à rendre certaines réactions asynchrones sans effort

Ajouter implements ShouldQueue à un listener suffit à le faire traiter par un worker en arrière-plan, exactement comme les jobs vus en leçon précédente — sans qu'il faille changer une seule ligne du code qui déclenche l'event. C'est l'un des plus grands avantages du système d'events Laravel : la décision "synchrone ou asynchrone" se prend au niveau du listener, jamais au niveau du déclencheur.

Maintenant, informer le client en temps réel, pas seulement le serveur

ShouldBroadcast diffuse un event via WebSocket (Reverb ou Pusher) vers un canal spécifique, permettant à une interface JavaScript connectée de réagir instantanément sans avoir à interroger le serveur en boucle (polling). broadcastOn() définit précisément quel canal reçoit l'information, et PrivateChannel garantit que seul le client concerné (ici, le client propriétaire de la commande) peut l'écouter.

Astuce

Pour un cas ponctuel qui ne mérite pas une classe listener dédiée, Event::listen(function (CommandeValidee $event) { ... }) accepte directement une closure — pratique pour du prototypage ou une réaction très simple, sans créer un fichier entier.

Maintenant que le code métier est bien découplé et réactif, il devient temps de s'assurer qu'il fonctionne réellement comme prévu : la prochaine leçon couvre les tests automatisés avec PHPUnit et Pest.

Commandes & code

Events et listeners

php
<?php
// app/Events/CommandeValidee.php : un event porte des données, rien d'autre
namespace App\Events;

use App\Models\Commande;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Broadcasting\PrivateChannel;

class CommandeValidee implements ShouldBroadcast {
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public readonly Commande $commande) {}

    // Diffusion temps réel (WebSocket via Reverb/Pusher) sur un canal privé
    public function broadcastOn(): array {
        return [new PrivateChannel("client.{$this->commande->client_id}")];
    }

    public function broadcastAs(): string {
        return 'commande.validee';
    }
}

// app/Listeners/EnvoyerConfirmationCommande.php
namespace App\Listeners;

use App\Events\CommandeValidee;
use Illuminate\Contracts\Queue\ShouldQueue;

class EnvoyerConfirmationCommande implements ShouldQueue {
    public function handle(CommandeValidee $event): void {
        $event->commande->client->notify(
            new \App\Notifications\CommandeConfirmeeNotification($event->commande)
        );
    }
}

class DecrementerStock {
    public function handle(CommandeValidee $event): void {
        foreach ($event->commande->produits as $produit) {
            $produit->decrement('stock', $produit->pivot->quantite);
        }
    }
}

// app/Providers/EventServiceProvider.php (ou config auto-discovery Laravel 11+)
namespace App\Providers;

use App\Events\CommandeValidee;
use App\Listeners\{EnvoyerConfirmationCommande, DecrementerStock};
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider {
    protected $listen = [
        // Plusieurs listeners indépendants réagissent au même event -- découplage fort
        CommandeValidee::class => [
            EnvoyerConfirmationCommande::class,
            DecrementerStock::class,
        ],
    ];
}

// Déclenchement de l'event depuis le code métier
namespace App\Services;

use App\Events\CommandeValidee;

class CommandeService {
    public function valider(\App\Models\Commande $commande): void {
        $commande->update(['statut' => 'validee']);
        event(new CommandeValidee($commande));  // ou CommandeValidee::dispatch($commande);
    }
}

// Listener anonyme (closure) pour un cas ponctuel, sans classe dédiée
\Illuminate\Support\Facades\Event::listen(function (CommandeValidee $event) {
    \Log::info("Commande {$event->commande->id} validée");
});

Résumé

  • Les events découplent "ce qui se passe" (validation d'une commande) de "ce qu'on en fait" (email, stock, log).
  • Plusieurs listeners peuvent réagir au même event indépendamment, sans se connaître entre eux.
  • ShouldQueue sur un listener le rend asynchrone automatiquement, sans changer le code du dispatcher.
  • ShouldBroadcast diffuse l'event en temps réel côté client via WebSocket (Reverb/Pusher).

Exercices pratiques

1 disponible
1

Mission : demeler un stock qui se decremente deux fois

Objectif : Diagnostiquer un listener duplique et concevoir une reaction supplementaire sans toucher au code existant.

Contexte

Le support signale que le stock de certains produits chute plus vite que le nombre de commandes reellement validees. En inspectant EventServiceProvider, tu remarques que DecrementerStock apparait DEUX FOIS dans le tableau $listen[CommandeValidee::class], probablement colle par erreur lors d'un merge Git. Par ailleurs, la direction souhaite ajouter une quatrieme reaction : enregistrer chaque commande validee dans un tableau de bord analytique externe, sans jamais ralentir la reponse HTTP de validation ni risquer de faire echouer la validation si ce service externe est indisponible.

Résoudre l’exercice →