From 379389e05e09c4d46b12536af2cca39273df22df Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 25 Mar 2017 15:47:30 +0800 Subject: [PATCH 1/5] update quickstart --- .travis.yml | 2 +- examples/quickstart.php | 132 +++++++++++++++++++++------------------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9875828..6dc49b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ before_script: script: - if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-clover ./build/logs/clover.xml; else ./vendor/bin/phpunit; fi - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi - - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/docheader check src/ tests/; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/docheader check examples/ src/ tests/; fi after_success: - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi diff --git a/examples/quickstart.php b/examples/quickstart.php index f1c0655..adeb37a 100644 --- a/examples/quickstart.php +++ b/examples/quickstart.php @@ -1,8 +1,8 @@ - * (c) 2015-2016 Sascha-Oliver Prolic + * (c) 2014-2017 prooph software GmbH + * (c) 2015-2017 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -62,9 +62,6 @@ public function name(): string return $this->name; } - /** - * @param $newName - */ public function changeName(string $newName): void { Assertion::notEmpty($newName); @@ -77,23 +74,6 @@ public function changeName(string $newName): void } } - /** - * Each applied event needs a corresponding handler method. - * - * The naming convention is: when[:ShortEventName] - */ - protected function whenUserWasCreated(UserWasCreated $event): void - { - //Simply assign the event payload to the appropriate properties - $this->uuid = Uuid::fromString($event->aggregateId()); - $this->name = $event->username(); - } - - protected function whenUserWasRenamed(UserWasRenamed $event): void - { - $this->name = $event->newName(); - } - /** * Every AR needs a hidden method that returns the identifier of the AR as a string */ @@ -101,15 +81,24 @@ protected function aggregateId(): string { return $this->uuid->toString(); } + + protected function apply(AggregateChanged $event): void + { + switch (get_class($event)) { + case UserWasCreated::class: + //Simply assign the event payload to the appropriate properties + $this->uuid = Uuid::fromString($event->aggregateId()); + $this->name = $event->username(); + break; + case UserWasRenamed::class: + $this->name = $event->newName(); + break; + } + } } /** - * Class UserWasCreated - * * ProophEventSourcing domain events are of the type AggregateChanged - * - * @package My\Model - * @author Alexander Miertsch */ class UserWasCreated extends AggregateChanged { @@ -120,12 +109,7 @@ public function username(): string } /** - * Class UserWasRenamed - * * ProophEventSourcing domain events are of the type AggregateChanged - * - * @package My\Model - * @author Alexander Miertsch */ class UserWasRenamed extends AggregateChanged { @@ -141,18 +125,13 @@ public function oldName(): string } /** - * Interface UserRepository - * * Simple interface for a user repository - * - * @package My\Model - * @author Alexander Miertsch */ interface UserRepository { - public function add(User $user): void; + public function save(User $user): void; - public function get(Uuid $uuid): User; + public function get(Uuid $uuid): ?User; } } @@ -165,12 +144,6 @@ public function get(Uuid $uuid): User; use Prooph\EventStore\EventStore; use Ramsey\Uuid\Uuid; - /** - * Class UserRepositoryImpl extends Prooph\EventSourcing\Aggregate\AggregateRepository and implements My\Model\UserRepository - * - * @package My\Infrastructure - * @author Alexander Miertsch - */ class UserRepositoryImpl extends AggregateRepository implements UserRepository { public function __construct(EventStore $eventStore) @@ -186,15 +159,12 @@ public function __construct(EventStore $eventStore) ); } - /** - * @param User $user - */ - public function add(User $user): void + public function save(User $user): void { - $this->addAggregateRoot($user); + $this->saveAggregateRoot($user); } - public function get(Uuid $uuid): User + public function get(Uuid $uuid): ?User { return $this->getAggregateRoot($uuid->toString()); } @@ -202,14 +172,18 @@ public function get(Uuid $uuid): User } namespace { - //Set up an EventStore with an InMemoryAdapter (Only useful for testing, persistent adapters for ProophEventStore are available) + //Set up an EventStore with an InMemoryAdapter (Only useful for testing, persistent implementations of ProophEventStore are available) use My\Infrastructure\UserRepositoryImpl; use My\Model\User; use Prooph\Common\Event\ActionEvent; use Prooph\Common\Event\ProophActionEventEmitter; use Prooph\EventStore\InMemoryEventStore; + use Prooph\EventStore\TransactionalActionEventEmitterEventStore; - $eventStore = new InMemoryEventStore(new ProophActionEventEmitter()); + $eventStore = new TransactionalActionEventEmitterEventStore( + new InMemoryEventStore(), + new ProophActionEventEmitter() + ); //Now we set up our user repository and inject the EventStore //Normally this should be done in an IoC-Container and the receiver of the repository should require My\Model\UserRepository @@ -220,18 +194,31 @@ public function get(Uuid $uuid): User $user = User::nameNew('John Doe'); - $userRepository->add($user); + //Before we save let's attach a listener to check that the UserWasCreated event is recorded + $eventStore->attach( + TransactionalActionEventEmitterEventStore::EVENT_CREATE, + function (ActionEvent $event): void { + foreach ($event->getParam('stream')->streamEvents() as $streamEvent) { + echo sprintf( + 'Event with name %s was recorded. It occurred on %s UTC /// ', + $streamEvent->messageName(), + $streamEvent->createdAt()->format('Y-m-d H:i:s') + ) . PHP_EOL; + } + }, + -1000 + ); + + $userRepository->save($user); - //Before we commit the transaction let's attach a listener to check that the UserWasCreated event is published after commit - $eventStore->getActionEventEmitter()->attachListener('commit.post', function (ActionEvent $event): void { - foreach ($event->getParam('recordedEvents', new \ArrayIterator()) as $streamEvent) { - echo sprintf( - 'Event with name %s was recorded. It occurred on %s UTC /// ', - $streamEvent->messageName(), - $streamEvent->createdAt()->format('Y-m-d H:i:s') - ); - } - }); + //Let's make sure the transaction is written + $eventStore->attach( + TransactionalActionEventEmitterEventStore::EVENT_COMMIT, + function (ActionEvent $event): void { + echo 'Transaction commited' . PHP_EOL; + }, + -1000 + ); $eventStore->commit(); @@ -249,6 +236,23 @@ public function get(Uuid $uuid): User $loadedUser->changeName('Max Mustermann'); + //Before we save let's attach a listener again on appendTo to check that the UserWasRenamed event is recorded + $eventStore->attach( + TransactionalActionEventEmitterEventStore::EVENT_APPEND_TO, + function (ActionEvent $event): void { + foreach ($event->getParam('streamEvents') as $streamEvent) { + echo sprintf( + 'Event with name %s was recorded. It occurred on %s UTC /// ', + $streamEvent->messageName(), + $streamEvent->createdAt()->format('Y-m-d H:i:s') + ) . PHP_EOL; + } + }, + -1000 + ); + + $userRepository->save($loadedUser); + //... so we only need to commit the transaction and the UserWasRenamed event should be recorded //(check output of the previously attached listener) $eventStore->commit(); From 2a2183661575948e21c1e6c79f8ff86efed56d20 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 25 Mar 2017 16:06:30 +0800 Subject: [PATCH 2/5] update docs --- docs/interop_factories.md | 7 +- docs/migration.md | 4 +- docs/repositories.md | 75 +++---------------- docs/snapshots.md | 52 +------------ .../Aggregate/AggregateRepositoryFactory.php | 1 + 5 files changed, 21 insertions(+), 118 deletions(-) diff --git a/docs/interop_factories.md b/docs/interop_factories.md index 1366dd1..f7e9567 100644 --- a/docs/interop_factories.md +++ b/docs/interop_factories.md @@ -47,6 +47,9 @@ configuration to our application configuration: If you want to speed up loading of aggregates with a snapshot store then you need to make it available as service in the container and use the configuration to let the factory inject the snapshot store in the repository. +Also you need to install [Prooph SnapshotStore](https://github.com/prooph/snapshot-store) and a persistable implementation of it, +like [pdo-snapshot-store](https://github.com/prooph/pdo-snapshot-store/) or [mongodb-snapshot-store](https://github.com/prooph/mongodb-snapshot-store/). + ```php [ 'prooph' => [ @@ -61,13 +64,13 @@ it available as service in the container and use the configuration to let the fa ], ], ], - // zf2 service manager example to configure snapshot store service below + // zf3 service manager example to configure snapshot store service below 'dependencies' => [ 'aliases' => [ 'awesome_snapshot_store' => InMemorySnaphotStore::class, ], 'factories' => [ - InMemorySnaphotStore::class => InvokaleFactory::class, + InMemorySnaphotStore::class => InvokableFactory::class, ], ], ] diff --git a/docs/migration.md b/docs/migration.md index 280ce6e..2cd24da 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -34,8 +34,8 @@ then you can implement the `Prooph\EventSourcing\Aggregate\AggregateTranslator` ## Snapshot Store -The snapshot store is now a simple interface, see `Prooph\EventSourcing\Snapshot\SnapshotStore`. The adapters are all removed -and replaced by different snapshot store implementation, f.e. `Prooph\EventSourcing\Snapshot\InMemorySnapshotStore`. +The snapshot store is now a simple interface, see `Prooph\SnapshotStore\SnapshotStore`. The adapters are all removed +and replaced by different snapshot store implementation, f.e. `Prooph\SnapshotStore\InMemorySnapshotStore`. ## Aggregate Root diff --git a/docs/repositories.md b/docs/repositories.md index 5eeb86d..5d374f5 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -26,16 +26,12 @@ And the repository must also be able to load persisted events from a stream and To provide this functionality the repository makes use of various helper classes explained below. ## AggregateType -Each repository is responsible for one `Prooph\EventStore\Aggregate\AggregateType`. Super types are not supported. -Imagine we have a domain with `Admin extends User` and `Employee extends User`. You'd need to have a `AdminRepository` and -a `EmployeeRepository` in this case. If this is not what you want you can create a custom aggregate translator (see below) -which is capable of reconstituting the correct types based on information derived from persisted domain events. -Then you can have a `UserRepository` set up with your custom aggregate translator and it should work. +Each repository is responsible for one `\Prooph\EventSourcing\Aggregate\AggregateType`. ## AggregateTranslator To achieve 100% decoupling between layers and/or contexts you can make use of translation adapters. -For prooph/event-store such a translation adapter is called an `Prooph\EventStore\Aggregate\AggregateTranslator`. +For prooph/event-store such a translation adapter is called a `Prooph\EventSourcing\Aggregate\AggregateTranslator`. The interface requires you to implement 5 methods: @@ -43,64 +39,16 @@ The interface requires you to implement 5 methods: - extractAggregateVersion - extractPendingStreamEvents - reconstituteAggregateFromHistory -- applyStreamEvents +- replayStreamEvents -To make your life easier prooph/event-store ships with a `Prooph\EventStore\Aggregate\ConfigurableAggregateTranslator` which implements the interface. - -Let's have a look at the constructor - -```php -public function __construct( - string $identifierMethodName = null, - string $versionMethodName = null, - string $popRecordedEventsMethodName = null, - string $replayEventsMethodsName = null, - string $staticReconstituteFromHistoryMethodName = null, - string $eventToMessageCallback = null, - string $messageToEventCallback = null) -{ - //... -} -``` - -We can identify 7 dependencies but all are optional. - -- `$identifierMethodName` - - defaults to `getId` - - used to `extractAggregateId` and must return a string - - you can have a translator per aggregate type, so if you prefer to have methods reflecting domain language you likely want to use methods like `getTrackingId`, `getProductNumber`, etc.. As you can see, this is no problem for the event store. Feel free to model your aggregates exactly the way you need it! -- `$versionMethodName` - - defaults to `getVersion` - - used to `extractVersion` of the aggregate root -- `$popRecordedEventsMethodName` - - defaults to `popRecordedEvents` - - with this method the `ConfigurableAggregateTranslator` requests the latest recorded events from your aggregate - - the aggregate should also clear its internal event cache before returning the events as no additional method is invoked -- `replayStreamEvents` - - defaults to `replay` - - used in case the repository loaded a snapshot and needs to replay newer events -- `$staticReconstituteFromHistoryMethodName` - - defaults to `reconstituteFromHistory` - - like indicated in the parameter name the referenced method must be static (a named constructor) which must return an instance of the aggregate with all events replayed -- `$eventToMessageCallback` - - completely optional - - you can pass any callable - - the callable is invoked for each domain event returned by `$popRecordedEventsMethodName` and can be used to translate a domain event into a `Prooph\Common\Messaging\Message` - - the message interface is required by the event store implementations to function correctly - - you can also decide to let your domain events implement the interface. This would make your life easier when you want to make use of advanced features provided by prooph. But, again. Your domain events don't have to implement the interface. It is your choice! -- `$messageToEventCallback` - - completely optional - - it is the opposite of `$eventToMessageCallback` - - when you pass a callable it is invoked for each message (loaded from the event store) before `$staticReconstituteFromHistoryMethodName` or `$applyEventsMethodsName`is called - - -*Note: When using the translation callbacks shown above you should consider translating domain events into `Prooph\Common\Messaging\DomainEvent` objects. It is a default implementation of the `Message` interface and all event store implementations can handle it out-of-the-box. -If you decide to provide your own implementation of `Prooph\Common\Messaging\Message` you should have a look at `Prooph\Common\Messaging\MessageFactory` and `Prooph\Common\Messaging\MessageConverter` because the event store implementations work with these to translate events into PHP arrays and back.* +To make your life easier prooph/event-sourcing ships with a `\Prooph\EventSourcing\EventStoreIntegration\AggregateTranslator` which implements the interface. ## Snapshot Store A repository can be set up with a snapshot store to speed up loading of aggregates. -Checkout the snapshot docs for more information. + +You need to install [Prooph SnapshotStore](https://github.com/prooph/snapshot-store) and a persistable implementation of it, +like [pdo-snapshot-store](https://github.com/prooph/pdo-snapshot-store/) or [mongodb-snapshot-store](https://github.com/prooph/mongodb-snapshot-store/). ## Event Streams @@ -112,7 +60,6 @@ in a `user_stream`. The repository can also be configured to create a new stream for each new aggregate instance. You need to turn the last constructor parameter `oneStreamPerAggregate` to true to enable the mode. -This can be useful when working for example with MongoDB and you want to persist all events of an aggregate in single document (take care of the document size limit). If the mode is enabled the repository builds a unique stream name for each aggregate by using the `AggregateType` and append the `aggregateId` of the aggregate. The stream name for a new `Acme\User` with id `123` would look like this: `Acme\User-123`. @@ -121,18 +68,18 @@ Check your event store implemtation of choice for details. You can also override for building the stream name. ## Wiring It Together -Best way to see a repository in action is by looking at the `ProophTest\EventStore\Aggregate\AggregateRepositoryTest`. +Best way to see a repository in action is by looking at the `\ProophTest\EventSourcing\Aggregate\AggregateRepositoryTest`. ### Set Up ```php $this->repository = new AggregateRepository( $this->eventStore, - AggregateType::fromAggregateRootClass('ProophTest\EventStore\Mock\User'), - new ConfigurableAggregateTranslator() + AggregateType::fromAggregateRootClass('ProophTest\EventSourcing\Mock\User'), + new AggregateTranslator() ); -$this->eventStore->create(new Stream(new StreamName('event_stream'), [])); +$this->eventStore->create(new Stream(new StreamName('event_stream'), new ArrayIterator())); ``` Notice the injected dependencies! Snapshot store, stream name and stream mode are optional and not injected for all tests. diff --git a/docs/snapshots.md b/docs/snapshots.md index de4051a..2da91e2 100644 --- a/docs/snapshots.md +++ b/docs/snapshots.md @@ -11,7 +11,7 @@ in most cases. If aggregate reconstitution gets slow you can add an additional layer to the system which is capable of providing aggregate snapshots. -Choose one of the `Prooph\*SnapshotStore` to take snapshots. +Choose one of the `Prooph\SnapshotStore\*` to take snapshots. Inject the snapshot store into an aggregate repository and the repository will use the snapshot store to speed up aggregate loading. @@ -19,52 +19,4 @@ Our example application [proophessor-do](https://github.com/prooph/proophessor-d *Note: All SnapshotStores ship with interop factories to ease set up.* -## Creating snapshot projections - -Basically there are two possibilities: - -First, if you are using a single stream or one stream per aggregate type, in this case you need to -create a projection from that stream. - -Second, if you are using one stream per aggregate, you need to create the stream from the category, -when you have no category stream created (see: [StandardProjection](https://github.com/prooph/standard-projections/)). - -All you have to do to create a snapshot projection is to create a small and simple script like this: - -```php -$projection = $eventStore->createReadModelProjection( - 'user_snapshots', - new \Prooph\EventSourcing\Aggregate\SnapshotReadModel( - $aggregateRepository, - $aggregateTranslator, - $snapshotStore - ) -); - -$projection - ->fromStream('user_stream') - ->whenAny(function ($state, Message $event): void { - $this->readModel()->stack('replay', $event); - }) - ->run(); -``` - -or in case you need to create the projection from category: - -```php -$projection = $eventStore->createReadModelProjection( - 'user_snapshots', - new \Prooph\EventSourcing\Aggregate\SnapshotReadModel( - $aggregateRepository, - $aggregateTranslator, - $snapshotStore - ) -); - -$projection - ->fromCategory('user') - ->whenAny(function ($state, Message $event): void { - $this->readModel()->stack('replay', $event); - }) - ->run(); -``` +For more informations about snapshots and how to configure them, see [Prooph Snapshotter](https://github.com/prooph/snapshotter). diff --git a/src/Container/Aggregate/AggregateRepositoryFactory.php b/src/Container/Aggregate/AggregateRepositoryFactory.php index 00258a2..87130e8 100644 --- a/src/Container/Aggregate/AggregateRepositoryFactory.php +++ b/src/Container/Aggregate/AggregateRepositoryFactory.php @@ -7,6 +7,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ + declare(strict_types=1); namespace Prooph\EventSourcing\Container\Aggregate; From 26ce1606d6993961dee900121e10f9a82e453d88 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 25 Mar 2017 16:37:01 +0800 Subject: [PATCH 3/5] update docs --- docs/interop_factories.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/interop_factories.md b/docs/interop_factories.md index f7e9567..ab3aa11 100644 --- a/docs/interop_factories.md +++ b/docs/interop_factories.md @@ -9,7 +9,7 @@ without the need to rely on a specific framework. However, the factories have th ### Requirements -1. Your Inversion of Control container must implement the [interop-container interface](https://github.com/container-interop/container-interop). +1. Your Inversion of Control container must implement the [PSR Container interface](https://github.com/php-fig/container). 2. [interop-config](https://github.com/sandrokeil/interop-config) must be installed 3. The application configuration should be registered with the service id `config` in the container. From fddc563da9b8a51a0fd4ae9b2dc03810d7cda214 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 25 Mar 2017 22:21:50 +0800 Subject: [PATCH 4/5] update docs --- docs/interop_factories.md | 2 +- docs/migration.md | 4 ++-- docs/repositories.md | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/interop_factories.md b/docs/interop_factories.md index ab3aa11..39c96ff 100644 --- a/docs/interop_factories.md +++ b/docs/interop_factories.md @@ -13,7 +13,7 @@ without the need to rely on a specific framework. However, the factories have th 2. [interop-config](https://github.com/sandrokeil/interop-config) must be installed 3. The application configuration should be registered with the service id `config` in the container. -*Note: Don't worry, if your environment doesn't provide the requirements. You can +*Note: Don't worry, if your environment doesn't provide these requirements, you can always bootstrap the components by hand. Just look at the factories for inspiration in this case.* ### AggregateRepositoryFactory diff --git a/docs/migration.md b/docs/migration.md index 2cd24da..4e29e1a 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -34,8 +34,8 @@ then you can implement the `Prooph\EventSourcing\Aggregate\AggregateTranslator` ## Snapshot Store -The snapshot store is now a simple interface, see `Prooph\SnapshotStore\SnapshotStore`. The adapters are all removed -and replaced by different snapshot store implementation, f.e. `Prooph\SnapshotStore\InMemorySnapshotStore`. +The snapshot store is now a simple interface, see `Prooph\SnapshotStore\SnapshotStore`. The adapters have all been removed +and replaced with different snapshot store implementation, f.e. `Prooph\SnapshotStore\InMemorySnapshotStore`. ## Aggregate Root diff --git a/docs/repositories.md b/docs/repositories.md index 5d374f5..129bea3 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -58,7 +58,7 @@ If you wish to use another name, you can pass a custom `Prooph\EventStore\Stream This is especially useful when you want to have an event stream per aggregate type, for example store all user related events in a `user_stream`. -The repository can also be configured to create a new stream for each new aggregate instance. You need to turn the last +The repository can also be configured to create a new stream for each new aggregate instance. You'll need to turn the last constructor parameter `oneStreamPerAggregate` to true to enable the mode. If the mode is enabled the repository builds a unique stream name for each aggregate by using the `AggregateType` and append the `aggregateId` of the aggregate. The stream name for a new `Acme\User` with id `123` would look like this: `Acme\User-123`. @@ -68,6 +68,7 @@ Check your event store implemtation of choice for details. You can also override for building the stream name. ## Wiring It Together + Best way to see a repository in action is by looking at the `\ProophTest\EventSourcing\Aggregate\AggregateRepositoryTest`. ### Set Up @@ -153,6 +154,7 @@ The test case has some more tests including snapshot usage and working with diff Just browse through the test methods for details. ## Loading of thousands aggregates + If you need to load thousands of aggregates for reading only, your memory can be exhausted, because the `AggregateRepository` uses an identity map. So every loaded aggregate is stored there, unless a commit is executed. If you have a read only process, you should consider to clear the identity map at some time. This can be done by calling From 7c9f037370e852fb09ee2029fa0b3b322a40916e Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 25 Mar 2017 22:30:13 +0800 Subject: [PATCH 5/5] update docs --- docs/inheritance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/inheritance.md b/docs/inheritance.md index 52801ba..aa9a7c7 100644 --- a/docs/inheritance.md +++ b/docs/inheritance.md @@ -91,11 +91,11 @@ final class UserAggregateTranslator extends \Prooph\EventSourcing\EventStoreInte final class EventStoreUserCollection extends \Prooph\EventStore\Aggregate\AggregateRepository { - public function add(User $user) + public function save(User $user): void { - $this->addAggregateRoot($user); + $this->saveAggregateRoot($user); } - public function get(UserId $userId) + public function get(UserId $userId): ?User { return $this->getAggregateRoot($userId->toString()); } @@ -111,7 +111,7 @@ final class EventStoreUserCollection extends ```php final class EventStoreUserCollectionFactory { - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container): EventStoreUserCollection { return new EventStoreUserCollection( $container->get(EventStore::class),