From 9a16551b9b6c0de37fa6791e2193c17ac48ca65c Mon Sep 17 00:00:00 2001 From: Codeliner Date: Thu, 16 Jan 2014 21:13:35 +0100 Subject: [PATCH] Add transaction handling --- .../EventStore/Adapter/AdapterException.php | 13 ++- .../Adapter/Doctrine/DoctrineDbalAdapter.php | 18 +++- .../Feature/TransactionFeatureInterface.php | 23 +++++ src/Malocher/EventStore/EventStore.php | 86 ++++++++++++++++++- .../Coverage/EventStore/EventStoreTest.php | 61 +++++++++++++ 5 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 src/Malocher/EventStore/Adapter/Feature/TransactionFeatureInterface.php diff --git a/src/Malocher/EventStore/Adapter/AdapterException.php b/src/Malocher/EventStore/Adapter/AdapterException.php index d891fe4..9078c62 100644 --- a/src/Malocher/EventStore/Adapter/AdapterException.php +++ b/src/Malocher/EventStore/Adapter/AdapterException.php @@ -19,11 +19,22 @@ class AdapterException extends \Exception /** * Throw a configuration exception * - * @param $msg + * @param string $msg * @return AdapterException */ public static function configurationException($msg) { return new self('[Adapter Configuration Error] ' . $msg . "\n"); } + + /** + * Throw an unsupported feature exception + * + * @param string $msg + * @return AdapterException + */ + public static function unsupportedFeatureException($msg) + { + return new self('[Adapter unsupported Feature] ' . $msg . "\n"); + } } diff --git a/src/Malocher/EventStore/Adapter/Doctrine/DoctrineDbalAdapter.php b/src/Malocher/EventStore/Adapter/Doctrine/DoctrineDbalAdapter.php index 62f94cc..c8d1021 100644 --- a/src/Malocher/EventStore/Adapter/Doctrine/DoctrineDbalAdapter.php +++ b/src/Malocher/EventStore/Adapter/Doctrine/DoctrineDbalAdapter.php @@ -12,6 +12,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Schema; use Malocher\EventStore\Adapter\AdapterInterface; +use Malocher\EventStore\Adapter\Feature\TransactionFeatureInterface; use Malocher\EventStore\Adapter\AdapterException; use Malocher\EventStore\EventSourcing\EventInterface; use Malocher\EventStore\EventSourcing\SnapshotEvent; @@ -24,7 +25,7 @@ * @author Manfred Weber * @package Malocher\EventStore\Adapter\Doctrine */ -class DoctrineDbalAdapter implements AdapterInterface +class DoctrineDbalAdapter implements AdapterInterface, TransactionFeatureInterface { /** * Doctrine DBAL connection @@ -262,6 +263,21 @@ public function getCurrentSnapshotVersion($sourceFQCN, $sourceId) return 0; } + + public function beginTransaction() + { + $this->conn->beginTransaction(); + } + + public function commit() + { + $this->conn->commit(); + } + + public function rollback() + { + $this->conn->rollBack(); + } /** * Insert an event diff --git a/src/Malocher/EventStore/Adapter/Feature/TransactionFeatureInterface.php b/src/Malocher/EventStore/Adapter/Feature/TransactionFeatureInterface.php new file mode 100644 index 0000000..458a961 --- /dev/null +++ b/src/Malocher/EventStore/Adapter/Feature/TransactionFeatureInterface.php @@ -0,0 +1,23 @@ + and Alexander Miertsch + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Malocher\EventStore\Adapter\Feature; + +/** + * Interface TransactionFeatureInterface + * + * @author Alexander Miertsch + */ +interface TransactionFeatureInterface +{ + public function beginTransaction(); + + public function commit(); + + public function rollback(); +} diff --git a/src/Malocher/EventStore/EventStore.php b/src/Malocher/EventStore/EventStore.php index 211c7c5..4f7cbdf 100644 --- a/src/Malocher/EventStore/EventStore.php +++ b/src/Malocher/EventStore/EventStore.php @@ -9,6 +9,8 @@ namespace Malocher\EventStore; use Malocher\EventStore\Adapter\AdapterInterface; +use Malocher\EventStore\Adapter\Feature\TransactionFeatureInterface; +use Malocher\EventStore\Adapter\AdapterException; use Malocher\EventStore\Configuration\Configuration; use Malocher\EventStore\EventSourcing\EventSourcedInterface; use Malocher\EventStore\EventSourcing\EventSourcedObjectFactory; @@ -76,6 +78,16 @@ class EventStore * @var EventDispatcherInterface */ protected $eventDispatcher; + + /** + * @var boolean + */ + protected $inTransaction = false; + + /** + * @var array + */ + protected $pendingEvents = array(); /** * Construct @@ -180,7 +192,8 @@ public function save(EventSourcedInterface $eventSourcedObject) ); } - $this->events()->dispatch(PostPersistEvent::NAME, $postPersistEvent); + $this->addPendingEvent($postPersistEvent); + $this->tryDispatchPostPersistEvents(); } $hash = $this->getIdentityHash( @@ -232,6 +245,53 @@ public function clear() $this->identityMap = array(); } + /** + * Begin transaction + * + * @throws AdapterException If adapter does not support transactions + */ + public function beginTransaction() + { + if (!$this->adapter instanceof TransactionFeatureInterface) { + throw AdapterException::unsupportedFeatureException('TransactionFeature'); + } + + $this->inTransaction = true; + $this->adapter->beginTransaction(); + } + + /** + * Commit transaction + * + * @throws AdapterException If adapter does not support transactions + */ + public function commit() + { + if (!$this->adapter instanceof TransactionFeatureInterface) { + throw AdapterException::unsupportedFeatureException('TransactionFeature'); + } + + $this->adapter->commit(); + $this->inTransaction = false; + $this->tryDispatchPostPersistEvents(); + } + + /** + * Rollback transaction + * + * @throws AdapterException If adapter does not support transactions + */ + public function rollback() + { + if (!$this->adapter instanceof TransactionFeatureInterface) { + throw AdapterException::unsupportedFeatureException('TransactionFeature'); + } + + $this->adapter->rollback(); + $this->inTransaction = false; + $this->pendingEvents = array(); + } + /** * Get hash to identify EventSourcedObject in the IdentityMap * @@ -244,4 +304,28 @@ protected function getIdentityHash($sourceFQCN, $sourceId) { return $sourceFQCN . '::' . $sourceId; } + + /** + * @param PostPersistEvent $event + */ + protected function addPendingEvent(PostPersistEvent $event) + { + $this->pendingEvents[] = $event; + } + + + /** + * Events are only dispatched if the event store has no running transaction + */ + protected function tryDispatchPostPersistEvents() + { + if (!$this->inTransaction) { + $events = $this->pendingEvents; + $this->pendingEvents = array(); + + foreach ($events as $event) { + $this->events()->dispatch(PostPersistEvent::NAME, $event); + } + } + } } diff --git a/tests/Malocher/EventStoreTest/Coverage/EventStore/EventStoreTest.php b/tests/Malocher/EventStoreTest/Coverage/EventStore/EventStoreTest.php index c016f54..964ce8c 100644 --- a/tests/Malocher/EventStoreTest/Coverage/EventStore/EventStoreTest.php +++ b/tests/Malocher/EventStoreTest/Coverage/EventStore/EventStoreTest.php @@ -157,4 +157,65 @@ function(PostPersistEvent $e) use (&$persistedEventList) { $this->assertEquals($check, $persistedEventList); } + + public function testBeginTransactionAndCommit() + { + $this->eventStore->beginTransaction(); + + $factory = new EventSourcedObjectFactory(); + $user = $factory->create('Malocher\EventStoreTest\Coverage\Mock\User', '1'); + + $user->changeName('Malocher'); + $user->changeEmail('my.email@getmalocher.org'); + + $persistedEventList = array(); + + $this->eventStore->events()->addListener( + PostPersistEvent::NAME, + function(PostPersistEvent $e) use (&$persistedEventList) { + foreach ($e->getPersistedEvents() as $persistedEvent) { + $persistedEventList[] = get_class($persistedEvent); + } + } + ); + + $this->eventStore->save($user); + + $this->eventStore->commit(); + + $check = array( + 'Malocher\EventStoreTest\Coverage\Mock\Event\UserNameChangedEvent', + 'Malocher\EventStoreTest\Coverage\Mock\Event\UserEmailChangedEvent' + ); + + $this->assertEquals($check, $persistedEventList); + } + + public function testBeginTransactionAndRollback() + { + $this->eventStore->beginTransaction(); + + $factory = new EventSourcedObjectFactory(); + $user = $factory->create('Malocher\EventStoreTest\Coverage\Mock\User', '1'); + + $user->changeName('Malocher'); + $user->changeEmail('my.email@getmalocher.org'); + + $persistedEventList = array(); + + $this->eventStore->events()->addListener( + PostPersistEvent::NAME, + function(PostPersistEvent $e) use (&$persistedEventList) { + foreach ($e->getPersistedEvents() as $persistedEvent) { + $persistedEventList[] = get_class($persistedEvent); + } + } + ); + + $this->eventStore->save($user); + + $this->eventStore->rollback(); + + $this->assertEmpty($persistedEventList); + } }