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 :

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