├── .coveralls.yml ├── .docheader ├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── examples ├── Infrastructure │ ├── InMemoryEmailGuard.php │ └── UserSpecification.php ├── Model │ ├── Command │ │ ├── ChangeUserName.php │ │ ├── RegisterUser.php │ │ └── UnknownCommand.php │ ├── Event │ │ ├── UserNameChanged.php │ │ ├── UserRegistered.php │ │ └── UserRegisteredWithDuplicateEmail.php │ ├── UniqueEmailGuard.php │ └── User.php └── register_and_change_username.php ├── faq.md ├── spec ├── CommandSpecificationSpec.php └── KernelSpec.php └── src ├── CommandSpecification.php ├── Kernel.php └── NotFound.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | service_name: travis-ci 3 | coverage_clover: build/logs/clover.xml 4 | -------------------------------------------------------------------------------- /.docheader: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the prooph/micro. 3 | * (c) 2017-%year% prooph software GmbH 4 | * (c) 2017-%year% Sascha-Oliver Prolic 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | /vendor 5 | /build 6 | .idea 7 | .php_cs.cache 8 | nbproject 9 | composer.lock 10 | docs/html 11 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | getFinder()->in(__DIR__); 5 | 6 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; 7 | 8 | $config->setCacheFile($cacheDir . '/.php_cs.cache'); 9 | 10 | return $config; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - php: 7.4 7 | env: 8 | - DEPENDENCIES="" 9 | - EXECUTE_CS_CHECK=true 10 | - TEST_COVERAGE=true 11 | - php: 7.4 12 | env: 13 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | - $HOME/.php-cs-fixer 19 | - $HOME/.local 20 | 21 | before_script: 22 | - mkdir -p "$HOME/.php-cs-fixer" 23 | - phpenv config-rm xdebug.ini 24 | - composer self-update 25 | - composer update --prefer-dist $DEPENDENCIES 26 | - mkdir -p build/logs 27 | 28 | script: 29 | - if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so ./vendor/bin/kahlan --coverage=4 --clover=./build/logs/clover.xml; else ./vendor/bin/kahlan; fi 30 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi 31 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/docheader check src/ tests/ examples/; fi 32 | 33 | after_success: 34 | - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi 35 | 36 | notifications: 37 | webhooks: 38 | urls: 39 | - https://webhooks.gitter.im/e/61c75218816eebde4486 40 | on_success: change # options: [always|never|change] default: always 41 | on_failure: always # options: [always|never|change] default: always 42 | on_start: never # options: [always|never|change] default: always 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.2.0](https://github.com/prooph/micro/tree/v0.2.0) 4 | 5 | [Full Changelog](https://github.com/prooph/micro/compare/v0.1.0...v0.2.0) 6 | 7 | **Implemented enhancements:** 8 | 9 | - Get aggregate state version by counting recorded events [\#46](https://github.com/prooph/micro/issues/46) 10 | - test php 7.2 on travis [\#49](https://github.com/prooph/micro/pull/49) ([prolic](https://github.com/prolic)) 11 | - userland code does not require to handle carrying the aggregate version [\#47](https://github.com/prooph/micro/pull/47) ([prolic](https://github.com/prolic)) 12 | - allow aggregate state to be an object [\#45](https://github.com/prooph/micro/pull/45) ([prolic](https://github.com/prolic)) 13 | - Refactoring [\#43](https://github.com/prooph/micro/pull/43) ([prolic](https://github.com/prolic)) 14 | 15 | ## [v0.1.0](https://github.com/prooph/micro/tree/v0.1.0) (2017-04-19) 16 | **Implemented enhancements:** 17 | 18 | - Change command handlers [\#35](https://github.com/prooph/micro/issues/35) 19 | - Add pdo env to php images if they are depended [\#22](https://github.com/prooph/micro/issues/22) 20 | - add micro command for composer [\#21](https://github.com/prooph/micro/issues/21) 21 | - Optimize for one stream per aggregate [\#16](https://github.com/prooph/micro/issues/16) 22 | - Version vs. event no problem with snapshots [\#13](https://github.com/prooph/micro/issues/13) 23 | - TypeError on example [\#12](https://github.com/prooph/micro/issues/12) 24 | - add amqp publisher tests [\#8](https://github.com/prooph/micro/issues/8) 25 | - Use new stable releases [\#41](https://github.com/prooph/micro/pull/41) ([codeliner](https://github.com/codeliner)) 26 | - kernel works with transactional event store [\#38](https://github.com/prooph/micro/pull/38) ([prolic](https://github.com/prolic)) 27 | - implement one stream per aggregate feature [\#37](https://github.com/prooph/micro/pull/37) ([prolic](https://github.com/prolic)) 28 | - Command handlers [\#36](https://github.com/prooph/micro/pull/36) ([prolic](https://github.com/prolic)) 29 | - add micro command for composer [\#31](https://github.com/prooph/micro/pull/31) ([oqq](https://github.com/oqq)) 30 | - remove AmqpProducer [\#30](https://github.com/prooph/micro/pull/30) ([prolic](https://github.com/prolic)) 31 | - change dispatch pipeline [\#23](https://github.com/prooph/micro/pull/23) ([prolic](https://github.com/prolic)) 32 | - add new commands [\#17](https://github.com/prooph/micro/pull/17) ([prolic](https://github.com/prolic)) 33 | - add setup command [\#15](https://github.com/prooph/micro/pull/15) ([prolic](https://github.com/prolic)) 34 | - Improvements [\#14](https://github.com/prooph/micro/pull/14) ([prolic](https://github.com/prolic)) 35 | - track version in example model [\#10](https://github.com/prooph/micro/pull/10) ([prolic](https://github.com/prolic)) 36 | - Snapshotter [\#9](https://github.com/prooph/micro/pull/9) ([prolic](https://github.com/prolic)) 37 | - Amqp publisher [\#7](https://github.com/prooph/micro/pull/7) ([prolic](https://github.com/prolic)) 38 | - replace Pipe with pipeline function [\#6](https://github.com/prooph/micro/pull/6) ([prolic](https://github.com/prolic)) 39 | - Functional [\#1](https://github.com/prooph/micro/pull/1) ([prolic](https://github.com/prolic)) 40 | 41 | **Fixed bugs:** 42 | 43 | - micro:setup:php-service cli command -\> start command not working [\#20](https://github.com/prooph/micro/issues/20) 44 | - Version vs. event no problem with snapshots [\#13](https://github.com/prooph/micro/issues/13) 45 | - Improvements [\#14](https://github.com/prooph/micro/pull/14) ([prolic](https://github.com/prolic)) 46 | - track version in example model [\#10](https://github.com/prooph/micro/pull/10) ([prolic](https://github.com/prolic)) 47 | 48 | **Closed issues:** 49 | 50 | - \[Question\] Should we remove state and snapshots? [\#34](https://github.com/prooph/micro/issues/34) 51 | - Remove upstream definition from nginx site config [\#29](https://github.com/prooph/micro/issues/29) 52 | - Remove AmqpReducer [\#26](https://github.com/prooph/micro/issues/26) 53 | - Move cli + folder structure to a skeleton repo [\#25](https://github.com/prooph/micro/issues/25) 54 | - Input and output streams can differ [\#5](https://github.com/prooph/micro/issues/5) 55 | - Add message validator [\#4](https://github.com/prooph/micro/issues/4) 56 | 57 | **Merged pull requests:** 58 | 59 | - Handle new EventStore::load return type [\#33](https://github.com/prooph/micro/pull/33) ([codeliner](https://github.com/codeliner)) 60 | - Fix pipeline name [\#32](https://github.com/prooph/micro/pull/32) ([codeliner](https://github.com/codeliner)) 61 | - micro:setup:php-service cli command -\> start command not working [\#27](https://github.com/prooph/micro/pull/27) ([oqq](https://github.com/oqq)) 62 | - updates test case to match new implementation [\#24](https://github.com/prooph/micro/pull/24) ([oqq](https://github.com/oqq)) 63 | - SetupCommand improvements [\#19](https://github.com/prooph/micro/pull/19) ([oqq](https://github.com/oqq)) 64 | - improves SetupCommand [\#18](https://github.com/prooph/micro/pull/18) ([oqq](https://github.com/oqq)) 65 | - import FQCNs [\#11](https://github.com/prooph/micro/pull/11) ([basz](https://github.com/basz)) 66 | 67 | 68 | 69 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2017, prooph software GmbH 2 | Copyright (c) 2017-2017, Sascha-Oliver Prolic 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the prooph software GmbH nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prooph-micro 2 | 3 | [Experimental] Functional prooph for microservices 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer install 9 | ``` 10 | 11 | ## Example usage 12 | 13 | ```bash 14 | php examples/register_and_change_username.php 15 | ``` 16 | 17 | ## Run example snapshotter 18 | 19 | ```bash 20 | php examples/user_snapshotter.php 21 | ``` 22 | 23 | ## prooph Microservices in Action 24 | 25 | [prooph/micro-do](https://github.com/prooph/micro-do) 26 | 27 | ## Support 28 | 29 | - Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). 30 | - File issues at [https://github.com/prooph/micro/issues](https://github.com/prooph/micro/issues). 31 | - Say hello in the [prooph gitter](https://gitter.im/prooph/improoph) chat. 32 | 33 | ## Contribute 34 | 35 | Please feel free to fork and extend existing or add new plugins and send a pull request with your changes! 36 | To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. 37 | 38 | ## License 39 | 40 | Released under the [New BSD License](LICENSE). 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prooph/micro", 3 | "description": "Functional prooph for microservices", 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://www.prooph.de" 12 | }, 13 | { 14 | "name": "Sascha-Oliver Prolic", 15 | "email": "saschaprolic@googlemail.com" 16 | } 17 | ], 18 | "keywords": [ 19 | "EventStore", 20 | "EventSourcing", 21 | "DDD", 22 | "prooph" 23 | ], 24 | "require": { 25 | "php": "^7.4", 26 | "amphp/amp": "^2.4.3", 27 | "prooph/event-store": "dev-master", 28 | "phunkie/phunkie": "0.11.1" 29 | }, 30 | "require-dev": { 31 | "kahlan/kahlan": "^4.7.4", 32 | "prooph/event-store-client": "dev-master", 33 | "prooph/php-cs-fixer-config": "^0.3.1", 34 | "php-coveralls/php-coveralls": "^2.1", 35 | "malukenho/docheader": "^0.1.4" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Prooph\\Micro\\": "src/" 40 | }, 41 | "files": [ 42 | "src/Kernel.php" 43 | ] 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "ProophTest\\Micro\\": "tests/" 48 | } 49 | }, 50 | "scripts": { 51 | "check": [ 52 | "@cs", 53 | "@test" 54 | ], 55 | "cs": "php-cs-fixer fix -v --diff --dry-run", 56 | "cs-fix": "php-cs-fixer fix -v --diff", 57 | "test": "phpunit" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/Infrastructure/InMemoryEmailGuard.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Infrastructure; 15 | 16 | use Amp\Promise; 17 | use Amp\Success; 18 | use Prooph\MicroExample\Model\UniqueEmailGuard; 19 | 20 | final class InMemoryEmailGuard implements UniqueEmailGuard 21 | { 22 | private $knownEmails = []; 23 | 24 | public function isUnique(string $email): Promise 25 | { 26 | $isUnique = ! \in_array($email, $this->knownEmails); 27 | 28 | if ($isUnique) { 29 | $this->knownEmails[] = $email; 30 | } 31 | 32 | return new Success($isUnique); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/Infrastructure/UserSpecification.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Infrastructure; 15 | 16 | use Phunkie\Types\ImmList; 17 | use Prooph\EventStore\EventData; 18 | use Prooph\EventStore\EventId; 19 | use Prooph\EventStore\ResolvedEvent; 20 | use Prooph\EventStore\Util\Json; 21 | use Prooph\Micro\CommandSpecification; 22 | use Prooph\MicroExample\Model\Event\UserNameChanged; 23 | use Prooph\MicroExample\Model\Event\UserRegistered; 24 | use Prooph\MicroExample\Model\Event\UserRegisteredWithDuplicateEmail; 25 | use Prooph\MicroExample\Model\User; 26 | 27 | final class UserSpecification extends CommandSpecification 28 | { 29 | public function mapToEventData(object $event): EventData 30 | { 31 | return new EventData( 32 | EventId::generate(), 33 | $event->messageName(), 34 | true, 35 | Json::encode($event->payload()), 36 | Json::encode(['causation_name' => $this->command->messageName()]) 37 | ); 38 | } 39 | 40 | public function mapToEvent(ResolvedEvent $resolvedEvent): object 41 | { 42 | switch ($resolvedEvent->originalEvent()->eventType()) { 43 | case 'username-changed': 44 | return new UserNameChanged(Json::decode($resolvedEvent->originalEvent()->data())); 45 | case 'user-registered': 46 | return new UserRegistered(Json::decode($resolvedEvent->originalEvent()->data())); 47 | case 'user-registered-with-duplicate-email': 48 | return new UserRegisteredWithDuplicateEmail(Json::decode($resolvedEvent->originalEvent()->data())); 49 | default: 50 | throw new \UnexpectedValueException( 51 | 'Unknown event type ' . $resolvedEvent->originalEvent()->eventType() . ' returned' 52 | ); 53 | } 54 | } 55 | 56 | public function initialState() 57 | { 58 | return []; 59 | } 60 | 61 | public function streamName(): string 62 | { 63 | return 'user-' . $this->command->payload()['id']; 64 | } 65 | 66 | public function apply($state, ImmList $events) 67 | { 68 | return User\apply($state, $events); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/Model/Command/ChangeUserName.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Command; 15 | 16 | final class ChangeUserName 17 | { 18 | protected array $payload; 19 | 20 | public function __construct(array $payload = []) 21 | { 22 | $this->payload = $payload; 23 | } 24 | 25 | public function messageName(): string 26 | { 27 | return 'change-username'; 28 | } 29 | 30 | public function payload(): array 31 | { 32 | return $this->payload; 33 | } 34 | 35 | public function id(): string 36 | { 37 | return $this->payload()['id']; 38 | } 39 | 40 | public function name(): string 41 | { 42 | return $this->payload()['name']; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/Model/Command/RegisterUser.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Command; 15 | 16 | final class RegisterUser 17 | { 18 | protected array $payload; 19 | 20 | public function __construct(array $payload = []) 21 | { 22 | $this->payload = $payload; 23 | } 24 | 25 | public function messageName(): string 26 | { 27 | return 'register-user'; 28 | } 29 | 30 | public function payload(): array 31 | { 32 | return $this->payload; 33 | } 34 | 35 | public function id(): string 36 | { 37 | return $this->payload()['id']; 38 | } 39 | 40 | public function name(): string 41 | { 42 | return $this->payload()['name']; 43 | } 44 | 45 | public function email(): string 46 | { 47 | return $this->payload()['email']; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Model/Command/UnknownCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Command; 15 | 16 | final class UnknownCommand 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /examples/Model/Event/UserNameChanged.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Event; 15 | 16 | final class UserNameChanged 17 | { 18 | protected array $payload; 19 | 20 | public function __construct(array $payload = []) 21 | { 22 | $this->payload = $payload; 23 | } 24 | 25 | public function messageName(): string 26 | { 27 | return 'user-name-changed'; 28 | } 29 | 30 | public function payload(): array 31 | { 32 | return $this->payload; 33 | } 34 | 35 | public function id(): string 36 | { 37 | return $this->payload()['id']; 38 | } 39 | 40 | public function name(): string 41 | { 42 | return $this->payload()['name']; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/Model/Event/UserRegistered.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Event; 15 | 16 | final class UserRegistered 17 | { 18 | protected array $payload; 19 | 20 | public function __construct(array $payload = []) 21 | { 22 | $this->payload = $payload; 23 | } 24 | 25 | public function messageName(): string 26 | { 27 | return 'user-registered'; 28 | } 29 | 30 | public function payload(): array 31 | { 32 | return $this->payload; 33 | } 34 | 35 | public function id(): string 36 | { 37 | return $this->payload()['id']; 38 | } 39 | 40 | public function name(): string 41 | { 42 | return $this->payload()['name']; 43 | } 44 | 45 | public function email(): string 46 | { 47 | return $this->payload()['email']; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Model/Event/UserRegisteredWithDuplicateEmail.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\Event; 15 | 16 | final class UserRegisteredWithDuplicateEmail 17 | { 18 | protected array $payload; 19 | 20 | public function __construct(array $payload = []) 21 | { 22 | $this->payload = $payload; 23 | } 24 | 25 | public function messageName(): string 26 | { 27 | return 'user-registered-with-duplicate-email'; 28 | } 29 | 30 | public function payload(): array 31 | { 32 | return $this->payload; 33 | } 34 | 35 | public function id(): string 36 | { 37 | return $this->payload()['id']; 38 | } 39 | 40 | public function name(): string 41 | { 42 | return $this->payload()['name']; 43 | } 44 | 45 | public function email(): string 46 | { 47 | return $this->payload()['email']; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Model/UniqueEmailGuard.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model; 15 | 16 | use Amp\Promise; 17 | 18 | interface UniqueEmailGuard 19 | { 20 | /** 21 | * @return Promise 22 | */ 23 | public function isUnique(string $email): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /examples/Model/User.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Model\User; 15 | 16 | use Generator; 17 | use InvalidArgumentException; 18 | use Phunkie\Types\ImmList; 19 | use Prooph\MicroExample\Model\Command\ChangeUserName; 20 | use Prooph\MicroExample\Model\Command\RegisterUser; 21 | use Prooph\MicroExample\Model\Event\UserNameChanged; 22 | use Prooph\MicroExample\Model\Event\UserRegistered; 23 | use Prooph\MicroExample\Model\Event\UserRegisteredWithDuplicateEmail; 24 | use Prooph\MicroExample\Model\UniqueEmailGuard; 25 | 26 | const registerUser = '\Prooph\MicroExample\Model\User\registerUser'; 27 | 28 | function registerUser(callable $stateResolver, RegisterUser $command, UniqueEmailGuard $guard): Generator 29 | { 30 | if (! yield $guard->isUnique($command->email())) { 31 | yield new UserRegisteredWithDuplicateEmail($command->payload()); 32 | 33 | return; 34 | } 35 | 36 | yield new UserRegistered($command->payload()); 37 | } 38 | 39 | const changeUserName = '\Prooph\MicroExample\Model\User\changeUserName'; 40 | 41 | function changeUserName(callable $stateResolver, ChangeUserName $command): Generator 42 | { 43 | if (! \mb_strlen($command->name()) > 3) { 44 | throw new InvalidArgumentException('Username too short'); 45 | } 46 | 47 | yield new UserNameChanged($command->payload()); 48 | } 49 | 50 | const apply = '\Prooph\MicroExample\Model\User\apply'; 51 | 52 | function apply($state, ImmList $events): array 53 | { 54 | return $events->fold($state, function ($state, $e) { 55 | switch (\get_class($e)) { 56 | case UserRegistered::class: 57 | return \array_merge($state, $e->payload(), ['activated' => true]); 58 | case UserRegisteredWithDuplicateEmail::class: 59 | return \array_merge($state, $e->payload(), ['activated' => false, 'blocked_reason' => 'duplicate email']); 60 | case UserNameChanged::class: 61 | return \array_merge($state, $e->payload()); 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /examples/register_and_change_username.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\MicroExample\Script; 15 | 16 | use Amp\Loop; 17 | use const PHP_EOL; 18 | use Phunkie\Types\ImmList; 19 | use Prooph\EventStore\UserCredentials; 20 | use Prooph\EventStore\Util\Guid; 21 | use Prooph\EventStoreClient\ConnectionSettings; 22 | use Prooph\EventStoreClient\EventStoreConnectionFactory; 23 | use Prooph\Micro\Kernel; 24 | use Prooph\MicroExample\Infrastructure\InMemoryEmailGuard; 25 | use Prooph\MicroExample\Infrastructure\UserSpecification; 26 | use Prooph\MicroExample\Model\Command\ChangeUserName; 27 | use Prooph\MicroExample\Model\Command\RegisterUser; 28 | use Prooph\MicroExample\Model\Command\UnknownCommand; 29 | use Prooph\MicroExample\Model\User; 30 | use Throwable; 31 | 32 | $autoloader = require __DIR__ . '/../vendor/autoload.php'; 33 | $autoloader->addPsr4('Prooph\\MicroExample\\', __DIR__); 34 | require 'Model/User.php'; 35 | 36 | function showResult($result): void 37 | { 38 | if ($result instanceof ImmList) { 39 | echo $result->show() . PHP_EOL; 40 | echo \json_encode($result->head()->payload()) . PHP_EOL . PHP_EOL; 41 | } 42 | } 43 | 44 | Loop::run(function (): \Generator { 45 | $start = \microtime(true); 46 | 47 | $settings = ConnectionSettings::create() 48 | ->setDefaultUserCredentials( 49 | new UserCredentials('admin', 'changeit') 50 | ); 51 | 52 | $connection = EventStoreConnectionFactory::createFromConnectionString( 53 | 'ConnectTo=tcp://admin:changeit@10.121.1.4:1113', 54 | $settings->build() 55 | ); 56 | 57 | $connection->onConnected(function () { 58 | echo 'Event Store connection established' . PHP_EOL; 59 | }); 60 | 61 | $connection->onClosed(function () { 62 | echo 'Event Store connection closed' . PHP_EOL; 63 | }); 64 | 65 | yield $connection->connectAsync(); 66 | 67 | $uniqueEmailGuard = new InMemoryEmailGuard(); 68 | 69 | $commandMap = ImmMap([ 70 | ChangeUserName::class => fn ($m) => new UserSpecification($m, User\changeUserName), 71 | RegisterUser::class => fn ($m) => new UserSpecification($m, fn (callable $s, $m) => User\registerUser($s, $m, $uniqueEmailGuard)), 72 | ]); 73 | 74 | $dispatch = Kernel\buildCommandDispatcher($connection, $commandMap, fn (object $c, object $e) => $e); 75 | 76 | $userId = Guid::generateString(); 77 | 78 | try { 79 | $result = yield $dispatch(new RegisterUser(['id' => $userId, 'name' => 'Alex', 'email' => 'member@getprooph.org'])); 80 | } catch (Throwable $e) { 81 | echo \get_class($e) . ': ' . $e->getMessage() . PHP_EOL . PHP_EOL; 82 | } 83 | 84 | showResult($result); 85 | 86 | try { 87 | $result = yield $dispatch(new ChangeUserName(['id' => $userId, 'name' => 'Sascha'])); 88 | } catch (Throwable $e) { 89 | echo \get_class($e) . ': ' . $e->getMessage() . PHP_EOL . PHP_EOL; 90 | } 91 | 92 | showResult($result); 93 | 94 | try { 95 | $result = yield $dispatch(new UnknownCommand()); 96 | } catch (Throwable $e) { 97 | echo \get_class($e) . ': ' . $e->getMessage() . PHP_EOL . PHP_EOL; 98 | } 99 | 100 | showResult($result); 101 | 102 | $time = \microtime(true) - $start; 103 | 104 | echo $time . "secs runtime\n\n"; 105 | 106 | $connection->close(); 107 | }); 108 | -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | What follows is a discussion between @max-voloshin and @codeliner in the prooph/improoph gitter chat. 4 | The dialog covers some detailed answers to questions that will arise when looking at prooph/micro for the first time. 5 | 6 | Max Voloshin @max-voloshin 10:14 7 | 8 | @codeliner do you advise to use prooph/micro instead of prooph/event-sourcing? 9 | 10 | Alexander Miertsch @codeliner 11:50 11 | 12 | @max-voloshin prooph/micro is in an early state. It aims to simplify event sourcing a lot due to the functional approach but it introduces a set of new problems: 13 | 14 | - not really tested in any production system 15 | - you need small services (ideally one php service for each aggregate) 16 | - Docker and Nginx become your best friends 17 | 18 | If you can live with that and want to be a pioneer then prooph/micro is an interesting choice. 19 | We want to build an eCommerce system based on prooph/micro. Everything will be dockerized and managed by swarm cluster in the cloud. We want to see how far we can get with that approach. But until we have working code (ideally in production), I cannot recommend anything. It is an experiment. To quote @Ocramius "When you use Microservices you introduce a new kind of failures. Where you have true or false before, you now have true/false or http error" 20 | While I get the point of his statement, I also think that smaller, dockerized services solve a lot of other problems. What I really really like so far is, that you can reduce the boilerplate php code to a bare minimum AND with Nginx and PHP-FPM you get non blocking I/O like you would have with Node.js but inside a single PHP process you can still work with the good old synchronous style and don't need things like async/await. 21 | tl;dr: prooph/micro is not for everybody. You need to be a) a PHP lover, who probably should be a Scala, Erlang or even Node.js developer and b) don't fear the complexity of Docker and Nginx management 22 | 23 | Max Voloshin @max-voloshin 13:39 24 | 25 | @codeliner thanks for the detailed answer! 26 | Small services, Docker, Nginx, Scala – 27 | 28 | I looked prooph/micro some time ago. 29 | 30 | I didn't get an idea of usage of dedicated php-fpm service for each aggregate type. Increasing number of php-fpm services doesn't effect on blocking I/O, does it? BTW usually services stick to bounded contexts, not aggregate type. 31 | Declaration of aggregate in that way https://github.com/prooph/micro-do/blob/master/service/user-write/src/Model/User.php looks more procedural than functional as for me. Which advantages do you get by that approach? 32 | 33 | Anyway I really appreciate prooph team's effort for current prod ready solutions and discovering a more efficient way to develop applications for the future 34 | 35 | Alexander Miertsch @codeliner 14:53 36 | 37 | @max-voloshin 38 | yes, BC <-> Service 1:1 39 | 40 | but PHP-FPM "Service" != business service, that is really a technical service 41 | 42 | my definition at the moment (as I said experimental) BC = Service = Nginx API Gateway + n PHP-FPM container (1 container per aggregate type) 43 | 44 | the Nginx API Gateway is the wall in front of your BC, like an application layer would be in a monolith 45 | regarding blocking/non blocking, the thing is that for example Node.js is single threaded but is non blocking I/O whereby PHP is normally blocking. If you handle http request routing in PHP your process is blocking longer, because it needs to route the request first, then select some data, then process the business logic (command) and if you're not using a queue also send out emails, update the read model .... All this in a single blocking process 46 | When you move http request handling to Nginx and just start with handling the command with PHP you should be able to get a faster system, that can handle more requests in parallel without additional hardware. Also this architecture is scalable by design and emphasis async read model projections and the usage of async process managers 47 | 48 | Can I ask back, why the user aggregate looks more procedual than functional? I'd anwser your question after you answered mine 49 | 50 | Max Voloshin @max-voloshin 20:43 51 | 52 | @codeliner 53 | 54 | > BC = Service = Nginx API Gateway + n PHP-FPM container (1 container per aggregate type) 55 | 56 | Thanks, now it's clear :grinning: 57 | 58 | Regarding blocking/non blocking. I agree with all things you wrote, but I don't understand how it's related to multiple php-fpm service. So why we can't work with requests in described way with one php-fpm service? 59 | Can I ask back, why the user aggregate looks more procedural than functional? I'd answer your question after you answered mine :wink: 60 | 61 | I think so because I see standalone functions which work with a standalone untyped data structure (array) and mixed public/private API. Yes, I also see nice pure functions, but previous observations are more important as for me. I admit I could be wrong because of incomplete visions, so I will be glad to know details from you :grinning: 62 | 63 | Alexander Miertsch @codeliner 21:55 64 | 65 | @max-voloshin 66 | 67 | > but I don't understand how it's related to multiple php-fpm service 68 | 69 | ah, now I understand your concern and btw. thank you for the discussion. It is a good way to think about the concept before coding too much. Back to the question: if you would handle more than one aggregate type within the same PHP-FPM service, you would need to do some more routing again. That alone is not a problem. Routing could be done by a command bus BUT dependency management becomes pain again. If you know that your PHP-FPM service always only handle commands of a specific aggregate type then the list of dependencies is kept very small. And that is basically the idea of Microservices: small services, focused on doing only one thing, ideally with very few dependencies so that each service of its own is very very simple to build and to maintain. 70 | 71 | If you have those units you compose them to "higher order" services aka. bounded contexts and then again those BCs can be composed to an application/system. 72 | 73 | So if you have very few dependencies, you don't need a DI container, which means a lot of reduced complexity. You also don't need heavy configuration and such. Just set up command dispatching in a programmatic way, invoke aggregate functions with commands, and push the outcome of command handling aka. the events to the next service that will maybe trigger more commands, send out emails and do other async stuff. 74 | 75 | My feeling is that the overall complexity of an enterprise system can be reduced with such an approach and Microservice fans observe exactly this. Complexity is moved out of the application into infrastructure. You need to set up Docker, swarm, AWS whatever ... but tooling around such an infrastructure is really good: you get auto-scaling, self-healing systems, monitoring and so on. Also it is so much easier to throw away an entire service behind an API and replace it with something totally different that may does the same thing 10x faster... or in a more secure way or both. 76 | I worked in a migration project where we tried to migrate a 30 years old system. It took us two years and was really anything but funny. If prooph/micro works as expected, it will be the complete opposite of such a monolith and I think this will be a perfect match for event sourcing and CQRS. I mean prooph/micro is not a new approach. See the Serverless framework for example BUT prooph/micro will be tailored to prooph. I'd also use Serverless but then I'd need to rewrite prooph in Node.js and I'm not sure if @prolic and the rest of the team would hate me for that :smiley: Anyway, Nginx + PHP-FPM + Docker + prooph are a great combination so no Serverless needed or at least no Node.js version needed for the things we want to achieve :wink: 77 | 78 | regarding untyped data structure (array): yes, that is not 100% functional. But we said: Hey, it is still PHP, so we pick some functional ideas but mix them with the things we have in PHP and arrays for example are semi immutable because they are not passed by reference by default :wink: 79 | 80 | Max Voloshin @max-voloshin 22:43 81 | 82 | More granular dependency management is right argument for multiple php-fpm services, now it's clear. But maybe prooph/nano is a better name for this solution? :laughing: 83 | arrays for example are semi immutable because they are not passed by reference by default :wink: 84 | Hm, I didn't think about it from that POV. Immutable objects are rare in PHP: https://twitter.com/maxvoloshindev/status/558616020534173696 85 | Follow 86 | Max Voloshin @maxvoloshindev 87 | php -r '$e=new Exception('0');$e->__construct('1');echo $e->getMessage();' works without errors and returns 1 ... #php #wat 88 | 2:23 PM - 23 Jan 2015 89 | Retweets likes 90 | :grinning: 91 | @codeliner thank you for answers again! Frankly, I can't say I am ready to use such approach in production right now, but at least it is much more understandable for me now. 92 | 93 | Alexander Miertsch @codeliner 23:51 94 | 95 | you're welcome. 96 | 97 | > I can't say I am ready to use such approach in production right now 98 | 99 | Understandable :smiley: But look at Lambda and Serverless ... Some crazy guys do exactly that and they receive a lot of money to improve this way of designing distributed systems. Also PHP was originally designed as a scripting language. See how a request/response cycle works in PHP. The global state is bootstrapped on every request and then thrown away once the response is sent back to the client. That is still a "script" which is invoked by a process manager (PHP-FPM) and killed when it is done. 100 | 101 | If you see it this way prooph/micro or nano :wink: makes use of the one feature in PHP that makes it easy to use on the one hand but hard to use at the same time, namely reset everything after each run. 102 | We just remove everything from the "script" that is better placed in a component that does not die: 103 | 104 | - http handling to a non blocking web server 105 | - parallel processes into PHP-FPM 106 | - event stream projections into simple long-running php scripts 107 | - .... 108 | 109 | If you remove all the bits where PHP sucks by design you are left with a super simple but very powerful runtime environment that does one thing and does it well: 110 | 111 | > command handler = f(fold history match -> state, command) -> events 112 | 113 | https://github.com/prooph/micro/issues/34#issuecomment-276124831 (by @gregoryyoung) 114 | _ 115 | 116 | -------------------------------------------------------------------------------- /spec/CommandSpecificationSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Micro; 15 | 16 | use Amp\Success; 17 | use Kahlan\Plugin\Double; 18 | use Prooph\EventStore\EventId; 19 | use Prooph\EventStore\ExpectedVersion; 20 | use Prooph\EventStore\RecordedEvent; 21 | use Prooph\EventStore\ResolvedEvent; 22 | use Prooph\Micro\CommandSpecification; 23 | 24 | describe('Prooph Micro', function () { 25 | context('Command Specification', function () { 26 | describe('Command Handling', function () { 27 | it('can handle incoming commands', function () { 28 | $command = new \stdClass(); 29 | $handler = fn ($s, $m) => yield 'foo'; 30 | 31 | $spec = Double::instance([ 32 | 'extends' => CommandSpecification::class, 33 | 'args' => [$command, $handler], 34 | ]); 35 | 36 | expect($spec->handle(fn () => []))->toBeAnInstanceOf(\Generator::class); 37 | }); 38 | }); 39 | 40 | describe('Aggregate State Reconstruction', function () { 41 | it('can reconstitute aggregate state from event history', function () { 42 | $command = new \stdClass(); 43 | $handler = fn () => new Success(); 44 | 45 | $spec = Double::instance([ 46 | 'extends' => CommandSpecification::class, 47 | 'args' => [$command, $handler], 48 | ]); 49 | allow($spec)->toReceive('initialState')->andReturn(0); 50 | allow($spec)->toReceive('mapToEvent')->andRun(function (ResolvedEvent $re): object { 51 | $e = new \stdClass(); 52 | $e->v = (int) $re->originalEvent()->data(); 53 | 54 | return $e; 55 | }); 56 | allow($spec)->toReceive('apply')->andReturn(6); 57 | 58 | $now = new \DateTimeImmutable(); 59 | 60 | $re1 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '1', '', $now), null, null); 61 | $re2 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '2', '', $now), null, null); 62 | $re3 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '3', '', $now), null, null); 63 | 64 | expect($spec->reconstituteFromHistory(ImmList($re1, $re2, $re3)))->toBe(6); 65 | }); 66 | }); 67 | 68 | describe('Default expected version value', function () { 69 | it('will return "any" as default value for expected version', function () { 70 | $command = new \stdClass(); 71 | $handler = fn () => new Success(); 72 | 73 | $spec = Double::instance([ 74 | 'extends' => CommandSpecification::class, 75 | 'args' => [$command, $handler], 76 | ]); 77 | 78 | expect($spec->expectedVersion())->toBe(ExpectedVersion::ANY); 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /spec/KernelSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\Micro; 15 | 16 | use function Amp\Promise\wait; 17 | use Amp\Success; 18 | use function expect; 19 | use Kahlan\Plugin\Double; 20 | use Phunkie\Types\ImmList; 21 | use Prooph\EventStore\Async\EventStoreConnection; 22 | use Prooph\EventStore\EventData; 23 | use Prooph\EventStore\EventId; 24 | use Prooph\EventStore\RecordedEvent; 25 | use Prooph\EventStore\ResolvedEvent; 26 | use Prooph\EventStore\SliceReadStatus; 27 | use Prooph\EventStore\StreamEventsSlice; 28 | use Prooph\Micro\CommandSpecification; 29 | use function Prooph\Micro\Kernel\buildCommandDispatcher; 30 | use function Prooph\Micro\Kernel\stateResolver; 31 | use Prooph\Micro\NotFound; 32 | use RuntimeException; 33 | 34 | describe('Prooph Micro', function () { 35 | context('Kernel', function () { 36 | describe('when resolving state', function () { 37 | it('will reconstitute state when event history found', function () { 38 | $command = new \stdClass(); 39 | $handler = fn ($s, $m) => $s(); 40 | 41 | $spec = Double::instance([ 42 | 'extends' => CommandSpecification::class, 43 | 'args' => [$command, $handler], 44 | ]); 45 | 46 | $connection = Double::instance([ 47 | 'implements' => EventStoreConnection::class, 48 | ]); 49 | 50 | $slice = Double::instance([ 51 | 'extends' => StreamEventsSlice::class, 52 | 'magicMethods' => true, 53 | ]); 54 | 55 | $now = new \DateTimeImmutable(); 56 | 57 | $re1 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '1', '', $now), null, null); 58 | $re2 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '2', '', $now), null, null); 59 | $re3 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '3', '', $now), null, null); 60 | 61 | allow($spec)->toReceive('streamName')->andReturn('test-stream'); 62 | allow($spec)->toReceive('initialState')->andReturn(0); 63 | allow($spec)->toReceive('mapToEvent')->andRun(function (ResolvedEvent $re): object { 64 | $e = new \stdClass(); 65 | $e->v = (int) $re->originalEvent()->data(); 66 | 67 | return $e; 68 | }); 69 | allow($spec)->toReceive('apply')->andReturn(6); 70 | 71 | allow($connection)->toReceive('readStreamEventsForwardAsync')->andReturn(new Success($slice)); 72 | 73 | allow($slice)->toReceive('isEndOfStream')->andReturn(true); 74 | allow($slice)->toReceive('status')->andReturn(SliceReadStatus::success()); 75 | allow($slice)->toReceive('events')->andReturn([$re1, $re2, $re3]); 76 | 77 | expect(wait(stateResolver($connection, $spec, 5)()))->toBe(6); 78 | }); 79 | 80 | it('will throw, when stream not found', function () { 81 | $command = new \stdClass(); 82 | $handler = fn ($s, $m) => $s(); 83 | 84 | $spec = Double::instance([ 85 | 'extends' => CommandSpecification::class, 86 | 'args' => [$command, $handler], 87 | ]); 88 | 89 | $connection = Double::instance([ 90 | 'implements' => EventStoreConnection::class, 91 | ]); 92 | 93 | $slice = Double::instance([ 94 | 'extends' => StreamEventsSlice::class, 95 | 'magicMethods' => true, 96 | ]); 97 | 98 | allow($spec)->toReceive('streamName')->andReturn('test-stream'); 99 | allow($connection)->toReceive('readStreamEventsForwardAsync')->andReturn(new Success($slice)); 100 | allow($slice)->toReceive('status')->andReturn(SliceReadStatus::streamNotFound()); 101 | 102 | $closure = fn () => wait(stateResolver($connection, $spec, 5)()); 103 | 104 | expect($closure)->toThrow(new NotFound('Stream not found')); 105 | }); 106 | 107 | it('will throw, when stream deleted', function () { 108 | $command = new \stdClass(); 109 | $handler = fn ($s, $m) => $s(); 110 | 111 | $spec = Double::instance([ 112 | 'extends' => CommandSpecification::class, 113 | 'args' => [$command, $handler], 114 | ]); 115 | 116 | $connection = Double::instance([ 117 | 'implements' => EventStoreConnection::class, 118 | ]); 119 | 120 | $slice = Double::instance([ 121 | 'extends' => StreamEventsSlice::class, 122 | 'magicMethods' => true, 123 | ]); 124 | 125 | allow($spec)->toReceive('streamName')->andReturn('test-stream'); 126 | allow($connection)->toReceive('readStreamEventsForwardAsync')->andReturn(new Success($slice)); 127 | allow($slice)->toReceive('status')->andReturn(SliceReadStatus::streamDeleted()); 128 | 129 | $closure = fn () => wait(stateResolver($connection, $spec, 5)()); 130 | 131 | expect($closure)->toThrow(new NotFound('Stream deleted')); 132 | }); 133 | }); 134 | 135 | describe('when executing the command dispatcher', function () { 136 | it('will execute the command handler successfully', function () { 137 | $command = new \stdClass(); 138 | $event = new \stdClass(); 139 | $event->v = 'foo'; 140 | $handler = function ($s, $m) use ($event) { 141 | yield $event; 142 | }; 143 | $enrich = fn ($e) => $e; 144 | 145 | $spec = Double::instance([ 146 | 'extends' => CommandSpecification::class, 147 | 'args' => [$command, $handler, $enrich], 148 | ]); 149 | 150 | $map = ImmMap([ 151 | \stdClass::class => fn ($m) => $spec, 152 | ]); 153 | 154 | $connection = Double::instance([ 155 | 'implements' => EventStoreConnection::class, 156 | ]); 157 | 158 | allow($spec)->toReceive('streamName')->andReturn('test-stream'); 159 | allow($spec)->toReceive('enrich')->andReturn($event); 160 | allow($spec)->toReceive('mapToEventData')->andRun(function (object $e): object { 161 | return new EventData( 162 | null, 163 | 'test-event', 164 | false, 165 | $e->v, 166 | '' 167 | ); 168 | }); 169 | 170 | allow($connection)->toReceive('appendToStreamAsync')->andRun(fn () => new Success()); 171 | 172 | $dispatch = buildCommandDispatcher($connection, $map, fn ($c, $e) => $e); 173 | 174 | $result = wait($dispatch($command)); 175 | 176 | $expectedEvent = new \stdClass(); 177 | $expectedEvent->v = 'foo'; 178 | 179 | expect($result)->toBeAnInstanceOf(ImmList::class); 180 | expect($result->head())->toEqual($expectedEvent); 181 | }); 182 | 183 | it('returns failure when no specification found for a given command', function () { 184 | $command = new \stdClass(); 185 | $map = ImmMap(); 186 | 187 | $connection = Double::instance([ 188 | 'implements' => EventStoreConnection::class, 189 | ]); 190 | 191 | $dispatch = buildCommandDispatcher($connection, $map, fn ($c, $e) => $e); 192 | $syncedDispatch = fn () => wait($dispatch($command)); 193 | 194 | expect($syncedDispatch)->toThrow( 195 | new RuntimeException('No configuration found for stdClass') 196 | ); 197 | }); 198 | 199 | it('returns failure when command handler throws', function () { 200 | $command = new \stdClass(); 201 | $handler = function ($s, $m) { 202 | throw new \RuntimeException('Boom!'); 203 | }; 204 | 205 | $spec = Double::instance([ 206 | 'extends' => CommandSpecification::class, 207 | 'args' => [$command, $handler], 208 | ]); 209 | 210 | $map = ImmMap([ 211 | \stdClass::class => fn ($m) => $spec, 212 | ]); 213 | 214 | $connection = Double::instance([ 215 | 'implements' => EventStoreConnection::class, 216 | ]); 217 | 218 | $slice = Double::instance([ 219 | 'extends' => StreamEventsSlice::class, 220 | 'magicMethods' => true, 221 | ]); 222 | 223 | $now = new \DateTimeImmutable(); 224 | 225 | $re1 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '1', '', $now), null, null); 226 | $re2 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '2', '', $now), null, null); 227 | $re3 = new ResolvedEvent(new RecordedEvent('stream', 0, EventId::generate(), 'test', false, '3', '', $now), null, null); 228 | 229 | allow($spec)->toReceive('streamName')->andReturn('test-stream'); 230 | allow($spec)->toReceive('initialState')->andReturn(0); 231 | allow($spec)->toReceive('mapToEvent')->andRun(function (ResolvedEvent $re): object { 232 | $e = new \stdClass(); 233 | $e->v = (int) $re->originalEvent()->data(); 234 | 235 | return $e; 236 | }); 237 | allow($spec)->toReceive('apply')->andReturn(6); 238 | 239 | allow($connection)->toReceive('readStreamEventsForwardAsync')->andReturn(new Success($slice)); 240 | 241 | allow($slice)->toReceive('status')->andReturn(SliceReadStatus::success()); 242 | allow($slice)->toReceive('events')->andReturn([$re1, $re2, $re3]); 243 | 244 | $dispatch = buildCommandDispatcher($connection, $map, fn ($c, $e) => $e); 245 | $syncedDispatch = fn () => wait($dispatch($command)); 246 | 247 | expect($syncedDispatch)->toThrow(new RuntimeException('Boom!')); 248 | }); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/CommandSpecification.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Micro; 15 | 16 | use Closure; 17 | use Generator; 18 | use Phunkie\Types\ImmList; 19 | use Prooph\EventStore\EventData; 20 | use Prooph\EventStore\ExpectedVersion; 21 | use Prooph\EventStore\ResolvedEvent; 22 | 23 | abstract class CommandSpecification 24 | { 25 | protected object $command; 26 | protected Closure $handler; 27 | 28 | public function __construct(object $command, callable $handler) 29 | { 30 | $this->command = $command; 31 | $this->handler = Closure::fromCallable($handler); 32 | } 33 | 34 | public function handle(Closure $stateResolver): Generator 35 | { 36 | return ($this->handler)($stateResolver, $this->command); 37 | } 38 | 39 | /** 40 | * @param ImmList $events 41 | * @return mixed 42 | */ 43 | public function reconstituteFromHistory(ImmList $events) 44 | { 45 | return $this->apply($this->initialState(), $events->map(fn ($e) => $this->mapToEvent($e))); 46 | } 47 | 48 | public function expectedVersion(): int 49 | { 50 | return ExpectedVersion::ANY; 51 | } 52 | 53 | public function enrich(object $command, object $event, callable $enrich): object 54 | { 55 | return $enrich($command, $event); 56 | } 57 | 58 | abstract public function mapToEventData(object $event): EventData; 59 | 60 | abstract public function mapToEvent(ResolvedEvent $resolvedEvent): object; 61 | 62 | /** @return mixed */ 63 | abstract public function initialState(); 64 | 65 | abstract public function streamName(): string; 66 | 67 | /** 68 | * @param mixed $initialState 69 | * @param ImmList $events 70 | * @return mixed 71 | */ 72 | abstract public function apply($initialState, ImmList $events); 73 | } 74 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Micro\Kernel; 15 | 16 | use function Amp\call; 17 | use Amp\Producer; 18 | use Amp\Promise; 19 | use Closure; 20 | use Generator; 21 | use Phunkie\Types\ImmMap; 22 | use Prooph\EventStore\Async\EventStoreConnection; 23 | use Prooph\EventStore\SliceReadStatus; 24 | use Prooph\EventStore\StreamEventsSlice; 25 | use Prooph\Micro\CommandSpecification; 26 | use Prooph\Micro\NotFound; 27 | use RuntimeException; 28 | 29 | const buildCommandDispatcher = 'Prooph\Micro\Kernel\buildCommandDispatcher'; 30 | 31 | function buildCommandDispatcher( 32 | EventStoreConnection $eventStore, 33 | ImmMap $commandMap, 34 | callable $enrich, 35 | int $readBatchSize = 200 36 | ): callable { 37 | return function (object $m) use ($eventStore, $commandMap, $enrich, $readBatchSize): Promise { 38 | return call(function () use ($m, $eventStore, $commandMap, $enrich, $readBatchSize): Generator { 39 | $messageClass = \get_class($m); 40 | $config = $commandMap->get($messageClass); 41 | 42 | if ($config->isEmpty()) { 43 | throw new RuntimeException( 44 | 'No configuration found for ' . $messageClass 45 | ); 46 | } 47 | 48 | $specification = $config->get()($m); 49 | \assert($specification instanceof CommandSpecification); 50 | 51 | $iterator = new Producer(function (callable $emit) use ($eventStore, $specification, $enrich, $readBatchSize): Generator { 52 | $generator = $specification->handle(stateResolver($eventStore, $specification, $readBatchSize)); 53 | 54 | while ($generator->valid()) { 55 | $eventOrPromise = $generator->current(); 56 | 57 | if ($eventOrPromise instanceof Promise) { 58 | try { 59 | $generator->send(yield $eventOrPromise); 60 | } catch (\Throwable $exception) { 61 | $generator->throw($exception); 62 | } 63 | } else { 64 | yield $emit($eventOrPromise); 65 | $generator->next(); 66 | } 67 | } 68 | }); 69 | 70 | $events = []; 71 | $eventData = []; 72 | 73 | while (yield $iterator->advance()) { 74 | $event = $specification->enrich($m, $iterator->getCurrent(), $enrich); 75 | 76 | $events[] = $event; 77 | $eventData[] = $specification->mapToEventData($event); 78 | } 79 | 80 | yield $eventStore->appendToStreamAsync( 81 | $specification->streamName(), 82 | $specification->expectedVersion(), 83 | $eventData 84 | ); 85 | 86 | return ImmList(...$events); 87 | }); 88 | }; 89 | } 90 | 91 | const stateResolver = 'Prooph\Micro\Kernel\stateResolver'; 92 | 93 | function stateResolver(EventStoreConnection $eventStore, CommandSpecification $specification, int $readBatchSize): Closure 94 | { 95 | return function () use ($eventStore, $specification, $readBatchSize): Promise { 96 | return call(function () use ($eventStore, $specification, $readBatchSize): Generator { 97 | $events = []; 98 | 99 | do { 100 | $slice = yield $eventStore->readStreamEventsForwardAsync( 101 | $specification->streamName(), 102 | 0, 103 | $readBatchSize, 104 | ); 105 | \assert($slice instanceof StreamEventsSlice); 106 | 107 | switch ($slice->status()->value()) { 108 | case SliceReadStatus::STREAM_NOT_FOUND: 109 | throw new NotFound('Stream not found'); 110 | case SliceReadStatus::STREAM_DELETED: 111 | throw new NotFound('Stream deleted'); 112 | } 113 | 114 | $events = \array_merge($events, $slice->events()); 115 | } while (! $slice->isEndOfStream() && \count($slice->events()) === $readBatchSize); 116 | 117 | return $specification->reconstituteFromHistory(ImmList(...$events)); 118 | }); 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /src/NotFound.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2020 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\Micro; 15 | 16 | use RuntimeException; 17 | 18 | class NotFound extends RuntimeException 19 | { 20 | } 21 | --------------------------------------------------------------------------------