From 307d20edd81eff1de434f805555f1cef06dc2255 Mon Sep 17 00:00:00 2001 From: Alexander Miertsch Date: Wed, 21 Oct 2015 21:09:15 +0200 Subject: [PATCH] Add AbstractAggregateRootFactory --- README.md | 1 + docs/event_store.md | 46 +---- docs/interop_factories.md | 97 ++++++++++ docs/repositories.md | 5 + .../AbstractAggregateRepositoryFactory.php | 89 +++++++++ ...AbstractAggregateRepositoryFactoryTest.php | 173 ++++++++++++++++++ tests/Mock/FaultyRepositoryMock.php | 15 ++ tests/Mock/RepositoryMock.php | 60 ++++++ tests/Mock/RepositoryMockFactory.php | 26 +++ 9 files changed, 468 insertions(+), 44 deletions(-) create mode 100644 docs/interop_factories.md create mode 100644 src/Container/Aggregate/AbstractAggregateRepositoryFactory.php create mode 100644 tests/Container/Aggregate/AbstractAggregateRepositoryFactoryTest.php create mode 100644 tests/Mock/FaultyRepositoryMock.php create mode 100644 tests/Mock/RepositoryMock.php create mode 100644 tests/Mock/RepositoryMockFactory.php diff --git a/README.md b/README.md index 7ffbd62b..23b397e9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ For a short overview please see the annotated [quickstart.php](examples/quicksta - [Apply Events Late](docs/apply_events_late.md) - [Replay History](docs/replay_history.md) - [Upcasting](docs/upcasting.md) +- [Framework Integration](docs/interop_factories.md) # Support diff --git a/docs/event_store.md b/docs/event_store.md index 7ac934ff..7f63a4cb 100644 --- a/docs/event_store.md +++ b/docs/event_store.md @@ -61,48 +61,6 @@ The event-driven system opens the door for customizations. Here are some ideas w - Implement your own Unit of Work and synchronizes it with the `transaction`, `commit.pre/post` and `rollback` events - ... -## Container-Driven Creation +## Factory-Driven Creation -If you are familiar with the factory pattern and you use an implementation of [interop-container](https://github.com/container-interop/container-interop) -in your project you may want to have a look at the factories shipped with prooph/event-store. -You can find them in the [Container](../src/Container) folder. - -### Requirements - -1. Your Inversion of Control container must implement the [interop-container interface](https://github.com/container-interop/container-interop). -2. 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 -always bootstrap the event store by hand. Just look at the factories for inspiration in this case.* - -If the requirements are met you just need to add a new section in your application config ... - -```php -[ - 'prooph' => [ - 'event_store' => [ - 'adapter' => [ - 'type' => 'adapter_service_id', //The factory will use this id to get the adapter from the container - //The options key is reserved for adapter factories - 'options' => [] - ], - 'event_emitter' => 'emitter_service_id' //The factory will use this id to get the event emitter from the container - 'plugins' => [ - //And again the factory will use each service id to get the plugin from the container - //Plugin::setUp($eventStore) is then invoked by the factory so your plugins get attached automatically - //Awesome, isn't it? - 'plugin_1_service_id', - 'plugin_2_service_id', - //... - ] - ] - ], - //... other application config here -] -``` - -... and register the factory in your IoC container. We recommend using the service id `Prooph\EventStore\EventStore (EventStore::class)` for the event store -because other factories like the [stream factories](../src/Container/Stream) try to locate the event store -by using this service id. - -*Note: The available event store adapters also ship with factories. Please refer to the adapter packages for details.* \ No newline at end of file +See [Interop Factories](interop_factories.md) diff --git a/docs/interop_factories.md b/docs/interop_factories.md new file mode 100644 index 00000000..302ff6b6 --- /dev/null +++ b/docs/interop_factories.md @@ -0,0 +1,97 @@ +# Interop Factories + +Instead of providing a module, a bundle, a bridge or similar framework integration prooph/event-store ships with `interop factories`. + +## Factory-Driven Creation + +The concept behind these [factories](../src/Container) is simple but powerful. It allows us to provide you with bootstrapping logic for the event store and related components +without the need to rely on a specific framework. However, the factories have three requirements. + +### Requirements + +1. Your Inversion of Control container must implement the [interop-container interface](https://github.com/container-interop/container-interop). +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 +always bootstrap the components by hand. Just look at the factories for inspiration in this case.* + +### Event Store Factory + +If the requirements are met you just need to add a new section in your application config ... + +```php +[ + 'prooph' => [ + 'event_store' => [ + 'adapter' => [ + 'type' => 'adapter_service_id', //The factory will use this id to get the adapter from the container + //The options key is reserved for adapter factories + 'options' => [] + ], + 'event_emitter' => 'emitter_service_id' //The factory will use this id to get the event emitter from the container + 'plugins' => [ + //And again the factory will use each service id to get the plugin from the container + //Plugin::setUp($eventStore) is then invoked by the factory so your plugins get attached automatically + //Awesome, isn't it? + 'plugin_1_service_id', + 'plugin_2_service_id', + //... + ] + ] + ], + //... other application config here +] +``` + +... and register the [EventStoreFactory](../src/Container/EventStoreFactory.php) in your IoC container. We recommend using the service id `Prooph\EventStore\EventStore (EventStore::class)` for the event store +because other factories like the [stream factories](../src/Container/Stream) try to locate the event store +by using this service id. + +*Note: The available event store adapters also ship with factories. Please refer to the adapter packages for details.* + +### AbstractAggregateRepositoryFactory + +To ease set up of repositories for your aggregate roots prooph/event-store also ships with a [AbstractAggregateRepositoryFactory](../src/Aggregate/AbstractAggregateRepositoryFactory.php). +It is an abstract class implementing the `container-interop RequiresContainerId` interface. The `containerId` method +itself is not implemented in the abstract class. You have to extend it and provide the container id because each +aggregate repository needs a slightly different configuration and therefor needs its own config key. + +*Note: You can have a look at the [RepositoryMock](../tests/Mock/RepositoryMock.php). It sounds more complex than it is.* + +Let's say we have a repository factory for a User aggregate root. We use `user_repository` as container id and add this +configuration to our application configuration: + +```php +[ + 'prooph' => [ + 'event_store' => [ + 'user_repository' => [ //<-- here the container id is referenced + 'repository_class' => MyUserRepository::class, //<-- FQCN of the repository responsible for the aggregate root + 'aggregate_type' => MyUser::class, //<-- The aggregate root FQCN the repository is responsible for + 'aggregate_translator' => 'user_translator', //<-- The aggregate translator must be available as service in the container + ] + ] + ] +] +``` + +If you also want to configure a custom stream strategy or want to make use of a snapshot adapter then you need to make +them available as services in the container and use the configuration to let the factory inject them in the repository. + +```php +[ + 'prooph' => [ + 'event_store' => [ + 'user_repository' => [ + 'repository_class' => MyUserRepository::class, + 'aggregate_type' => MyUser::class, + 'aggregate_translator' => 'user_translator', + 'stream_strategy' => 'user_stream' // <-- Custom stream strategy service id + 'snapshot_store' => 'awesome_snapshot_store' // <-- SnapshotStore service id + ] + ] + ] +] +``` + diff --git a/docs/repositories.md b/docs/repositories.md index cdf92320..ac159614 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -206,3 +206,8 @@ a repository method. Only `$this->eventStore->commit();` is called. But as you c is changed and the appropriate domain event was added to the `event_stream`. This happens becasue the repository manages an identity map internally. Each aggregate root loaded via `AggregateRepository::getAggregateRoot` is added to the identity map and new events recorded by such an agggregate root are added automatically to the event stream on `EventStore::commit`. + +## Factory-Driven Creation + +See [Interop Factories](interop_factories.md) + diff --git a/src/Container/Aggregate/AbstractAggregateRepositoryFactory.php b/src/Container/Aggregate/AbstractAggregateRepositoryFactory.php new file mode 100644 index 00000000..63f1a016 --- /dev/null +++ b/src/Container/Aggregate/AbstractAggregateRepositoryFactory.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 10/21/15 - 6:10 PM + */ +namespace Prooph\EventStore\Container\Aggregate; + +use Interop\Config\ConfigurationTrait; +use Interop\Config\RequiresContainerId; +use Interop\Config\RequiresMandatoryOptions; +use Interop\Container\ContainerInterface; +use Prooph\EventStore\Aggregate\AggregateRepository; +use Prooph\EventStore\Aggregate\AggregateType; +use Prooph\EventStore\EventStore; +use Prooph\EventStore\Exception\ConfigurationException; + +/** + * Class AbstractAggregateRepositoryFactory + * + * @package Prooph\EventStore\Container + */ +abstract class AbstractAggregateRepositoryFactory implements RequiresContainerId, RequiresMandatoryOptions +{ + use ConfigurationTrait; + + /** + * @param ContainerInterface $container + * @throws ConfigurationException + * @return AggregateRepository + */ + public function __invoke(ContainerInterface $container) + { + $config = $container->get('config'); + $config = $this->options($config); + + $repositoryClass = $config['repository_class']; + + if (! class_exists($repositoryClass)) { + throw ConfigurationException::configurationError(sprintf('Repository class %s cannot be found', $repositoryClass)); + } + + if (! is_subclass_of($repositoryClass, AggregateRepository::class)) { + throw ConfigurationException::configurationError(sprintf('Repository class %s must be a sub class of %s', $repositoryClass, AggregateRepository::class)); + } + + $eventStore = $container->get(EventStore::class); + $aggregateType = AggregateType::fromAggregateRootClass($config['aggregate_type']); + $aggregateTranslator = $container->get($config['aggregate_translator']); + + $streamStrategy = isset($config['stream_strategy'])? $container->get($config['stream_strategy']) : null; + + $snapshotStore = isset($config['snapshot_store'])? $container->get($config['snapshot_store']) : null; + + return new $repositoryClass($eventStore, $aggregateType, $aggregateTranslator, $streamStrategy, $snapshotStore); + } + + /** + * @inheritdoc + */ + public function vendorName() + { + return 'prooph'; + } + + /** + * @inheritdoc + */ + public function packageName() + { + return 'event_store'; + } + + /** + * @inheritdoc + */ + public function mandatoryOptions() + { + return [ + 'repository_class', + 'aggregate_type', + 'aggregate_translator', + ]; + } +} diff --git a/tests/Container/Aggregate/AbstractAggregateRepositoryFactoryTest.php b/tests/Container/Aggregate/AbstractAggregateRepositoryFactoryTest.php new file mode 100644 index 00000000..8ab5d9b5 --- /dev/null +++ b/tests/Container/Aggregate/AbstractAggregateRepositoryFactoryTest.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 10/21/15 - 8:07 PM + */ +namespace ProophTest\EventStore\Container\Aggregate; + +use Interop\Container\ContainerInterface; +use Prooph\EventStore\Aggregate\AggregateTranslator; +use Prooph\EventStore\Aggregate\AggregateType; +use Prooph\EventStore\EventStore; +use Prooph\EventStore\Snapshot\SnapshotStore; +use Prooph\EventStore\Stream\SingleStreamStrategy; +use Prooph\EventStore\Stream\StreamStrategy; +use ProophTest\EventStore\Mock\FaultyRepositoryMock; +use ProophTest\EventStore\Mock\RepositoryMock; +use ProophTest\EventStore\Mock\RepositoryMockFactory; +use ProophTest\EventStore\Mock\User; +use ProophTest\EventStore\TestCase; + +class AbstractAggregateRepositoryFactoryTest extends TestCase +{ + /** + * @test + */ + public function it_creates_repository_with_default_stream_strategy_and_no_snapshot_adapter() + { + $factory = new RepositoryMockFactory(); + + $container = $this->prophesize(ContainerInterface::class); + + $container->has('config')->willReturn(true); + $container->get('config')->willReturn([ + 'prooph' => [ + 'event_store' => [ + 'repository_mock' => [ + 'repository_class' => RepositoryMock::class, + 'aggregate_type' => User::class, + 'aggregate_translator' => 'user_translator', + ] + ] + ] + ]); + + $container->get(EventStore::class)->willReturn($this->eventStore); + + $userTranslator = $this->prophesize(AggregateTranslator::class); + + $container->get('user_translator')->willReturn($userTranslator->reveal()); + + /** @var $repo RepositoryMock */ + $repo = $factory($container->reveal()); + + $this->assertInstanceOf(RepositoryMock::class, $repo); + + $this->assertSame($this->eventStore, $repo->accessEventStore()); + $this->assertInstanceOf(AggregateType::class, $repo->accessAggregateType()); + $this->assertEquals(User::class, $repo->accessAggregateType()->toString()); + $this->assertSame($userTranslator->reveal(), $repo->accessAggregateTranslator()); + $this->assertInstanceOf(SingleStreamStrategy::class, $repo->accessStreamStrategy()); + $this->assertNull($repo->accessSnapshotStore()); + } + + /** + * @test + * @expectedException \Prooph\EventStore\Exception\ConfigurationException + * @expectedExceptionMessage Repository class UnknownClass cannot be found + */ + public function it_throws_exception_if_repository_class_does_not_exist() + { + $factory = new RepositoryMockFactory(); + + $container = $this->prophesize(ContainerInterface::class); + + $container->has('config')->willReturn(true); + $container->get('config')->willReturn([ + 'prooph' => [ + 'event_store' => [ + 'repository_mock' => [ + 'repository_class' => 'UnknownClass', + 'aggregate_type' => User::class, + 'aggregate_translator' => 'user_translator', + ] + ] + ] + ]); + + $factory($container->reveal()); + } + + /** + * @test + * @expectedException \Prooph\EventStore\Exception\ConfigurationException + * @expectedExceptionMessage Repository class ProophTest\EventStore\Mock\FaultyRepositoryMock must be a sub class of Prooph\EventStore\Aggregate\AggregateRepository + */ + public function it_throws_exception_if_repository_class_is_not_a_subclass_of_aggregate_repository() + { + $factory = new RepositoryMockFactory(); + + $container = $this->prophesize(ContainerInterface::class); + + $container->has('config')->willReturn(true); + $container->get('config')->willReturn([ + 'prooph' => [ + 'event_store' => [ + 'repository_mock' => [ + 'repository_class' => FaultyRepositoryMock::class, + 'aggregate_type' => User::class, + 'aggregate_translator' => 'user_translator', + ] + ] + ] + ]); + + $factory($container->reveal()); + } + + /** + * @test + */ + public function it_creates_repository_with_configured_stream_strategy_and_snapshot_store_if_given() + { + $factory = new RepositoryMockFactory(); + + $container = $this->prophesize(ContainerInterface::class); + + $container->has('config')->willReturn(true); + $container->get('config')->willReturn([ + 'prooph' => [ + 'event_store' => [ + 'repository_mock' => [ + 'repository_class' => RepositoryMock::class, + 'aggregate_type' => User::class, + 'aggregate_translator' => 'user_translator', + 'stream_strategy' => 'custom_stream_strategy', + 'snapshot_store' => 'ultra_fast_snapshot_store' + ] + ] + ] + ]); + + $container->get(EventStore::class)->willReturn($this->eventStore); + + $userTranslator = $this->prophesize(AggregateTranslator::class); + + $container->get('user_translator')->willReturn($userTranslator->reveal()); + + $streamStrategy = $this->prophesize(StreamStrategy::class); + $container->has('custom_stream_strategy')->willReturn(true); + $container->get('custom_stream_strategy')->willReturn($streamStrategy->reveal()); + + $snapshotStore = $this->prophesize(SnapshotStore::class); + $container->has('ultra_fast_snapshot_store')->willReturn(true); + $container->get('ultra_fast_snapshot_store')->willReturn($snapshotStore->reveal()); + + /** @var $repo RepositoryMock */ + $repo = $factory($container->reveal()); + + $this->assertInstanceOf(RepositoryMock::class, $repo); + + $this->assertSame($this->eventStore, $repo->accessEventStore()); + $this->assertInstanceOf(AggregateType::class, $repo->accessAggregateType()); + $this->assertEquals(User::class, $repo->accessAggregateType()->toString()); + $this->assertSame($userTranslator->reveal(), $repo->accessAggregateTranslator()); + $this->assertSame($streamStrategy->reveal(), $repo->accessStreamStrategy()); + $this->assertSame($snapshotStore->reveal(), $repo->accessSnapshotStore()); + } +} diff --git a/tests/Mock/FaultyRepositoryMock.php b/tests/Mock/FaultyRepositoryMock.php new file mode 100644 index 00000000..e24c0b10 --- /dev/null +++ b/tests/Mock/FaultyRepositoryMock.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 10/21/15 - 8:14 PM + */ +namespace ProophTest\EventStore\Mock; + +final class FaultyRepositoryMock +{ +} diff --git a/tests/Mock/RepositoryMock.php b/tests/Mock/RepositoryMock.php new file mode 100644 index 00000000..9a08ef1f --- /dev/null +++ b/tests/Mock/RepositoryMock.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 10/21/15 - 8:10 PM + */ +namespace ProophTest\EventStore\Mock; + +use Prooph\EventStore\Aggregate\AggregateRepository; +use Prooph\EventStore\Aggregate\AggregateTranslator; +use Prooph\EventStore\Aggregate\AggregateType; +use Prooph\EventStore\EventStore; +use Prooph\EventStore\Snapshot\SnapshotStore; +use Prooph\EventStore\Stream\StreamStrategy; + +final class RepositoryMock extends AggregateRepository +{ + /** + * @return EventStore + */ + public function accessEventStore() + { + return $this->eventStore; + } + /** + * @return AggregateType + */ + public function accessAggregateType() + { + return $this->aggregateType; + } + + /** + * @return AggregateTranslator + */ + public function accessAggregateTranslator() + { + return $this->aggregateTranslator; + } + + /** + * @return StreamStrategy + */ + public function accessStreamStrategy() + { + return $this->streamStrategy; + } + + /** + * @return SnapshotStore + */ + public function accessSnapshotStore() + { + return $this->snapshotStore; + } +} diff --git a/tests/Mock/RepositoryMockFactory.php b/tests/Mock/RepositoryMockFactory.php new file mode 100644 index 00000000..2f7e0120 --- /dev/null +++ b/tests/Mock/RepositoryMockFactory.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 10/21/15 - 8:18 PM + */ +namespace ProophTest\EventStore\Mock; + +use Prooph\EventStore\Container\Aggregate\AbstractAggregateRepositoryFactory; + +final class RepositoryMockFactory extends AbstractAggregateRepositoryFactory +{ + /** + * Returns the container identifier + * + * @return string + */ + public function containerId() + { + return 'repository_mock'; + } +}