Domain Events with Symfony and Doctrine

Domain Events are important for any application that puts the business domain in the centre. A common requirement within a system is the ability to trigger an action based on the consequence of another. Domain Events is a pattern used in Domain Driven Design but can be used in any application where you set up the domain as the most important layer.

It’s important to understand that Domain Events are the past. Whenever a Domain Event is dispatched, it notifies that something happened in the past. This means that a Domain Event is always past tense.

If you dispatch an event that a Product is created, the moment the event is dispatched. That thing has already happened. Thus, the product was created.

Difference between Application Events and Domain Events

Whenever I start talking about Domain Events. I get the question; what is the difference between ‘normal’ events and Domain Events?

Well, a Domain Event is dispatched after a change in your Domain has happened. When a new product was created, or a user was registered. It’s always related to something that is important to the business.

Application Events, on the other hand, are events between components inside the core of your application. This allows the decoupling of components. If component A needs to trigger something in component B. Then A should dispatch an event that B listens on. If you, for example, want the stock to be decreased in the stock component whenever someone orders something in the Order component. Then the Order component should dispatch a DecreaseStock event. These application events are thus used to decouple the communications between components in a monolithic system or between micro-services.

You can also have infrastructure events. These can be events concerning infrastructure components like Doctrine. For example, dispatching Domain Events after Doctrine has flushed its entities.

Events come in different forms, but the most important of them is the Domain Event. That’s why we take a look at home to implement Domain Events in Symfony.

Creating our Entity and Event

We before we start let us create an entity called Product with a basic Doctrine ProductRepository.

<?php
namespace App\Domain\Entity;

class Product
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    public static function createWithData(string $name): Product
    {
        $self = new self();
        $self->setName($name);
        return $self;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }
}
<?php
namespace App\Infrastructure\Repository;

use App\Domain\Entity\Product;
use App\Domain\Repository\ProductRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;

final class ProductRepository implements ProductRepositoryInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function find(int $id): Product
    {
        return $this->entityManager->find(Product::class, $id);
    }

    public function save(Product $product): void
    {
        $this->entityManager->persist($product);
        $this->entityManager->flush();
    }

    public function remove(Product $product): void
    {
        $this->entityManager->remove($product);
        $this->entityManager->flush();
    }
}

So we want to raise a domain event when a new product is created.

<?php
namespace App\Domain\Event;

use Symfony\Component\EventDispatcher\Event;

final class ProductWasCreatedEvent extends Event
{
    /**
     * @var int
     */
    private $productId;

    /**
     * @var string
     */
    private $productName;

    public function __construct(int $productId, string $productName)
    {
        $this->productId = $productId;
        $this->productName = $productName;
    }

    public function getProductId(): int
    {
        return $this->productId;
    }

    public function getProductName(): string
    {
        return $this->productName;
    }
}

But this event should only be executed after the product has actually been persisted to the database. So we can not just dispatch the event at the same time we create a new product. The product is not yet persisted to the database, and might never be.

We need a solution to record events in the Product entity, and then dispatch them at the moment the Product is persisted and saved to the database.

Event recorder

We can do this by first setting up some interfaces.

<?php
namespace App\Domain\Shared;

interface ContainsEvents
{
    public function getRecordedEvents(): array;

    public function clearRecordedEvents(): void;
}
<?php
namespace App\Domain\Shared;

interface RecordsEvents
{
    public function record($event): void;
}

Now we can create a trait for a PrivateEventRecorder. This means we can add the interfaces to our entity, and then use this trait to easily give multiple entities the ability to record and keep its own event. We could also create a public event recorder that records its event into another object. But we are nog going to do that for now.

<?php
namespace App\Domain\Shared;

trait PrivateEventRecorder
{
    /**
     * @var array
     */
    private $messages = [];

    public function getRecordedEvents(): array
    {
        return $this->messages;
    }

    public function clearRecordedEvents(): void
    {
        $this->messages = [];
    }

    protected function record($message): void
    {
        $this->messages[] = $message;
    }
}

And then we apply the interfaces and trait to our entity.

class Product implements ContainsEvents, RecordsEvents
{
    use PrivateEventRecorder;

And now we can record the event that a product was created.

public static function createWithData(string $name): Product
{
    $self = new self();
    $self->setName($name);

    $self->record(new ProductWasCreatedEvent($self->getId(), $name));

    return $self;
}

Popping domain events

Now we have recorded events in our Entities. But they will not be popped and executed anywhere. We need to create a Doctrine EventSubscriber to listen for all pre-events to get all changed entities that contain events and then dispatch these events after Doctrine flushed.

So first we subscribe to prePersist, preUpdate, preRemove, preFlush and postFlush.

<?php
namespace App\Infrastructure\Persistence\Doctrine\EventSubscriber;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

final class DomainEventSubscriber implements EventSubscriber
{
    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @var Collection
     */
    private $entities;

    /**
     * DomainEventSubscriber constructor.
     * @param EventDispatcherInterface $eventDispatcher
     */
    public function __construct(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
        $this->entities = new ArrayCollection();
    }

    /**
     * Returns an array of events this subscriber wants to listen to.
     *
     * @return array
     */
    public function getSubscribedEvents(): array
    {
        return [
            'prePersist',
            'preUpdate',
            'preRemove',
            'preFlush',
            'postFlush'
        ];
    }
}

Next, we are going to create a collection of entities which contains events that were persisted, updated or removed.

/**
 * @param LifecycleEventArgs $args
 */
public function prePersist(LifecycleEventArgs $args): void
{
    $this->addContainsEventsEntityToCollection($args);
}

/**
 * @param LifecycleEventArgs $args
 */
public function preUpdate(LifecycleEventArgs $args): void
{
    $this->addContainsEventsEntityToCollection($args);
}

/**
 * @param LifecycleEventArgs $args
 */
public function preRemove(LifecycleEventArgs $args): void
{
    $this->addContainsEventsEntityToCollection($args);
}

/**
 * @param LifecycleEventArgs $args
 */
private function addContainsEventsEntityToCollection(LifecycleEventArgs $args): void
{
    $entity = $args->getEntity();
    if ($entity instanceof ContainsEvents) {
        $this->entities->add($entity);
    }
}

Then on the preFlush event, we are also going to look for any entities with events. I’m pretty sure we now have catched them all…

/**
    * @param PreFlushEventArgs $args
    */
   public function preFlush(PreFlushEventArgs $args): void
   {
       $unitOfWork = $args->getEntityManager()->getUnitOfWork();
       foreach ($unitOfWork->getIdentityMap() as $class => $entities) {
           if (!\in_array(ContainsEvents::class, class_implements($class), true)) {
               continue;
           }
           foreach ($entities as $entity) {
               $this->entities->add($entity);
           }
       }
   }

Then, at last after the entities have been flushed, and thus any changes persisted back to the database. Now we e are going to go over all the entities that we saved in our collection. Get the events and clear them from the entities. Then we dispatch those events through the event dispatcher.

/**
 * @param PostFlushEventArgs $args
 */
public function postFlush(PostFlushEventArgs $args): void
{
    $events = new ArrayCollection();

    foreach ($this->entities as $entity) {
        foreach ($entity->getRecordedEvents() as $domainEvent) {
            $events->add($domainEvent);
        }
        $entity->clearRecordedEvents();
    }

    /** @var Event $event */
    foreach ($events as $event) {
        $this->eventDispatcher->dispatch(\get_class($event), $event);
    }
}

As of last, before all this will work, we need to register this DomainEventSubscriber to the doctrine event system by adding the following configuration to services.yaml:

App\Infrastructure\Doctrine\EventSubscriber\DomainEventSubscriber:
    tags:
    - { name: doctrine.event_subscriber, connection: default }

Conclusion

Domain Events are very important if you want to create a system that is driven by the business or domain. This is mostly used in Domain Driven Design but can be applied on less strict Domain Driven systems.

If you are working on a new application. Maybe think about making the Domain more important, and thus make use of Domain Events. You will see that this will make your application easier to understand and to reason with. It makes translation between bussiness and developers easier.

As always, think before you code!