Aller au contenu principal
Maîtriser les Domain Events avec Symfony : déclencher des actions après flush() sans violer le DDD

Nous souhaitons créer une entité Notification à la suite de la sauvegarde d'une entité ServiceRequest. Nous voulons que cet événement se déclenche uniquement si certaines modifications sont effectuées sur l'entité.
Dans notre cas, nous souhaitons que cela se déclenche uniquement si la méthode markAsAccepted() est exécutée, mais il faut attendre la sauvegarde de l'entité avant de créer l'entité Notification.

  /**
   * Marque la demande comme acceptée par le prestataire
   *
   * Change le statut vers ACCEPTED
   *
   * @return self Pour permettre le chaînage de méthodes
   */
  public function markAsAccepted(): self {
    $this->stage = ServiceRequestStage::ACCEPTED;
    // On prépare une notification.
    return $this;
  }

Architecture :
Maîtriser les Domain Events avec Symfony

1 - Création d'un événement : ServiceRequestAcceptedEvent

Nous devons commencer par mettre en place un événement qui va permettre d'informer notre application qu’un événement métier s’est produit, en l’occurrence : l’utilisateur a accepté la commande.

<?php

namespace App\Event;

use App\Entity\ServiceRequest;

class ServiceRequestAcceptedEvent {

  public function __construct(public readonly ServiceRequest $serviceRequest) {
  }

  public function getServiceRequest(): ServiceRequest {
    return $this->serviceRequest;
  }
}

2 - Souscription à l’événement dans l’entité ServiceRequest

Dans notre entité, nous devons souscrire à l’événement.

use App\Event\ServiceRequestAcceptedEvent;

...

/**
 * Contient les événements qui vont être exécutés après le flush.
 *
 * @var array<object>
 */
private array $domainEvents = [];

...

/**
 * Récupère et vide les événements en attente.
 *
 * @return array<object>
 */
public function pullDomainEvents(): array {
  $events = $this->domainEvents;
  $this->domainEvents = [];
  return $events;
}

...

/**
 * Marque la demande comme acceptée par le prestataire
 *
 * Change le statut vers ACCEPTED
 *
 * @return self Pour permettre le chaînage de méthodes
 */
public function markAsAccepted(): self {
  $this->stage = ServiceRequestStage::ACCEPTED;
  // On prépare une notification.
  $this->domainEvents[] = new ServiceRequestAcceptedEvent($this);
  return $this;
}

3 - Écoute des sauvegardes d’entités

Dans cette phase, on vérifie si parmi les entités qui sont sauvegardées, il y en a qui ont souscrit à des événements. Pour ces entités, on va publier les événements correspondants.

<?php

namespace App\EventSubscriber;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Events;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;

#[AsDoctrineListener(Events::onFlush)]
#[AsDoctrineListener(Events::postFlush)]
class DomainEventSubscriber {
  private array $collectedEntities = [];

  public function __construct(private readonly EventDispatcherInterface $dispatcher) {
  }

  public function onFlush(OnFlushEventArgs $event): void {
    $em = $event->getObjectManager();
    $uow = $em->getUnitOfWork();
    /**
     * On collecte les données.
     */
    foreach ($uow->getScheduledEntityUpdates() as $entity) {
      if (method_exists($entity, 'pullDomainEvents')) {
        $this->collectedEntities[] = $entity;
      }
    }

    /**
     * On collecte les données.
     */
    foreach ($uow->getScheduledEntityInsertions() as $entity) {
      if (method_exists($entity, 'pullDomainEvents')) {
        $this->collectedEntities[] = $entity;
      }
    }
  }

  /**
   * On collecte les données.
   *
   * @param PostFlushEventArgs $args
   */
  public function postFlush(PostFlushEventArgs $args): void {
    foreach ($this->collectedEntities as $entity) {
      $events = $entity->pullDomainEvents();
      foreach ($events as $event) {
        $this->dispatcher->dispatch($event);
      }
    }
    // Important : on nettoie après le flush
    $this->collectedEntities = [];
  }
}

4 - Écoute de l’événement

À ce stade, on va créer effectivement la notification.

<?php

namespace App\EventListener;

use App\Event\ServiceRequestAcceptedEvent;
use App\Entity\ {
  Notification,
  NotificationReceipt
};
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Core\User\UserInterface;

#[AsEventListener]
class ServiceRequestAcceptedListener {

  public function __construct(private readonly EntityManagerInterface $em, private readonly Security $security) {
  }

  public function __invoke(ServiceRequestAcceptedEvent $event): void {
    $serviceRequest = $event->serviceRequest;
    $currentUser = $this->getCurrentUser();
    // Création de la notification
    $notification = new Notification();
    $notification->setSender($currentUser);
    $notification->setTitle("Votre demande a été acceptée.");
    $notification->setMessage("Votre demande a été acceptée.");
    $this->em->persist($notification);
    // Envoie au client.
    $notificationReceipt = new NotificationReceipt();
    $notificationReceipt->setRecipient($serviceRequest->getClient()->getUser());
    $notificationReceipt->setNotification($notification);
    $this->em->persist($notificationReceipt);
    // Cet execution se fait apres le flush() de l'entite qui la creer, donc on
    // fait lance un flux.
    $this->em->flush();
  }

  private function getCurrentUser(): UserInterface {
    $user = $this->security->getUser();

    if (null === $user) {
      throw new \RuntimeException('Aucun utilisateur authentifié');
    }

    return $user;
  }
}




À lire :
DDD : Domain Driven Design avec PHP & Symfony

Profile picture for user admin Stephane K

Écrit le

Il y'a 1 jour
Modifié
Il y'a 23 heures
Loading ...
WhatsApp
Support Habeuk : +237 694 900 622
WhatsApp Send