From 6c0f75b45876b77e4a6681214e2fb08ba8860e49 Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Thu, 21 Apr 2016 11:07:08 +0200 Subject: [PATCH 1/5] Adding cqrs components --- .gitignore | 2 + .php_cs | 34 ++++++++++++ .travis.yml | 22 ++++++++ LICENSE.md | 20 +++++++ composer.json | 43 +++++++++++++++ config/broadway.php | 33 ++++++++++++ src/EventStore/Console/Replay.php | 62 +++++++++++++++++++++ src/EventStore/ServiceProvider.php | 66 +++++++++++++++++++++++ src/EventStore/Services/Replay.php | 86 ++++++++++++++++++++++++++++++ src/Identifier/Identifier.php | 30 +++++++++++ src/Identifier/UuidIdentifier.php | 65 ++++++++++++++++++++++ src/ReadModel/Projector.php | 34 ++++++++++++ 12 files changed, 497 insertions(+) create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 composer.json create mode 100644 config/broadway.php create mode 100644 src/EventStore/Console/Replay.php create mode 100644 src/EventStore/ServiceProvider.php create mode 100644 src/EventStore/Services/Replay.php create mode 100644 src/Identifier/Identifier.php create mode 100644 src/Identifier/UuidIdentifier.php create mode 100644 src/ReadModel/Projector.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f4acd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..e2e6c1f --- /dev/null +++ b/.php_cs @@ -0,0 +1,34 @@ +in(['src', 'tests']); +$header = <<< EOF +This file is part of LaravelCqrsEs + +(c) Madewithlove + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +HeaderCommentFixer::setHeader($header); + +return Config::create() + ->level(FixerInterface::SYMFONY_LEVEL) + ->fixers([ + 'ereg_to_preg', + 'header_comment', + 'multiline_spaces_before_semicolon', + 'ordered_use', + 'php4_constructor', + 'phpdoc_order', + 'short_array_syntax', + 'short_echo_tag', + 'strict', + 'strict_param', + ]) + ->setUsingCache(true) + ->finder($finder); \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b393396 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +# Use Docker environment +sudo: false + +# Setup build matrix +language: php +php: + - 5.6 + - 7 + +env: + matrix: + - PREFER_LOWEST="--prefer-lowest" + - PREFER_LOWEST="" + +# Dependencies +before_install: + - composer self-update + +install: + - travis_retry composer update --no-interaction --prefer-source --dev $PREFER_LOWEST + +script: composer test \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..28b1e86 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Madewithlove + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f682755 --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "madewithlove/laravel-cqrs-es", + "description": "A Laravel package to kick start ES/CQRS projects using Broadway and Tactician.", + "license": "MIT", + "keywords": [ + "laravel", + "broadway", + "event sourcing", + "ES", + "CQRS", + "Domain Driven Design", + "DDD" + ], + "authors": [ + { + "name": "Madewithlove", + "email": "heroes@madewithlove.be" + } + ], + "require": { + "php": ">=5.6.0", + "doctrine/dbal": "^2.5", + "broadway/broadway": "^0.9", + "illuminate/console": "^5.1", + "illuminate/database": "^5.1", + "illuminate/support": "^5.1", + "madewithlove/tactician-laravel": "^1.0" + }, + "require-dev": { + "fabpot/php-cs-fixer": "2.0.*@dev", + "phpunit/phpunit": "^4.7" + }, + "autoload": { + "psr-4": { + "Madewithlove\\LaravelCqrsEs\\": "src/" + } + }, + "suggest": { + "elasticsearch/elasticsearch": "For persisting read models" + }, + "minimum-stability": "dev", + "prefer-stable": true +} \ No newline at end of file diff --git a/config/broadway.php b/config/broadway.php new file mode 100644 index 0000000..0759a12 --- /dev/null +++ b/config/broadway.php @@ -0,0 +1,33 @@ + [ + 'table' => 'event_store', + 'driver' => 'dbal', + ], + + /* + |-------------------------------------------------------------------------- + | Choose which read model implementation to use + | Possible options are: elasticsearch, inmemory + |-------------------------------------------------------------------------- + */ + 'read-model' => 'elasticsearch', + 'read-model-connections' => [ + 'elasticsearch' => [ + 'config' => [ + 'hosts' => ['localhost:9200'], + ], + ], + ], +]; \ No newline at end of file diff --git a/src/EventStore/Console/Replay.php b/src/EventStore/Console/Replay.php new file mode 100644 index 0000000..9972e64 --- /dev/null +++ b/src/EventStore/Console/Replay.php @@ -0,0 +1,62 @@ +replayService = $replayService; + $this->container = $container; + } + + /** + * @return void + */ + public function handle() + { + $params = []; + + if ($id = $this->argument('id')) { + $params['id'] = explode(',', $id); + } + + if ($types = $this->option('types')) { + $params['types'] = explode(',', $types); + } + + $this->replayService->replay($params); + } +} \ No newline at end of file diff --git a/src/EventStore/ServiceProvider.php b/src/EventStore/ServiceProvider.php new file mode 100644 index 0000000..a08fda0 --- /dev/null +++ b/src/EventStore/ServiceProvider.php @@ -0,0 +1,66 @@ +commands([ + Replay::class, + ]); + + $this->app->singleton(Connection::class, function () { + $driver = $this->app['config']->get('database.default'); + $params = $this->app['config']->get("database.connections.{$driver}"); + $params['dbname'] = $params['database']; + $params['user'] = $params['username']; + $params['driver'] = "pdo_$driver"; + + unset($params['database'], $params['username']); + + return DriverManager::getConnection($params); + }); + + $this->app->singleton(DBALEventStore::class, function () { + return new DBALEventStore( + $this->app->make(Connection::class), + $this->app->make(SerializerInterface::class), + $this->app->make(SerializerInterface::class), + $this->app['config']->get('broadway.event-store.table', 'event_store') + ); + }); + + $this->app->singleton(EventStoreInterface::class, DBALEventStore::class); + $this->app->singleton(EventStoreManagementInterface::class, DBALEventStore::class); + } + + /** + * @return array + */ + public function provides() + { + return [ + Connection::class, + EventStoreManagementInterface::class, + EventStoreInterface::class, + DBALEventStore::class, + ]; + } +} \ No newline at end of file diff --git a/src/EventStore/Services/Replay.php b/src/EventStore/Services/Replay.php new file mode 100644 index 0000000..7c68ded --- /dev/null +++ b/src/EventStore/Services/Replay.php @@ -0,0 +1,86 @@ +eventBus = $eventBus; + $this->eventManager = $eventManager; + } + + /** + * @param array $parameters + */ + public function replay($parameters = []) + { + $criteria = Criteria::create(); + + if (isset($parameters['types'])) { + $criteria = $criteria->withEventTypes($parameters['types']); + } + + if (isset($parameters['id'])) { + $criteria = $criteria->withAggregateRootIds($parameters['id']); + } + + $visitor = new CallableEventVisitor(function ($event) { + $this->addEvent($event); + }); + + $this->eventManager->visitEvents($criteria, $visitor); + $this->publishEvents(); + } + + /** + * @return void + */ + protected function publishEvents() + { + $this->eventBus->publish(new DomainEventStream($this->eventBuffer)); + $this->eventBuffer = []; + } + + /** + * @param $event + */ + private function addEvent($event) + { + $this->eventBuffer[] = $event; + + if ($this->eventBufferSize < count($this->eventBuffer)) { + $this->publishEvents(); + } + } +} \ No newline at end of file diff --git a/src/Identifier/Identifier.php b/src/Identifier/Identifier.php new file mode 100644 index 0000000..13cc20a --- /dev/null +++ b/src/Identifier/Identifier.php @@ -0,0 +1,30 @@ +value = $value; + } + + /** + * @return static + */ + public static function generate() + { + return new static(Uuid::uuid4()); + } + + /** + * @param $string + * + * @return static + */ + public static function fromString($string) + { + return new static(Uuid::fromString($string)); + } + + /** + * @param Identifier $identifier + * + * @return bool + */ + public function equals(Identifier $identifier) + { + return $this == $identifier; + } + + /** + * @return string + */ + public function toString() + { + return $this->value->toString(); + } + + /** + * @return string + */ + public function __toString() + { + return $this->value->toString(); + } +} \ No newline at end of file diff --git a/src/ReadModel/Projector.php b/src/ReadModel/Projector.php new file mode 100644 index 0000000..aaf0884 --- /dev/null +++ b/src/ReadModel/Projector.php @@ -0,0 +1,34 @@ +getPayload(); + $method = $this->getHandleMethod($event); + + if (! method_exists($this, $method)) { + return; + } + + $this->$method($event, $domainMessage); + } + + private function getHandleMethod($event) + { + $classParts = explode('\\', get_class($event)); + + return 'project' . end($classParts); + } +} \ No newline at end of file From 487db47929f4ad7b5d87f1794245e01cae09584e Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Fri, 22 Apr 2016 15:26:58 +0200 Subject: [PATCH 2/5] first working version --- composer.json | 5 +- config/broadway.php | 7 +- .../2016_04_22_151617_init_event_store.php | 40 ++++++++++ src/EventStore/Console/Replay.php | 4 +- src/EventStore/EventStoreManager.php | 73 +++++++++++++++++++ src/EventStore/ServiceProvider.php | 35 ++------- src/EventStore/Services/Replay.php | 2 +- src/Identifier/Identifier.php | 2 +- src/Identifier/UuidIdentifier.php | 2 +- src/ReadModel/MethodNameInflector.php | 12 +++ src/ReadModel/ProjectClassNameInflector.php | 17 +++++ src/ReadModel/Projector.php | 27 +++++-- src/ReadModel/ServiceProvider.php | 28 +++++++ src/ServiceProvider.php | 29 ++++++++ 14 files changed, 236 insertions(+), 47 deletions(-) create mode 100644 database/migrations/2016_04_22_151617_init_event_store.php create mode 100644 src/EventStore/EventStoreManager.php create mode 100644 src/ReadModel/MethodNameInflector.php create mode 100644 src/ReadModel/ProjectClassNameInflector.php create mode 100644 src/ReadModel/ServiceProvider.php create mode 100644 src/ServiceProvider.php diff --git a/composer.json b/composer.json index f682755..efc1a61 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "madewithlove/laravel-cqrs-es", - "description": "A Laravel package to kick start ES/CQRS projects using Broadway and Tactician.", + "description": "A Laravel package to kick start CQRS\ES projects using Broadway and Tactician.", "license": "MIT", "keywords": [ "laravel", @@ -24,7 +24,6 @@ "illuminate/console": "^5.1", "illuminate/database": "^5.1", "illuminate/support": "^5.1", - "madewithlove/tactician-laravel": "^1.0" }, "require-dev": { "fabpot/php-cs-fixer": "2.0.*@dev", @@ -36,7 +35,7 @@ } }, "suggest": { - "elasticsearch/elasticsearch": "For persisting read models" + "madewithlove/tactician-laravel": "Laravel wrapper for a configurable command bus", }, "minimum-stability": "dev", "prefer-stable": true diff --git a/config/broadway.php b/config/broadway.php index 0759a12..2152a70 100644 --- a/config/broadway.php +++ b/config/broadway.php @@ -12,8 +12,11 @@ | dbal, inmemory */ 'event-store' => [ - 'table' => 'event_store', 'driver' => 'dbal', + 'dbal' => [ + 'connection' => 'mysql', + 'table' => 'event_store', + ], ], /* @@ -30,4 +33,4 @@ ], ], ], -]; \ No newline at end of file +]; diff --git a/database/migrations/2016_04_22_151617_init_event_store.php b/database/migrations/2016_04_22_151617_init_event_store.php new file mode 100644 index 0000000..89c6800 --- /dev/null +++ b/database/migrations/2016_04_22_151617_init_event_store.php @@ -0,0 +1,40 @@ +table = Config::get('broadway.event-store.dbal.table', 'event_store'); + } + + public function up() + { + Schema::create($this->table, function (Blueprint $table) { + $table->increments('id'); + + $table->string('uuid', 36); + $table->integer('playhead')->unsigned(); + $table->text('metadata'); + $table->text('payload'); + $table->string('recorded_on', 32); + $table->text('type'); + + $table->unique(['uuid', 'playhead']); + }); + } + + public function down() + { + Schema::drop($this->table); + } +} \ No newline at end of file diff --git a/src/EventStore/Console/Replay.php b/src/EventStore/Console/Replay.php index 9972e64..f8f8247 100644 --- a/src/EventStore/Console/Replay.php +++ b/src/EventStore/Console/Replay.php @@ -1,10 +1,10 @@ app = $app; + } + + /** + * Get the default driver name. + * + * @return string + */ + public function getDefaultDriver() + { + return $this->app['config']->get('broadway.event-store.driver'); + } + + /** + * @return DBALEventStore + */ + protected function createDbalDriver() + { + $config = $this->app['config']->get('broadway.event-store.dbal'); + $driver = $config['connection']; + + $params = $this->app['config']->get("database.connections.{$driver}"); + $params['dbname'] = $params['database']; + $params['user'] = $params['username']; + $params['driver'] = "pdo_$driver"; + + unset($params['database'], $params['username']); + + $connection = DriverManager::getConnection($params); + + + return new DBALEventStore( + $connection, + $this->app->make(SerializerInterface::class), + $this->app->make(SerializerInterface::class), + array_get($config, 'table', 'event_store') + ); + } + + /** + * @return InMemoryEventStore + */ + protected function createInMemory() + { + return new InMemoryEventStore(); + } +} \ No newline at end of file diff --git a/src/EventStore/ServiceProvider.php b/src/EventStore/ServiceProvider.php index a08fda0..bcc1de3 100644 --- a/src/EventStore/ServiceProvider.php +++ b/src/EventStore/ServiceProvider.php @@ -1,14 +1,10 @@ app->singleton(Connection::class, function () { - $driver = $this->app['config']->get('database.default'); - $params = $this->app['config']->get("database.connections.{$driver}"); - $params['dbname'] = $params['database']; - $params['user'] = $params['username']; - $params['driver'] = "pdo_$driver"; - - unset($params['database'], $params['username']); - - return DriverManager::getConnection($params); - }); - - $this->app->singleton(DBALEventStore::class, function () { - return new DBALEventStore( - $this->app->make(Connection::class), - $this->app->make(SerializerInterface::class), - $this->app->make(SerializerInterface::class), - $this->app['config']->get('broadway.event-store.table', 'event_store') - ); + $this->app->singleton('event_store.driver', function () { + return (new EventStoreManager($this->app))->driver(); }); - $this->app->singleton(EventStoreInterface::class, DBALEventStore::class); - $this->app->singleton(EventStoreManagementInterface::class, DBALEventStore::class); + $this->app->alias(EventStoreInterface::class, 'event_store.driver'); + $this->app->alias(EventStoreManagementInterface::class, 'event_store.driver'); } /** @@ -57,10 +36,8 @@ public function register() public function provides() { return [ - Connection::class, EventStoreManagementInterface::class, EventStoreInterface::class, - DBALEventStore::class, ]; } } \ No newline at end of file diff --git a/src/EventStore/Services/Replay.php b/src/EventStore/Services/Replay.php index 7c68ded..8ad4ba1 100644 --- a/src/EventStore/Services/Replay.php +++ b/src/EventStore/Services/Replay.php @@ -1,6 +1,6 @@ methodNameInflector = $methodNameInflector; + } + + /** * {@inheritDoc} */ public function handle(DomainMessage $domainMessage) { $event = $domainMessage->getPayload(); - $method = $this->getHandleMethod($event); + $method = $this->methodNameInflector->inflect($event); if (! method_exists($this, $method)) { return; @@ -24,11 +42,4 @@ public function handle(DomainMessage $domainMessage) $this->$method($event, $domainMessage); } - - private function getHandleMethod($event) - { - $classParts = explode('\\', get_class($event)); - - return 'project' . end($classParts); - } } \ No newline at end of file diff --git a/src/ReadModel/ServiceProvider.php b/src/ReadModel/ServiceProvider.php new file mode 100644 index 0000000..2f8590a --- /dev/null +++ b/src/ReadModel/ServiceProvider.php @@ -0,0 +1,28 @@ +app->singleton(MethodNameInflector::class, ProjectClassNameInflector::class); + } + + public function provides() + { + return [ + MethodNameInflector::class + ]; + } +} \ No newline at end of file diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php new file mode 100644 index 0000000..07835fc --- /dev/null +++ b/src/ServiceProvider.php @@ -0,0 +1,29 @@ +app->register(EventStore\ServiceProvider::class); + $this->app->register(ReadModel\ServiceProvider::class); + } + + /** + * Perform post-registration booting of services. + * + * @return void + */ + public function boot() + { + $this->publishes([ + __DIR__.'/../config/broadway.php' => config_path('broadway.php') + ], 'config'); + + $this->publishes([ + __DIR__.'/../database/migrations/' => database_path('migrations') + ], 'migrations'); + } + +} \ No newline at end of file From f197e3ed303720b50818946b0b5ba19f600b15c4 Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Fri, 22 Apr 2016 15:34:04 +0200 Subject: [PATCH 3/5] improve readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 1c2f210..2c52cdf 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ # laravel-cqrs-es + +A package to kickstart your CQRS/ES development in Laravel using the Broadway event store. + +## Installation + +``` +$ composer require madewithlove/laravel-cqrs-es +``` + +## Configuration + +Add the service provider to config/app.php: + +``` +Madewithlove\LaravelCqrsEs\ServiceProvider::class +``` + + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. \ No newline at end of file From 8badd49f6a5c7e7c90cba2be21c10ea9773a6e70 Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Fri, 22 Apr 2016 15:37:36 +0200 Subject: [PATCH 4/5] remove obsolete config vars --- config/broadway.php | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/config/broadway.php b/config/broadway.php index 2152a70..62d192e 100644 --- a/config/broadway.php +++ b/config/broadway.php @@ -5,11 +5,10 @@ |-------------------------------------------------------------------------- | Event Store configuration |-------------------------------------------------------------------------- - | Set the table name where the event will be stored. Make sure - | this corresponds to what you specified in the migration - | - | You should also choose a driver, possible options are + | You can choose a driver, possible options are: + | | dbal, inmemory + | */ 'event-store' => [ 'driver' => 'dbal', @@ -18,19 +17,4 @@ 'table' => 'event_store', ], ], - - /* - |-------------------------------------------------------------------------- - | Choose which read model implementation to use - | Possible options are: elasticsearch, inmemory - |-------------------------------------------------------------------------- - */ - 'read-model' => 'elasticsearch', - 'read-model-connections' => [ - 'elasticsearch' => [ - 'config' => [ - 'hosts' => ['localhost:9200'], - ], - ], - ], ]; From e3f009e027e5cc00bf95bbb2c99d8638df2ae1ae Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Fri, 22 Apr 2016 15:44:13 +0200 Subject: [PATCH 5/5] cleanup --- src/EventStore/EventStoreManager.php | 17 ----------------- src/ReadModel/Projector.php | 9 +-------- src/ServiceProvider.php | 1 - 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/EventStore/EventStoreManager.php b/src/EventStore/EventStoreManager.php index b3f488f..cbd10a5 100644 --- a/src/EventStore/EventStoreManager.php +++ b/src/EventStore/EventStoreManager.php @@ -2,31 +2,14 @@ namespace Madewithlove\LaravelCqrsEs\EventStore; - use Broadway\EventStore\DBALEventStore; use Broadway\EventStore\InMemoryEventStore; use Broadway\Serializer\SerializerInterface; use Doctrine\DBAL\DriverManager; -use Illuminate\Contracts\Container\Container; use Illuminate\Support\Manager; class EventStoreManager extends Manager { - - /** - * @var Container - */ - protected $app; - - /** - * EventStoreManager constructor. - * @param Container $app - */ - public function __construct(Container $app) - { - $this->app = $app; - } - /** * Get the default driver name. * diff --git a/src/ReadModel/Projector.php b/src/ReadModel/Projector.php index 36e1502..0b16c35 100644 --- a/src/ReadModel/Projector.php +++ b/src/ReadModel/Projector.php @@ -5,12 +5,6 @@ use Broadway\Domain\DomainMessage; use Broadway\ReadModel\ProjectorInterface; -/** - * Created by PhpStorm. - * User: jonas - * Date: 20/04/16 - * Time: 16:24 - */ class Projector implements ProjectorInterface { /** @@ -26,8 +20,7 @@ public function __construct(MethodNameInflector $methodNameInflector) { $this->methodNameInflector = $methodNameInflector; } - - + /** * {@inheritDoc} */ diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 07835fc..7ad660c 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -25,5 +25,4 @@ public function boot() __DIR__.'/../database/migrations/' => database_path('migrations') ], 'migrations'); } - } \ No newline at end of file