Skip to content

Instantly share code, notes, and snippets.

@beberlei
Last active December 10, 2021 19:26
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save beberlei/53cd6580d87b1f5cd9ca to your computer and use it in GitHub Desktop.
Save beberlei/53cd6580d87b1f5cd9ca to your computer and use it in GitHub Desktop.
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
Copy link

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,

@OssiPesonen
Copy link

I know I'm three years later in this, but I'm implementing Domain Events with Slim Framework + Doctrine ORM + DDD and used parts of your code, except I wrote a publisher to which I registered subscribers that listen to specific domain events. Anyway, you have a typo with postInsert in:

$evm->addEventListener(
    array('postInsert', 'postUpdate', 'postRemove', 'postFlush'),
    new DomainEventListener()
);

It should be postPersist. I was scratching my head about why it wasn't triggered, but flush was.

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