Skip to content

Instantly share code, notes, and snippets.

@beberlei
Last active May 16, 2020
Embed
What would you like to do?
Doctrine + DomainEvents
vendor/
db.sqlite
composer.lock
{
"require": {
"doctrine/orm": "@stable"
}
}
<?php
namespace MyProject\Domain;
use Doctrine\ORM\Mapping as ORM;
use MyProject\DomainSuperTypes\AggregateRoot;
/**
* @Entity
*/
class InventoryItem extends AggregateRoot
{
/**
* @Id @GeneratedValue @Column(type="integer")
*/
private $id;
/**
* @Column
*/
private $name;
/**
* @Column(type="integer")
*/
private $counter = 0;
public function __construct($name)
{
$this->name = $name;
$this->raise('InventoryItemCreated', array('name' => $name));
}
public function rename($name)
{
$this->name = $name;
$this->raise('InventoryItemRenamed', array('name' => $name));
}
public function checkIn($count)
{
$this->counter += $count;
$this->raise('ItemsCheckedIntoInventory', array('count' => $count));
}
public function remove($count)
{
$this->counter -= $count;
$this->raise('ItemsRemovedFromInventory', array('count' => $count));
}
}
class EchoInventoryListener
{
public function onInventoryItemCreated($event)
{
printf("New item created with name %s\n", $event->name);
}
public function onInventoryItemRenamed($event)
{
printf("Item was renamed to %s\n", $event->name);
}
public function onItemsCheckedIntoInventory($event)
{
printf("There were %d new items checked into inventory\n", $event->count);
}
public function onItemsRemovedFromInventory($event)
{
printf("There were %d items removed from inventory\n", $event->count);
}
}
<?php
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\EventManager;
use MyProject\DomainEvents\DirectEventDispatcher;
use MyProject\DomainEvents\DomainEventListener;
use MyProject\Domain\InventoryItem;
use MyProject\Domain\EchoInventoryListener;
require_once __DIR__ . "/vendor/autoload.php";
require_once "DomainEventDispatcher.php";
require_once "DomainSuperTypes.php";
require_once "Domain.php";
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__));
$conn = array(
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/db.sqlite',
);
$evm = new EventManager();
$evm->addEventListener(
array('postInsert', 'postUpdate', 'postRemove', 'postFlush'),
new DomainEventListener()
);
$evm->addEventListener(
array('onInventoryItemCreated', 'onInventoryItemRenamed', 'onItemsRemovedFromInventory', 'onItemsCheckedIntoInventory'),
new EchoInventoryListener
);
$entityManager = EntityManager::create($conn, $config, $evm);
$schemaTool = new SchemaTool($entityManager);
try {
$schemaTool->createSchema(array($entityManager->getClassMetadata(InventoryItem::CLASS)));
} catch(\Exception $e) {
}
$item = new InventoryItem('Cookies');
$item->checkIn(10);
$entityManager->persist($item);
$entityManager->flush();
$item->rename('Chocolate Cookies');
$item->remove(5);
$entityManager->flush();
<?php
namespace MyProject\DomainEvents;
use Doctrine\ORM\EntityManager;
use MyProject\DomainSuperTypes\AggregateRoot;
class DomainEventListener
{
private $entities = array();
public function postPersist($event)
{
$this->keepAggregateRoots($event);
}
public function postUpdate($event)
{
$this->keepAggregateRoots($event);
}
public function postRemove($event)
{
$this->keepAggregateRoots($event);
}
public function postFlush($event)
{
$entityManager = $event->getEntityManager();
$evm = $entityManager->getEventManager();
foreach ($this->entities as $entity) {
$class = $entityManager->getClassMetadata(get_class($entity));
foreach ($entity->popEvents() as $event) {
$event->setAggregate($class->name, $class->getSingleIdReflectionProperty()->getValue($entity));
$evm->dispatchEvent("on" . $event->getName(), $event);
}
}
$this->entities = array();
}
private function keepAggregateRoots($event)
{
$entity = $event->getEntity();
if (!($entity instanceof AggregateRoot)) {
return;
}
$this->entities[] = $entity;
}
}
<?php
namespace MyProject\DomainSuperTypes;
use Doctrine\Common\EventArgs;
abstract class AggregateRoot
{
private $events = array();
public function popEvents()
{
$events = $this->events;
$this->events = array();
return $events;
}
protected function raise($eventName, array $properties)
{
$this->events[] = new DomainEvent($eventName, $properties);
}
}
class DomainEvent extends EventArgs
{
private $eventName;
private $properties;
private $aggregateClass;
private $aggregateId;
private $time;
public function __construct($eventName, array $properties)
{
$this->eventName = $eventName;
$this->properties = $properties;
$this->time = microtime(true);
}
public function getTime()
{
return $this->time;
}
public function getName()
{
return $this->eventName;
}
public function __get($name)
{
if (!isset($this->properties[$name])) {
throw new \RuntimeException("Property '" . $name . "' does not exist on event '" . $this->eventName);
}
return $this->properties[$name];
}
public function setAggregate($class, $id)
{
$this->aggregateClass = $class;
$this->aggregateId = $id;
}
}
@fabwu

This comment has been minimized.

Copy link

@fabwu fabwu commented Jun 25, 2018

Hi Benjamin,

I've just stumbled upon your example for implementing domain events and I really like your approach.

I've implemented your example but I get an infinity loop when I save an entity in a event handler:

// Saving another entity in a handler
public function onInventoryItemCreated($event) 
{
        $this->entityManager->persist($fooEntity);
        $this->entityManager->flush();
}

This triggers a flush event again and ends in a infinity loop. Did you experience this issue as well?

EDIT: I solved the problem. Just for future reference: I didn't implemented a popEvents() function but a getEvents() and clearEvents() function so the events got cleared too late and that's the reasons for the infinity loop,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment