-
-
Save beberlei/53cd6580d87b1f5cd9ca to your computer and use it in GitHub Desktop.
Doctrine + DomainEvents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
vendor/ | |
db.sqlite | |
composer.lock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"require": { | |
"doctrine/orm": "@stable" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
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
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:
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 agetEvents()
andclearEvents()
function so the events got cleared too late and that's the reasons for the infinity loop,