Skip to content

Les événements ​

Quand quelque chose d'important se produit dans le domaine — une commande est passée, un statut change, un compte est créé — le cœur émet un événement. Vous y réagissez par un listener, sans modifier le code qui l'émet.

Le problème ​

À la validation d'une commande, il faut envoyer un e-mail de confirmation, décrémenter le stock, notifier un ERP… Ces réactions appartiennent à des packages différents, et le cœur ne doit en connaître aucune. L'événement est le point de découplage.

Le principe ​

Le cœur émet ; plusieurs listeners écoutent, indépendamment.

Les événements de domaine ​

ÉvénementQuand
OrderPlacedcommande créée et persistée
OrderStatusChangedchangement de statut de commande
OrderModifiedcommande retouchée après coup (ligne, montant, adresses), totaux refaits
CartItemAdded / CartItemUpdated / CartItemRemovedmodification du panier
CartMergedfusion panier invité → compte
UserRegistered / UserApproved / UserEmailChangedcycle de vie du compte
CurrencyChangedchangement de devise
ReindexRequesteddemande de réindexation produit
CheckoutStarted / CheckoutStepCompletedentrée dans le tunnel de commande, puis validation d'une étape
CheckoutCompletedle tunnel a abouti — à distinguer d'OrderPlaced, qui constate la création d'une commande d'où qu'elle vienne (import, back-office, API)
ProductViewed / CategoryViewed / SearchPerformedconsultation du catalogue

Chaque événement porte son contexte en propriétés readonly : OrderStatusChanged expose $order, $status et $previous — l'historique qui précède la transition, null à la toute première ; CheckoutStarted et CheckoutStepCompleted portent $cart et $step (la clé de l'étape servie ou validée) ; CategoryViewed porte $filters et $sort, SearchPerformed $terms et $results. UserApproved marque la validation d'une inscription en attente par un administrateur : quand les inscriptions sont soumises à validation, c'est là — et non à la création du compte — que l'onboarding commence. La liste à jour est dans la référence.

Déclarés par le cœur, émis par le thème

Les événements de parcours (tunnel, consultation) sont déclarés par le framework bien qu'émis par le thème — comme les noms de routes et les slots. Un package de mesure ou de recommandation s'y branche sans dépendre d'un thème particulier.

Empêcher une action : les événements annulables ​

Les événements ci-dessus constatent (participe passé : CartItemAdded, OrderPlaced). D'autres annoncent une intention avant qu'elle ne soit exécutée (participe présent) : un écouteur peut alors s'y opposer. C'est le seul moyen d'empêcher une action du cœur sans remplacer le service qui la porte.

ÉvénementRefus
CartItemAdding / CartItemUpdating / CartItemRemovingla méthode du CartService rend false, le panier est inchangé
OrderPlacingActionCancelled est levée avant la transaction — rien n'est écrit
OrderStatusChangingActionCancelled est levée, le statut ne change pas (rien n'est émis quand le statut demandé est déjà le statut courant)
OrderModifyingActionCancelled est levée, la commande n'est pas retouchée
ModelDeletingDeletionService::delete() lève ActionCancelled, l'objet n'est pas supprimé
ModelSavingSavingService::save() lève ActionCancelled, l'objet n'est ni créé ni modifié

OrderModifying couvre toute modification d'une commande existante et se lit à deux niveaux : la clé $change (line.added, line.updated, line.removed, component.updated, component.removed, addresses.updated) dit ce qu'on s'apprête à faire, $context sur quoi. Un écouteur peut ainsi n'en refuser qu'une partie — retoucher un montant, mais pas ajouter un produit. Le verrou, lui, est vérifié avant : une commande fermée par l'OrderLockPolicy est refusée sans que l'événement soit émis (le cœur en réutilise seulement le type pour porter le motif dans l'ActionCancelled) — un écouteur ne voit donc passer que les modifications qui étaient permises. Son pendant de constat, OrderModified, porte la même clé et le détail des valeurs ($changes, en before/after) : c'est là que se branche ce qui doit réagir après — réémettre un document, prévenir un ERP, alerter un commercial.

ModelDeleting porte le $model que le DeletionService s'apprête à supprimer : c'est par ce service que passent les suppressions des écrans d'administration. Ce n'est pas l'événement deleting d'Eloquent. Celui-là annule sans rien dire (delete() rend false), alors que ModelDeleting fait remonter le motif jusqu'à la personne qui a cliqué. Le cœur s'en sert lui-même : Slab\Framework\Listeners\RefuseDeletingShopDefaults refuse la suppression de la langue, de la devise et de la zone de la boutique, et Slab\Framework\Listeners\RefuseDeletingOneself empêche un utilisateur de supprimer son propre compte.

ModelSaving est son pendant pour le SavingService, par où passent les créations et modifications des écrans — et celles d'une IA. Il est émis les nouvelles valeurs déjà posées : $saving->model->isDirty('value') dit si un champ change, $saving->model->exists s'il s'agit d'une création.

php
use Slab\Framework\Core\Cart\Events\CartItemAdding;

Event::listen(CartItemAdding::class, function (CartItemAdding $event): void {
    $available = $event->product->stock_available; // null = stock non suivi

    if ($available !== null && $available < $event->quantity) {
        $event->cancel(__('Stock insuffisant.'));
    }
});

Le motif du premier écouteur qui s'oppose est conservé : c'est lui qui remonte à l'appelant, et qui porte le message de l'exception Slab\Framework\Core\Event\ActionCancelled (laquelle expose aussi l'événement refusé, via $exception->event).

Pour émettre un tel événement dans votre propre package, faites-lui implémenter Slab\Framework\Core\Event\Contracts\CancellableEvent et utilisez le trait Slab\Framework\Core\Event\Cancellable : sa méthode statique propose(...) construit l'événement, le donne aux écouteurs et le rend — là où dispatch() rend les réponses des écouteurs, seul l'événement porte le veto. Le motif se relit ensuite avec reason().

php
$event = MonIntention::propose($contexte);

if ($event->isCancelled()) {
    return false; // ou throw new ActionCancelled($event);
}

Écouter un événement ​

Dans un package, l'écoute se déclare explicitement (un provider vendor n'a pas l'auto-découverte des listeners). Le plus simple, au boot() :

php
use Illuminate\Support\Facades\Event;
use Slab\Framework\Core\Order\Events\OrderPlaced;

Event::listen(OrderPlaced::class, function (OrderPlaced $event) {
    // $event->order : la commande figée
    NotifierErp::dispatch($event->order); // déléguez à un Job pour ne pas bloquer la requête
});

Pour une vraie feature, préférez un EventServiceProvider dédié avec un listener dédié (queueable) — c'est ce que fait le cœur pour l'e-mail de statut de commande (Slab\Framework\Listeners\SendOrderStatusMail) et pour la réindexation (Slab\Framework\Listeners\ReindexProducts).

Émettre vos propres événements ​

Rien ne vous empêche de définir et d'émettre vos événements dans votre package (MonEvent::dispatch(...)) pour que d'autres parties s'y branchent. En revanche, les événements du cœur restent émis par le cœur : vous les écoutez, vous ne les déclenchez pas à sa place.

Voir aussi ​