├── .coveralls.yml ├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build ├── .gitignore └── logs │ └── .gitignore ├── composer.json ├── config ├── dependencies.php └── prooph.php ├── examples ├── command_bus.php ├── event_bus.php └── query_bus.php ├── ide_stubs.php ├── phpunit.xml.dist ├── src ├── Container │ ├── InvokableFactory.php │ └── LaravelContainer.php ├── Exception │ └── QueryResultFailed.php ├── Facades │ ├── CommandBus.php │ ├── EventBus.php │ └── QueryBus.php ├── Migration │ └── Schema │ │ ├── EventStoreSchema.php │ │ └── SnapshotSchema.php ├── ProophServiceProvider.php └── Queue │ ├── HandleMessageJob.php │ └── LaravelQueueMessageProducer.php └── tests ├── Exception └── QueryResultFailedTest.php ├── Facade ├── CommandBusTest.php ├── EventBusTest.php └── QueryBusTest.php └── Queue └── LaravelQueueMessageProducerTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | service_name: travis-ci 3 | coverage_clover: build/logs/clover.xml 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | .php_cs.cache -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | getFinder()->in(__DIR__ . '/src')->in(__DIR__ . '/tests'); 4 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; 5 | $config->setCacheFile($cacheDir . '/.php_cs.cache'); 6 | return $config; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | - vendor 9 | 10 | matrix: 11 | fast_finish: true 12 | include: 13 | - php: 7.1 14 | env: 15 | - EXECUTE_CS_CHECK=true 16 | - EXECUTE_TEST_COVERALLS=true 17 | - php: hhvm 18 | allow_failures: 19 | - php: hhvm 20 | 21 | before_install: 22 | - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi 23 | - composer self-update 24 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer require --dev --no-update satooshi/php-coveralls:dev-master ; fi 25 | 26 | before_script: 27 | - mkdir -p "$HOME/.php-cs-fixer" 28 | 29 | install: 30 | - travis_retry composer install --no-interaction 31 | - composer info -i 32 | 33 | script: 34 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer test-coverage ; fi 35 | - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then composer test ; fi 36 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi 37 | 38 | after_success: 39 | - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer coveralls ; fi 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # prooph Laravel package CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 0.2.0 TBA 6 | 7 | ### Added 8 | 9 | * [#6](https://github.com/prooph/laravel-package/pull/6): use version interop-config 1.0 10 | 11 | ### Deprecated 12 | 13 | * Nothing 14 | 15 | ### Removed 16 | 17 | * Nothing 18 | 19 | ### Fixed 20 | 21 | * Nothing 22 | 23 | ## 0.1.0 (2016-02-05) 24 | 25 | ### Added 26 | 27 | * Service provider and default configuration 28 | * Database migration schema 29 | 30 | ### Deprecated 31 | 32 | * Nothing 33 | 34 | ### Removed 35 | 36 | * Nothing 37 | 38 | ### Fixed 39 | 40 | * Nothing 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, prooph software GmbH 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | - Neither the name of prooph software GmbH nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel package for prooph components 2 | 3 | ## Overview 4 | This is a Laravel package for *prooph components* to get started out of the box with message bus, CQRS, event sourcing 5 | and snapshots. It uses the `prooph/pdo-event-store` event store however there are more adapters available. 6 | 7 | It provides all [service definitions and a default configuration](config "Laravel Package Resources"). This is more like 8 | a Quick-Start package. If you want to use the prooph components in production, we recommend to configure the 9 | *prooph components* for your requirements. See the [documentation](http://getprooph.org/ "prooph components documentation") 10 | for more details of the *prooph components*. 11 | 12 | For rapid prototyping we recommend to use our 13 | [prooph-cli](https://github.com/proophsoftware/prooph-cli "prooph command line interface") tool. 14 | 15 | ### Available services 16 | * `Prooph\ServiceBus\CommandBus`: Dispatches commands 17 | * `Prooph\ServiceBus\EventBus`: Dispatches events 18 | * `Prooph\ServiceBus\QueryBus`: Allows for querying over a message bus. 19 | * `Prooph\EventStoreBusBridge\TransactionManager`: Transaction manager for service bus and event store 20 | * `Prooph\EventStoreBusBridge\EventPublisher`: Publishes events on the event bus 21 | 22 | ### Available event stores 23 | * `Prooph\EventStore\Pdo\MariaDbEventStore`: MariaDB event store adapter 24 | * `Prooph\EventStore\Pdo\MySqlEventStore`: MySQL event store adapter 25 | * `Prooph\EventStore\Pdo\PostgresEventStore`: PostgreSQL event store adapter 26 | 27 | ### Available facades 28 | * `CommandBus`: Usage: https://github.com/prooph/laravel-package/blob/master/examples/command_bus.php 29 | * `EventBus`: Usage: https://github.com/prooph/laravel-package/blob/master/examples/event_bus.php 30 | * `QueryBus`: Usage: https://github.com/prooph/laravel-package/blob/master/examples/query_bus.php 31 | 32 | 33 | ## Installation 34 | You can install `prooph/laravel-package` via Composer by adding `"prooph/laravel-package": "^0.4"` 35 | as requirement to your composer.json. 36 | 37 | ### Service Provider 38 | 39 | If you are using Laravel 5.5 or higher the package will automatically register itself. Otherwise you need to add `Prooph\Package\ProophServiceProvider` to your 40 | [providers](https://laravel.com/docs/master/providers#registering-providers "Visit Laravel Documentation") array. 41 | Then you will have access to the services above. 42 | 43 | This package has configuration files which can be configured to your needs. 44 | 45 | Deploy the prooph config files to add your configuration for the prooph components. 46 | 47 | ```bash 48 | $ php artisan vendor:publish 49 | ``` 50 | 51 | ### Database 52 | Setup your [database migrations](https://github.com/prooph/event-store-doctrine-adapter#database-set-up) 53 | for the Event Store and Snapshot with: 54 | 55 | ```bash 56 | $ php artisan make:migration create_event_stream_table 57 | ``` 58 | 59 | Update the class `CreateEventStreamTable`: 60 | 61 | ```php 62 | class CreateEventStreamTable extends Migration 63 | { 64 | /** 65 | * Run the migrations. 66 | * 67 | * @return void 68 | */ 69 | public function up() 70 | { 71 | \Prooph\Package\Migration\Schema\EventStoreSchema::createSingleStream('event_stream', true); 72 | } 73 | 74 | /** 75 | * Reverse the migrations. 76 | * 77 | * @return void 78 | */ 79 | public function down() 80 | { 81 | \Prooph\Package\Migration\Schema\EventStoreSchema::dropStream('event_stream'); 82 | } 83 | } 84 | ``` 85 | 86 | And now for the snapshot table. 87 | 88 | ```bash 89 | $ php artisan make:migration create_snapshot_table 90 | ``` 91 | 92 | Update the class `CreateSnapshotTable`: 93 | 94 | ```php 95 | class CreateSnapshotTable extends Migration 96 | { 97 | /** 98 | * Run the migrations. 99 | * 100 | * @return void 101 | */ 102 | public function up() 103 | { 104 | \Prooph\Package\Migration\Schema\SnapshotSchema::create('snapshot'); 105 | } 106 | 107 | /** 108 | * Reverse the migrations. 109 | * 110 | * @return void 111 | */ 112 | public function down() 113 | { 114 | \Prooph\Package\Migration\Schema\SnapshotSchema::drop('snapshot'); 115 | } 116 | } 117 | ``` 118 | 119 | Now it's time to execute the migrations: 120 | 121 | ```bash 122 | $ php artisan migrate 123 | ``` 124 | 125 | ## Example 126 | You have only to define your models (Entities, Repositories) and commands / routes. Here is an example config 127 | from the [proophessor-do example app](https://github.com/prooph/proophessor-do "prooph components in action"). 128 | 129 | Define the aggregate repository, command route and event route for `RegisterUser` in `config/prooph.php`. 130 | 131 | ```php 132 | // add the following config in your config/prooph.php under the specific config key 133 | return [ 134 | 'event_store' => [ 135 | // list of aggregate repositories 136 | 'user_collection' => [ 137 | 'repository_class' => \Prooph\ProophessorDo\Infrastructure\Repository\EventStoreUserCollection::class, 138 | 'aggregate_type' => \Prooph\ProophessorDo\Model\User\User::class, 139 | 'aggregate_translator' => \Prooph\EventSourcing\EventStoreIntegration\AggregateTranslator::class, 140 | 'snapshot_store' => \Prooph\EventStore\Snapshot\SnapshotStore::class, 141 | ], 142 | ], 143 | 'service_bus' => [ 144 | 'command_bus' => [ 145 | 'router' => [ 146 | 'routes' => [ 147 | // list of commands with corresponding command handler 148 | \Prooph\ProophessorDo\Model\User\Command\RegisterUser::class => \Prooph\ProophessorDo\Model\User\Handler\RegisterUserHandler::class, 149 | ], 150 | ], 151 | ], 152 | 'event_bus' => [ 153 | 'router' => [ 154 | 'routes' => [ 155 | // list of events with a list of projectors 156 | \Prooph\ProophessorDo\Model\User\Event\UserWasRegistered::class => [ 157 | \Prooph\ProophessorDo\Projection\User\UserProjector::class 158 | ], 159 | ], 160 | ], 161 | ], 162 | ], 163 | ]; 164 | ``` 165 | 166 | Add the service container factories to `config/dependencies.php`. 167 | 168 | ```php 169 | // add the following config in your config/dependencies.php after the other factories 170 | return [ 171 | // your factories 172 | // Model 173 | \Prooph\ProophessorDo\Model\User\Handler\RegisterUserHandler::class => \Prooph\ProophessorDo\Container\Model\User\RegisterUserHandlerFactory::class, 174 | \Prooph\ProophessorDo\Model\User\UserCollection::class => \Prooph\ProophessorDo\Container\Infrastructure\Repository\EventStoreUserCollectionFactory::class, 175 | // Projections 176 | \Prooph\ProophessorDo\Projection\User\UserProjector::class => \Prooph\ProophessorDo\Container\Projection\User\UserProjectorFactory::class, 177 | \Prooph\ProophessorDo\Projection\User\UserFinder::class => \Prooph\ProophessorDo\Container\Projection\User\UserFinderFactory::class, 178 | ]; 179 | ``` 180 | 181 | Here is an example how to call the `RegisterUser` command: 182 | 183 | ```php 184 | /* @var $container \Illuminate\Container\Container */ 185 | 186 | /* @var $commandBus \Prooph\ServiceBus\CommandBus */ 187 | $commandBus = $container->make(Prooph\ServiceBus\CommandBus::class); 188 | 189 | $command = new \Prooph\ProophessorDo\Model\User\Command\RegisterUser( 190 | [ 191 | 'user_id' => \Rhumsaa\Uuid\Uuid::uuid4()->toString(), 192 | 'name' => 'prooph', 193 | 'email' => 'my@domain.com', 194 | ] 195 | ); 196 | 197 | $commandBus->dispatch($command); 198 | ``` 199 | 200 | Here is an example how to get a list of all users from the example above: 201 | 202 | ```php 203 | /* @var $container \Illuminate\Container\Container */ 204 | $userFinder = $container->make(Prooph\ProophessorDo\Projection\User\UserFinder::class); 205 | 206 | $users = $userFinder->findAll(); 207 | ``` 208 | 209 | ## Support 210 | 211 | - Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). 212 | - File issues at [https://github.com/prooph/laravel-package/issues](https://github.com/prooph/laravel-package/issues). 213 | - Say hello in the [prooph gitter](https://gitter.im/prooph/improoph) chat. 214 | 215 | ## Contribute 216 | 217 | Please feel free to fork and extend existing or add new plugins and send a pull request with your changes! 218 | To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. 219 | 220 | ## License 221 | 222 | Released under the [New BSD License](LICENSE.md). 223 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | * 3 | -------------------------------------------------------------------------------- /build/logs/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | * 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prooph/laravel-package", 3 | "description": "Laravel package for prooph components to get started out of the box with message bus, CQRS, event sourcing and snapshots", 4 | "type": "library", 5 | "license": "BSD-3-Clause", 6 | "homepage": "http://getprooph.org", 7 | "authors": [ 8 | { 9 | "name": "Alexander Miertsch", 10 | "email": "contact@prooph.de", 11 | "homepage": "http://prooph-software.com/" 12 | }, 13 | { 14 | "name": "Sandro Keil", 15 | "email": "contact@prooph.de", 16 | "homepage": "http://prooph-software.com/" 17 | }, 18 | { 19 | "name": "Sascha-Oliver Prolic", 20 | "email": "saschaprolic@googlemail.com" 21 | } 22 | ], 23 | "keywords": [ 24 | "prooph", 25 | "laravel", 26 | "package", 27 | "cqrs", 28 | "service bus", 29 | "event sourcing", 30 | "snapshots", 31 | "integration" 32 | ], 33 | "require": { 34 | "php": "^7.1", 35 | "psr/container": "^1.0", 36 | "illuminate/support": "^5.0", 37 | "illuminate/contracts": "^5.0", 38 | "illuminate/bus": "^5.0", 39 | "prooph/event-sourcing": "^5.0", 40 | "prooph/event-store": "^7.0", 41 | "prooph/event-store-bus-bridge": "^3.0", 42 | "prooph/pdo-event-store": "^1.0", 43 | "prooph/service-bus": "^6.0", 44 | "prooph/pdo-snapshot-store": "^1.1", 45 | "sandrokeil/interop-config": "^2.0" 46 | }, 47 | "require-dev": { 48 | "laravel/framework": "^5.0", 49 | "phpunit/phpunit": "^4.8 || ^5.0", 50 | "satooshi/php-coveralls": "^1.0", 51 | "prooph/php-cs-fixer-config": "^0.1.1", 52 | "react/promise": "^2.5" 53 | }, 54 | "suggest": { 55 | "illuminate/database": "If you want to use the database migration schema classes for event store and snapshot", 56 | "proophsoftware/prooph-cli": "For Rapid Prototyping, if you want to generate your aggregates, commands, handlers and events." 57 | }, 58 | "conflict": { 59 | "sandrokeil/interop-config": "<2.0.1" 60 | }, 61 | "autoload": { 62 | "psr-4": { 63 | "Prooph\\Package\\": "src/" 64 | } 65 | }, 66 | "autoload-dev": { 67 | "psr-4": { 68 | "ProophTest\\Package\\": "tests/" 69 | } 70 | }, 71 | "scripts": { 72 | "check": [ 73 | "@cs", 74 | "@test" 75 | ], 76 | "coveralls": "coveralls", 77 | "cs": "php-cs-fixer fix -v --diff --dry-run", 78 | "cs-fix": "php-cs-fixer fix -v --diff", 79 | "test": "phpunit", 80 | "test-coverage": "phpunit --coverage-clover clover.xml" 81 | }, 82 | "extra": { 83 | "laravel": { 84 | "providers": [ 85 | "Prooph\\Package\\ProophServiceProvider" 86 | ], 87 | "aliases": { 88 | "CommandBus": "Prooph\\Package\\Facades\\CommandBus", 89 | "EventBus": "Prooph\\Package\\Facades\\EventBus", 90 | "QueryBus": "Prooph\\Package\\Facades\\QueryBus" 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config/dependencies.php: -------------------------------------------------------------------------------- 1 | InvokableFactory::class, 18 | \Prooph\EventStore\EventStore::class => \Prooph\EventStore\Pdo\Container\MySqlEventStoreFactory::class, 19 | // prooph/service-bus set up 20 | \Prooph\ServiceBus\CommandBus::class => \Prooph\ServiceBus\Container\CommandBusFactory::class, 21 | \Prooph\ServiceBus\EventBus::class => \Prooph\ServiceBus\Container\EventBusFactory::class, 22 | \Prooph\ServiceBus\QueryBus::class => \Prooph\ServiceBus\Container\QueryBusFactory::class, 23 | \Prooph\ServiceBus\Plugin\InvokeStrategy\OnEventStrategy::class => InvokableFactory::class, 24 | // prooph/event-store-bus-bridge set up 25 | \Prooph\EventStoreBusBridge\TransactionManager::class => \Prooph\EventStoreBusBridge\Container\TransactionManagerFactory::class, 26 | \Prooph\EventStoreBusBridge\EventPublisher::class => \Prooph\EventStoreBusBridge\Container\EventPublisherFactory::class, 27 | // your factories 28 | ]; 29 | -------------------------------------------------------------------------------- /config/prooph.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'adapter' => [ 15 | 'type' => \Prooph\EventStore\Pdo\MySqlEventStore::class, 16 | 'options' => [ 17 | 'connection_alias' => 'laravel.connections.pdo', 18 | ], 19 | ], 20 | 'plugins' => [ 21 | \Prooph\EventStoreBusBridge\EventPublisher::class, 22 | \Prooph\EventStoreBusBridge\TransactionManager::class, 23 | ], 24 | // list of aggregate repositories 25 | ], 26 | 'service_bus' => [ 27 | 'command_bus' => [ 28 | 'router' => [ 29 | 'routes' => [ 30 | // list of commands with corresponding command handler 31 | ], 32 | ], 33 | ], 34 | 'event_bus' => [ 35 | 'plugins' => [ 36 | \Prooph\ServiceBus\Plugin\InvokeStrategy\OnEventStrategy::class, 37 | ], 38 | 'router' => [ 39 | 'routes' => [ 40 | // list of events with a list of projectors 41 | ], 42 | ], 43 | ], 44 | ], 45 | 'snapshot_store' => [ 46 | 'adapter' => [ 47 | 'type' => \Prooph\SnapshotStore\Pdo\PdoSnapshotStore::class, 48 | 'options' => [ 49 | 'connection_alias' => 'laravel.connections.pdo', 50 | 'snapshot_table_map' => [ 51 | // list of aggregate root => table (default is snapshot) 52 | ], 53 | ], 54 | ], 55 | ], 56 | 'snapshotter' => [ 57 | 'version_step' => 5, // every 5 events a snapshot 58 | 'aggregate_repositories' => [ 59 | // list of aggregate root => aggregate repositories 60 | ], 61 | ], 62 | ]; 63 | -------------------------------------------------------------------------------- /examples/command_bus.php: -------------------------------------------------------------------------------- 1 | init(); 36 | $this->tweet = $tweet; 37 | } 38 | 39 | public function payload(): array 40 | { 41 | return ['tweet' => $this->tweet]; 42 | } 43 | 44 | protected function setPayload(array $payload): void 45 | { 46 | $this->tweet = $payload['tweet']; 47 | } 48 | } 49 | 50 | CommandBus::utilize(new CommandRouter([ 51 | 'PostTwitterStatus' => function (PostTwitterStatus $command): void { 52 | // You know the drill... curl_exec.... 53 | }, 54 | ])); 55 | 56 | CommandBus::dispatch(PostTwitterStatus::shockingTweet('prooph is awesome.')); 57 | CommandBus::dispatch(PostTwitterStatus::funnyTweet('cats rule.')); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /examples/event_bus.php: -------------------------------------------------------------------------------- 1 | init(); 31 | $this->tweet = $tweet; 32 | } 33 | 34 | public function readTweet(): string 35 | { 36 | return $this->tweet; 37 | } 38 | 39 | public function payload(): array 40 | { 41 | return ['tweet' => $this->tweet]; 42 | } 43 | 44 | protected function setPayload(array $payload): void 45 | { 46 | $this->tweet = $payload['tweet']; 47 | } 48 | } 49 | 50 | EventBus::utilize(new EventRouter([ 51 | 'TwitterStatusWasPosted' => [ 52 | function (TwitterStatusWasPosted $event): void { 53 | // Mail the admins the wicked news. 54 | }, 55 | ], 56 | ])); 57 | 58 | EventBus::dispatch(TwitterStatusWasPosted::withTweet('prooph is awesome.')); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /examples/query_bus.php: -------------------------------------------------------------------------------- 1 | orderBy = 'tweets.created_at DESC'; 30 | 31 | return $query; 32 | } 33 | 34 | private function __construct() 35 | { 36 | $this->init(); 37 | } 38 | 39 | public function payload(): array 40 | { 41 | return ['order_by' => $this->orderBy]; 42 | } 43 | 44 | protected function setPayload(array $payload): void 45 | { 46 | $this->orderBy = $payload['order_by']; 47 | } 48 | } 49 | 50 | QueryBus::utilize(new QueryRouter([ 51 | 'GetTweets' => function (GetTweets $command, Deferred $deferred): void { 52 | // You know the drill... mysql query or something remote.... 53 | $deferred->resolve([ 54 | 'data' => [ 55 | [ 56 | 'tweet' => 'I like prooph', 57 | 'created_at' => '2017-04-27', 58 | ], 59 | ] 60 | ]); 61 | }, 62 | ])); 63 | 64 | // The following two code blocks represent a way of getting 65 | // and handling the result from a query. 66 | 67 | QueryBus::dispatch(GetTweets::latest()) 68 | ->done(function ($result) { 69 | // Handle tweets here. 70 | }, function ($error) { 71 | // Handle error here. 72 | }); 73 | 74 | 75 | try { 76 | $result = QueryBus::resultFrom(GetTweets::latest()); 77 | } catch (QueryResultFailed $error) { 78 | // 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /ide_stubs.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | ./src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Container/InvokableFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function get($id) 43 | { 44 | return $this->container->make($id); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function has($id) 51 | { 52 | if ($this->hasIsCached($id)) { 53 | return $this->hasFromCache($id); 54 | } 55 | 56 | $has = $this->container->bound($id) || $this->isInstantiable($id); 57 | 58 | $this->cacheHas($id, $has); 59 | 60 | return $has; 61 | } 62 | 63 | private function hasIsCached(string $id): bool 64 | { 65 | return array_key_exists($id, $this->cacheForHas); 66 | } 67 | 68 | private function hasFromCache(string $id) 69 | { 70 | return $this->cacheForHas[$id]; 71 | } 72 | 73 | private function cacheHas(string $id, bool $has) 74 | { 75 | $this->cacheForHas[$id] = $has; 76 | } 77 | 78 | private function isInstantiable(string $id): bool 79 | { 80 | if (class_exists($id)) { 81 | return true; 82 | } 83 | 84 | try { 85 | $reflectionClass = new ReflectionClass($id); 86 | 87 | return $reflectionClass->isInstantiable(); 88 | } catch (ReflectionException $e) { 89 | return false; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Exception/QueryResultFailed.php: -------------------------------------------------------------------------------- 1 | 0 && $response[0] instanceof \Throwable) { 34 | $previousException = $response[0]; 35 | } 36 | 37 | $error = new self(sprintf( 38 | 'Query %s failed to get a response.', 39 | self::getQueryName($failedQuery), 40 | $previousException 41 | ), 0, $previousException); 42 | 43 | $error->failedQuery = $failedQuery; 44 | $error->response = $response; 45 | 46 | return $error; 47 | } 48 | 49 | private static function getQueryName($query): string 50 | { 51 | if (is_object($query)) { 52 | return get_class($query); 53 | } 54 | 55 | if (is_array($query)) { 56 | return json_encode($query); 57 | } 58 | 59 | return (string) $query; 60 | } 61 | 62 | public function getFailedQuery() 63 | { 64 | return $this->failedQuery; 65 | } 66 | 67 | public function getResponse(): array 68 | { 69 | return $this->response; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Facades/CommandBus.php: -------------------------------------------------------------------------------- 1 | dispatch($aQuery) 47 | ->done(function ($result) use (&$ret) { 48 | $ret = $result; 49 | }, function () use ($aQuery) { 50 | throw QueryResultFailed::fromQuery($aQuery, func_get_args()); 51 | }); 52 | 53 | return $ret; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Migration/Schema/EventStoreSchema.php: -------------------------------------------------------------------------------- 1 | char('event_id', 36); 35 | // Version of the aggregate after event was recorded 36 | $eventStream->integer('version', false, true); 37 | // Name of the event 38 | $eventStream->string('event_name', 100); 39 | // Event payload 40 | $eventStream->text('payload'); 41 | // DateTime ISO8601 + microseconds UTC stored as a string e.g. 2016-02-02T11:45:39.000000 42 | $eventStream->char('created_at', 26); 43 | // UUID4 of linked aggregate 44 | $eventStream->char('aggregate_id', 36); 45 | // Class of the linked aggregate 46 | $eventStream->string('aggregate_type', 150); 47 | 48 | if ($withCausationColumns) { 49 | // UUID4 of the command which caused the event 50 | $eventStream->char('causation_id', 36); 51 | // Name of the command which caused the event 52 | $eventStream->string('causation_name', 100); 53 | } 54 | $eventStream->primary('event_id'); 55 | // Concurrency check on database level 56 | $eventStream->unique(['aggregate_id', 'aggregate_type', 'version'], $streamName . '_m_v_uix'); 57 | }); 58 | } 59 | 60 | /** 61 | * Use this method when you work with an aggregate type stream strategy 62 | * 63 | * @param string $streamName [shortclassname]_stream 64 | * @param bool $withCausationColumns Enable causation columns when using prooph/event-store-bus-bridge 65 | */ 66 | public static function createAggregateTypeStream($streamName, $withCausationColumns = false) 67 | { 68 | Schema::create($streamName, function (Blueprint $eventStream) use ($streamName, $withCausationColumns) { 69 | // UUID4 of the event 70 | $eventStream->char('event_id', 36); 71 | // Version of the aggregate after event was recorded 72 | $eventStream->integer('version', false, true); 73 | // Name of the event 74 | $eventStream->string('event_name', 100); 75 | // Event payload 76 | $eventStream->text('payload'); 77 | // DateTime ISO8601 + microseconds UTC stored as a string e.g. 2016-02-02T11:45:39.000000 78 | $eventStream->char('created_at', 26); 79 | // UUID4 of linked aggregate 80 | $eventStream->char('aggregate_id', 36); 81 | // Class of the linked aggregate 82 | $eventStream->string('aggregate_type', 150); 83 | 84 | if ($withCausationColumns) { 85 | // UUID4 of the command which caused the event 86 | $eventStream->char('causation_id', 36); 87 | // Name of the command which caused the event 88 | $eventStream->string('causation_name', 100); 89 | } 90 | $eventStream->primary('event_id'); 91 | // Concurrency check on database level 92 | $eventStream->unique(['aggregate_id', 'version'], $streamName . '_m_v_uix'); 93 | }); 94 | } 95 | 96 | /** 97 | * Drop a stream schema 98 | * 99 | * @param string $streamName Defaults to 'event_stream' 100 | */ 101 | public static function dropStream($streamName = 'event_stream') 102 | { 103 | Schema::drop($streamName); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Migration/Schema/SnapshotSchema.php: -------------------------------------------------------------------------------- 1 | char('aggregate_id', 36); 35 | // Class of the linked aggregate 36 | $snapshot->string('aggregate_type', 150); 37 | // Version of the aggregate after event was recorded 38 | $snapshot->integer('last_version', false, true); 39 | // DateTime ISO8601 + microseconds UTC stored as a string e.g. 2016-02-02T11:45:39.000000 40 | $snapshot->char('created_at', 26); 41 | $snapshot->binary('aggregate_root'); 42 | 43 | $snapshot->index(['aggregate_id', 'aggregate_type'], $snapshotName . '_m_v_uix'); 44 | }); 45 | } 46 | 47 | /** 48 | * Drop a snapshot schema 49 | * 50 | * @param string $snapshotName Defaults to 'snapshot' 51 | */ 52 | public static function drop($snapshotName = 'snapshot') 53 | { 54 | Schema::drop($snapshotName); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ProophServiceProvider.php: -------------------------------------------------------------------------------- 1 | getConfigPath(); 30 | 31 | $this->publishes( 32 | [ 33 | $path . '/prooph.php' => config_path('prooph.php'), 34 | $path . '/dependencies.php' => config_path('dependencies.php'), 35 | ], 36 | 'config'); 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | private function getConfigPath() 43 | { 44 | return dirname(__DIR__) . '/config'; 45 | } 46 | 47 | /** 48 | * @interitdoc 49 | */ 50 | public function register() 51 | { 52 | $path = $this->getConfigPath(); 53 | 54 | $this->mergeConfigFrom( 55 | $path . '/prooph.php', 'prooph' 56 | ); 57 | $this->mergeConfigFrom( 58 | $path . '/dependencies.php', 'dependencies' 59 | ); 60 | 61 | $this->app->singleton(LaravelContainer::class, function ($app) { 62 | return new LaravelContainer($app); 63 | }); 64 | 65 | foreach (config('dependencies') as $service => $factory) { 66 | $this->app->singleton($factory, function () use ($factory) { 67 | return new $factory(); 68 | }); 69 | $this->app->singleton($service, function ($app) use ($service, $factory) { 70 | return $app->make($factory)->__invoke($app->make(LaravelContainer::class), $service); 71 | }); 72 | } 73 | 74 | $this->app->singleton('laravel.connections.pdo', function ($app) { 75 | return $app['database']->connection()->getPdo(); 76 | }); 77 | } 78 | 79 | /** 80 | * @interitdoc 81 | */ 82 | public function provides() 83 | { 84 | return [ 85 | // service bus 86 | \Prooph\ServiceBus\CommandBus::class, 87 | \Prooph\ServiceBus\EventBus::class, 88 | \Prooph\ServiceBus\QueryBus::class, 89 | // event-store-bus-bridge 90 | \Prooph\EventStoreBusBridge\TransactionManager::class, 91 | \Prooph\EventStoreBusBridge\EventPublisher::class, 92 | // event store 93 | \Prooph\EventStore\EventStore::class, 94 | \Prooph\EventStore\Pdo\MySqlEventStore::class, 95 | ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Queue/HandleMessageJob.php: -------------------------------------------------------------------------------- 1 | message = $message; 38 | } 39 | 40 | /** 41 | * Execute the job. 42 | * 43 | * @return void 44 | */ 45 | public function handle() 46 | { 47 | // This code will be executed after pulling this class from the queue and deserialization 48 | // 49 | // pass the message to the Message Bus 50 | // 51 | switch ($this->message->messageType()) { 52 | case Message::TYPE_COMMAND: 53 | CommandBus::dispatch($this->message); 54 | break; 55 | case Message::TYPE_QUERY: 56 | QueryBus::dispatch($this->message); 57 | break; 58 | case Message::TYPE_EVENT: 59 | EventBus::dispatch($this->message); 60 | break; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Queue/LaravelQueueMessageProducer.php: -------------------------------------------------------------------------------- 1 | laravel_queue_dispatcher = $laravel_queue_dispatcher; 33 | } 34 | 35 | public function __invoke(Message $message, Deferred $deferred = null): void 36 | { 37 | // As far as I know - Laravel won't let me to get a promise, so no deferred queries are allowed 38 | if ($deferred) { 39 | throw new RuntimeException(__CLASS__ . ' cannot handle query messages which require future responses.'); 40 | } 41 | 42 | // Now dispatch the Laravel job to deal with the queue 43 | $this->laravel_queue_dispatcher->dispatch(new HandleMessageJob($message)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Exception/QueryResultFailedTest.php: -------------------------------------------------------------------------------- 1 | getPrevious()); 33 | } 34 | 35 | /** 36 | * Depending on how the implementation is for handling errors a 37 | * query handler may of done something along the lines of: 38 | * 39 | * ```php 40 | * rejected(404, 'Not Found'); 41 | * ``` 42 | * 43 | * @test 44 | */ 45 | public function it_will_allow_us_to_get_original_response_data() 46 | { 47 | $query = new class([]) extends Query { 48 | use PayloadTrait; 49 | }; 50 | 51 | $customError = [404, 'Not Found']; 52 | $queryException = QueryResultFailed::fromQuery($query, $customError); 53 | 54 | static::assertEquals($customError, $queryException->getResponse()); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function if_no_previous_exception_was_set_it_should_be_null() 61 | { 62 | $query = new class([]) extends Query { 63 | use PayloadTrait; 64 | }; 65 | 66 | $queryException = QueryResultFailed::fromQuery($query, []); 67 | 68 | static::assertNull($queryException->getPrevious()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Facade/CommandBusTest.php: -------------------------------------------------------------------------------- 1 | reveal(); 29 | 30 | Facade::clearResolvedInstances(); 31 | Facade::setFacadeApplication($app); 32 | 33 | $stub->dispatch('AttemptToPassTest')->shouldBeCalled(); 34 | 35 | \Prooph\Package\Facades\CommandBus::dispatch('AttemptToPassTest'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Facade/EventBusTest.php: -------------------------------------------------------------------------------- 1 | reveal(); 29 | 30 | Facade::clearResolvedInstances(); 31 | Facade::setFacadeApplication($app); 32 | 33 | $stub->dispatch('TestWasPassed')->shouldBeCalled(); 34 | 35 | \Prooph\Package\Facades\EventBus::dispatch('TestWasPassed'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Facade/QueryBusTest.php: -------------------------------------------------------------------------------- 1 | reveal(); 30 | 31 | Facade::clearResolvedInstances(); 32 | Facade::setFacadeApplication($app); 33 | 34 | $stub->dispatch('DidTestPass')->shouldBeCalled(); 35 | 36 | \Prooph\Package\Facades\QueryBus::dispatch('DidTestPass'); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function it_can_return_the_result_fine() 43 | { 44 | $app = new Application(); 45 | $stub = static::prophesize(QueryBus::class); 46 | 47 | $app[QueryBus::class] = $stub->reveal(); 48 | 49 | Facade::clearResolvedInstances(); 50 | Facade::setFacadeApplication($app); 51 | 52 | $promise = new Promise(function ($resolve) { 53 | $resolve('abc'); 54 | }, function () { 55 | }); 56 | $stub->dispatch('DidTestPass')->willReturn($promise); 57 | 58 | $result = \Prooph\Package\Facades\QueryBus::resultFrom('DidTestPass'); 59 | 60 | static::assertEquals('abc', $result); 61 | } 62 | 63 | /** 64 | * @test 65 | * @expectedException \Prooph\Package\Exception\QueryResultFailed 66 | */ 67 | public function it_can_return_an_error_fine() 68 | { 69 | $app = new Application(); 70 | $stub = static::prophesize(QueryBus::class); 71 | 72 | $app[QueryBus::class] = $stub->reveal(); 73 | 74 | Facade::clearResolvedInstances(); 75 | Facade::setFacadeApplication($app); 76 | 77 | $promise = new Promise(function ($_, $reject) { 78 | $reject('abc'); 79 | }, function () { 80 | }); 81 | $stub->dispatch('DidTestPass')->willReturn($promise); 82 | 83 | \Prooph\Package\Facades\QueryBus::resultFrom('DidTestPass'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Queue/LaravelQueueMessageProducerTest.php: -------------------------------------------------------------------------------- 1 | reveal(); 35 | $app->bind('Illuminate\Contracts\Bus\Dispatcher', 'Illuminate\Bus\Dispatcher'); 36 | 37 | Facade::clearResolvedInstances(); 38 | Facade::setFacadeApplication($app); 39 | 40 | $command = new AsyncCommandSample(['name' => 'Joe']); 41 | $producer = new \Prooph\Package\Queue\LaravelQueueMessageProducer($app['Illuminate\Contracts\Bus\Dispatcher']); 42 | 43 | $stub->dispatch($command)->shouldBeCalled(); 44 | $producer->__invoke($command); 45 | } 46 | 47 | public function test_producer_will_throw_exception_if_deferred() 48 | { 49 | $app = new Application(); 50 | $app->bind('Illuminate\Contracts\Bus\Dispatcher', 'Illuminate\Bus\Dispatcher'); 51 | 52 | $this->expectException(RuntimeException::class); 53 | 54 | $command = new AsyncCommandSample(['name' => 'Joe']); 55 | $deferred = new Deferred(); 56 | 57 | $producer = new \Prooph\Package\Queue\LaravelQueueMessageProducer($app['Illuminate\Contracts\Bus\Dispatcher']); 58 | $producer->__invoke($command, $deferred); 59 | } 60 | } 61 | --------------------------------------------------------------------------------