├── app ├── config │ ├── routing_prod.yml │ ├── routing_dev.yml │ ├── routing_test.yml │ ├── services.yml │ └── config.yml ├── console └── AppKernel.php ├── tests ├── .env ├── bootstrap.php └── SuperAwesome │ └── Blog │ └── Domain │ └── Model │ └── Post │ └── Adapter │ └── SuperAwesome │ ├── Command │ └── Handler │ │ ├── CreatePostHandlerTest.php │ │ ├── AbstractPostHandlerTest.php │ │ ├── PostHandlerScenario.php │ │ ├── TagPostHandlerTest.php │ │ ├── UntagPostHandlerTest.php │ │ └── PublishPostHandlerTest.php │ ├── PostScenario.php │ └── PostTest.php ├── .gitignore ├── .env.dist ├── src └── SuperAwesome │ ├── Common │ ├── Domain │ │ ├── Model │ │ │ ├── AppliesRecordedEvents.php │ │ │ ├── RecordsEvents.php │ │ │ ├── EventSourcing.php │ │ │ └── Adapter │ │ │ │ └── Broadway │ │ │ │ └── BroadwayModelRepository.php │ │ └── ReadModel │ │ │ └── Adapter │ │ │ └── Broadway │ │ │ └── PoorlyDesignedBroadwayDbalRepository.php │ └── Infrastructure │ │ ├── EventBus │ │ ├── EventFromBusListener.php │ │ ├── EventBus.php │ │ ├── SimpleEventBus.php │ │ └── SpyingEventBus.php │ │ └── EventStore │ │ ├── EventStore.php │ │ ├── InMemoryEventStore.php │ │ └── SpyingEventStore.php │ ├── Symfony │ └── BlogBundle │ │ ├── SuperAwesomeBlogBundle.php │ │ ├── Command │ │ ├── CommandBusCommand.php │ │ ├── UntagPostCommand.php │ │ ├── CreatePostCommand.php │ │ ├── TagPostCommand.php │ │ ├── PostTagCountListCommand.php │ │ ├── PostCategoryCountListCommand.php │ │ ├── PostListCommand.php │ │ ├── PublishPostCommand.php │ │ ├── SchemaInitCommand.php │ │ ├── AbstractSchemaEventStoreCommand.php │ │ ├── SchemaEventStoreDropCommand.php │ │ └── SchemaEventStoreCreateCommand.php │ │ ├── DependencyInjection │ │ └── SuperAwesomeBlogExtension.php │ │ └── Resources │ │ └── config │ │ ├── read-model │ │ ├── post-tag-count.xml │ │ ├── published-post.xml │ │ └── post-category-count.xml │ │ └── model │ │ └── post.xml │ └── Blog │ └── Domain │ ├── Model │ └── Post │ │ ├── Command │ │ ├── CreatePost.php │ │ ├── Handler │ │ │ ├── UntagPostHandler.php │ │ │ ├── CreatePostHandler.php │ │ │ ├── TagPostHandler.php │ │ │ ├── PublishPostHandler.php │ │ │ └── PostHandler.php │ │ ├── TagPost.php │ │ ├── UntagPost.php │ │ └── PublishPost.php │ │ ├── PostRepository.php │ │ ├── Event │ │ ├── PostWasCreated.php │ │ ├── PostWasUntagged.php │ │ ├── PostWasTagged.php │ │ ├── PostWasCategorized.php │ │ ├── PostWasUncategorized.php │ │ └── PostWasPublished.php │ │ ├── Adapter │ │ ├── Broadway │ │ │ ├── BroadwayPostRepository.php │ │ │ └── BroadwayPostCommandHandler.php │ │ └── SuperAwesome │ │ │ └── SuperAwesomePostRepository.php │ │ └── Post.php │ └── ReadModel │ ├── PublishedPost │ ├── PublishedPostRepository.php │ ├── Adapter │ │ ├── Broadway │ │ │ ├── BroadwayPublishedPostProjector.php │ │ │ └── BroadwayPublishedPostRepository.php │ │ └── SuperAwesome │ │ │ └── Dbal │ │ │ └── DbalPublishedPostRepository.php │ ├── PublishedPostProjector.php │ └── PublishedPost.php │ ├── PostTagCount │ ├── PostTagCountRepository.php │ ├── PostTagCountProjector.php │ ├── Adapter │ │ ├── Broadway │ │ │ ├── BroadwayPostTagCountProjector.php │ │ │ └── BroadwayPostTagCountRepository.php │ │ └── SuperAwesome │ │ │ └── Redis │ │ │ └── RedisPostTagCountRepository.php │ └── PostTagCount.php │ └── PostCategoryCount │ ├── PostCategoryCountRepository.php │ ├── PostCategoryCountProjector.php │ ├── Adapter │ ├── Broadway │ │ ├── BroadwayPostCategoryCountProjector.php │ │ └── BroadwayPostCategoryCountRepository.php │ └── SuperAwesome │ │ └── Eloquent │ │ └── EloquentPostCategoryCountRepository.php │ └── PostCategoryCount.php ├── phpunit.xml.dist ├── composer.json └── README.md /app/config/routing_prod.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | SYMFONY_ENV=test 2 | SYMFONY_DEBUG=1 3 | 4 | SYMFONY__SECRET= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /app/cache/ 2 | /app/data.sqlite 3 | /app/logs/ 4 | /vendor/ 5 | /.env 6 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | SYMFONY_ENV=dev 2 | SYMFONY_DEBUG=1 3 | 4 | SYMFONY__SECRET=UNSAFE_DEFAULT 5 | 6 | SYMFONY__MONOLOG_ACTION_LEVEL=debug 7 | 8 | SYMFONY__ELASTICSEARCH_HOST=localhost 9 | SYMFONY__ELASTICSEARCH_PORT=9200 10 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Domain/Model/AppliesRecordedEvents.php: -------------------------------------------------------------------------------- 1 | id = $id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Command/Handler/UntagPostHandler.php: -------------------------------------------------------------------------------- 1 | id = $id; 20 | $this->tag = $tag; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Command/UntagPost.php: -------------------------------------------------------------------------------- 1 | id = $id; 20 | $this->tag = $tag; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/PostRepository.php: -------------------------------------------------------------------------------- 1 | id); 13 | 14 | $this->getPostRepository()->save($post); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Command/Handler/TagPostHandler.php: -------------------------------------------------------------------------------- 1 | getPostRepository()->find($command->id); 12 | $post->addTag($command->tag); 13 | 14 | $this->getPostRepository()->save($post); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/CommandBusCommand.php: -------------------------------------------------------------------------------- 1 | getContainer()->get('broadway.command_handling.command_bus'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PublishedPost/PublishedPostRepository.php: -------------------------------------------------------------------------------- 1 | getPostRepository()->find($command->id); 12 | $post->publish($command->title, $command->content, $command->category); 13 | 14 | $this->getPostRepository()->save($post); 15 | // TODO: Implement handle() method. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Command/Handler/PostHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 17 | } 18 | 19 | /** 20 | * @return PostRepository 21 | */ 22 | protected function getPostRepository() 23 | { 24 | return $this->postRepository; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Infrastructure/EventBus/SimpleEventBus.php: -------------------------------------------------------------------------------- 1 | listeners[] = $listener; 15 | } 16 | 17 | public function publish(array $events) 18 | { 19 | foreach ($events as $event) { 20 | foreach ($this->listeners as $listener) { 21 | $listener->handle($event); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | src/Artensify 13 | 14 | src/SuperAwesome/Symfony/*Bundle/Resources 15 | src/SuperAwesome/Symfony/*Bundle/Tests 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); 17 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; 18 | 19 | $kernel = new AppKernel($env, (bool)$debug); 20 | $application = new Application($kernel); 21 | $application->run($input); 22 | 23 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Command/PublishPost.php: -------------------------------------------------------------------------------- 1 | id = $id; 30 | $this->title = $title; 31 | $this->content = $content; 32 | $this->category = $category; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/PostTagCountRepository.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | } 18 | 19 | /** 20 | * @return mixed The object instance 21 | */ 22 | public static function deserialize(array $data) 23 | { 24 | return new static($data['id']); 25 | } 26 | 27 | /** 28 | * @return array 29 | */ 30 | public function serialize(): array 31 | { 32 | return [ 33 | 'id' => $this->id, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Infrastructure/EventStore/InMemoryEventStore.php: -------------------------------------------------------------------------------- 1 | ensureEventsAreSetup($id); 12 | 13 | return $this->events[$id]; 14 | } 15 | 16 | public function appendEvents($id, array $events) 17 | { 18 | $this->ensureEventsAreSetup($id); 19 | 20 | foreach ($events as $event) { 21 | $this->events[$id][] = $event; 22 | } 23 | } 24 | 25 | private function ensureEventsAreSetup($id) 26 | { 27 | if (isset($this->events[$id])) { 28 | return; 29 | } 30 | 31 | $this->events[$id] = []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/PostTagCountProjector.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 18 | } 19 | 20 | public function applyPostWasTagged(PostWasTagged $event) 21 | { 22 | $this->repository->increment($event->tag); 23 | } 24 | 25 | public function applyPostWasUntagged(PostWasUntagged $event) 26 | { 27 | $this->repository->decrement($event->tag); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostCategoryCount/PostCategoryCountProjector.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 18 | } 19 | 20 | public function applyPostWasCategorized(PostWasCategorized $event) 21 | { 22 | $this->repository->increment($event->category); 23 | } 24 | 25 | public function applyPostWasUncategorized(PostWasUncategorized $event) 26 | { 27 | $this->repository->decrement($event->category); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Event/PostWasUntagged.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->tag = $tag; 23 | } 24 | 25 | /** 26 | * @return mixed The object instance 27 | */ 28 | public static function deserialize(array $data) 29 | { 30 | return new static( 31 | $data['id'], 32 | $data['tag'] 33 | ); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function serialize() 40 | { 41 | return [ 42 | 'id' => $this->id, 43 | 'tag' => $this->tag, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Event/PostWasTagged.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->tag = $tag; 23 | } 24 | 25 | /** 26 | * @return mixed The object instance 27 | */ 28 | public static function deserialize(array $data) 29 | { 30 | return new static( 31 | $data['id'], 32 | $data['tag'] 33 | ); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function serialize(): array 40 | { 41 | return [ 42 | 'id' => $this->id, 43 | 'tag' => $this->tag, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/UntagPostCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:untag'); 14 | $this->setDescription('Untag a post.'); 15 | 16 | $this->setDefinition([ 17 | new InputArgument('id', InputArgument::REQUIRED, 'ID for the Post to be untagged'), 18 | new InputArgument('tag', InputArgument::REQUIRED, 'Tag'), 19 | ]); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output) 23 | { 24 | $id = $input->getArgument('id'); 25 | $tag = $input->getArgument('tag'); 26 | 27 | // @TODO Implement 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/CreatePostCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:create'); 15 | $this->setDescription('Create post.'); 16 | 17 | $this->setDefinition([ 18 | new InputArgument('id', InputArgument::REQUIRED, 'ID for the new Post'), 19 | ]); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output) 23 | { 24 | $id = $input->getArgument('id'); 25 | 26 | $createPost = new CreatePost($id); 27 | 28 | $this->getCommandBus()->dispatch($createPost); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/Adapter/Broadway/BroadwayPostTagCountProjector.php: -------------------------------------------------------------------------------- 1 | projector = $projector; 20 | } 21 | 22 | public function applyPostWasTagged(PostWasTagged $event) 23 | { 24 | $this->projector->applyPostWasTagged($event); 25 | } 26 | 27 | public function applyPostWasUntagged(PostWasUntagged $event) 28 | { 29 | $this->projector->applyPostWasUntagged($event); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Event/PostWasCategorized.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->category = $category; 23 | } 24 | 25 | /** 26 | * @return mixed The object instance 27 | */ 28 | public static function deserialize(array $data) 29 | { 30 | return new static( 31 | $data['id'], 32 | $data['category'] 33 | ); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function serialize(): array 40 | { 41 | return [ 42 | 'id' => $this->id, 43 | 'category' => $this->category, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Event/PostWasUncategorized.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->category = $category; 23 | } 24 | 25 | /** 26 | * @return mixed The object instance 27 | */ 28 | public static function deserialize(array $data) 29 | { 30 | return new static( 31 | $data['id'], 32 | $data['category'] 33 | ); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function serialize(): array 40 | { 41 | return [ 42 | 'id' => $this->id, 43 | 'category' => $this->category, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PublishedPost/Adapter/Broadway/BroadwayPublishedPostProjector.php: -------------------------------------------------------------------------------- 1 | projector = $projector; 20 | } 21 | 22 | public function applyPostWasCreated(PostWasCreated $event) 23 | { 24 | $this->projector->applyPostWasCreated($event); 25 | } 26 | 27 | public function applyPostWasPublished(PostWasPublished $event) 28 | { 29 | $this->projector->applyPostWasPublished($event); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dflydev/es-cqrs-broadway-workshop-one", 3 | "description": "Introduction to Event Sourcing and CQRS with Broadway", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Beau Simensen", 8 | "email": "beau@dflydev.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "SuperAwesome\\": "src/SuperAwesome" 14 | } 15 | }, 16 | "require": { 17 | "broadway/broadway": "^2", 18 | "vlucas/phpdotenv": "~1.1", 19 | "doctrine/doctrine-bundle": "~1.4", 20 | "doctrine/dbal": "~2.5", 21 | "symfony/symfony": "^2.8", 22 | "symfony/monolog-bundle": "^3.2", 23 | "predis/predis": "^1.0", 24 | "broadway/broadway-bundle": "^0.4.1", 25 | "broadway/event-store-dbal": "^0.2.1" 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "SuperAwesome\\": "tests/SuperAwesome" 30 | } 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^7.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/CreatePostHandlerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Post cannot be created.'); 16 | 17 | $id = 'my-id'; 18 | 19 | $this->scenario 20 | ->when(new CreatePost($id)) 21 | ->then([ 22 | new PostWasCreated($id), 23 | ]) 24 | ; 25 | } 26 | 27 | protected function createCommandHandler(PostRepository $postRepository) 28 | { 29 | return new CreatePostHandler($postRepository); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Infrastructure/EventBus/SpyingEventBus.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 20 | } 21 | 22 | public function subscribe(EventFromBusListener $listener) 23 | { 24 | $this->eventBus->subscribe($listener); 25 | } 26 | 27 | public function publish(array $events) 28 | { 29 | $this->eventBus->publish($events); 30 | 31 | foreach ($events as $event) { 32 | $this->recordedEvents[] = $event; 33 | } 34 | } 35 | 36 | public function getRecordedEvents() 37 | { 38 | return $this->recordedEvents; 39 | } 40 | 41 | public function clearRecordedEvents() 42 | { 43 | $this->recordedEvents = []; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Infrastructure/EventStore/SpyingEventStore.php: -------------------------------------------------------------------------------- 1 | eventStore = $eventStore; 20 | } 21 | 22 | public function getEvents($id) 23 | { 24 | return $this->eventStore->getEvents($id); 25 | } 26 | 27 | public function appendEvents($id, array $events) 28 | { 29 | $this->eventStore->appendEvents($id, $events); 30 | 31 | foreach ($events as $event) { 32 | $this->recordedEvents[] = $event; 33 | } 34 | } 35 | 36 | public function getRecordedEvents() 37 | { 38 | return $this->recordedEvents; 39 | } 40 | 41 | public function clearRecordedEvents() 42 | { 43 | $this->recordedEvents = []; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostCategoryCount/Adapter/Broadway/BroadwayPostCategoryCountProjector.php: -------------------------------------------------------------------------------- 1 | projector = $projector; 20 | } 21 | 22 | public function applyPostWasCategorized(PostWasCategorized $event) 23 | { 24 | $this->projector->applyPostWasCategorized($event); 25 | } 26 | 27 | public function applyPostWasUncategorized(PostWasUncategorized $event) 28 | { 29 | $this->projector->applyPostWasUncategorized($event); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/TagPostCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:tag'); 15 | $this->setDescription('Tag a post.'); 16 | 17 | $this->setDefinition([ 18 | new InputArgument('id', InputArgument::REQUIRED, 'ID for the Post to be tagged'), 19 | new InputArgument('tag', InputArgument::REQUIRED, 'Tag'), 20 | ]); 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $id = $input->getArgument('id'); 26 | $tag = $input->getArgument('tag'); 27 | 28 | $tagPost = new TagPost($id, $tag); 29 | 30 | $this->getCommandBus()->dispatch($tagPost); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/PostTagCountListCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:list-tag-counts'); 15 | $this->setDescription('List post tag counts.'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output) 19 | { 20 | /** @var $repository PostTagCountRepository */ 21 | $repository = $this->getContainer()->get('superawesome.blog.domain.read_model.post_tag_count.repository'); 22 | 23 | foreach ($repository->findAll() as $postTagCount) { 24 | $output->writeln(sprintf("%15s%3d", $postTagCount->getTag(), $postTagCount->getCount())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PublishedPost/PublishedPostProjector.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 18 | } 19 | 20 | public function applyPostWasCreated(PostWasCreated $event) 21 | { 22 | $publishedPost = new PublishedPost($event->id); 23 | 24 | $this->repository->save($publishedPost); 25 | } 26 | 27 | public function applyPostWasPublished(PostWasPublished $event) 28 | { 29 | $publishedPost = $this->repository->find($event->id); 30 | 31 | $publishedPost->title = $event->title; 32 | $publishedPost->content = $event->content; 33 | $publishedPost->category = $event->category; 34 | 35 | $this->repository->save($publishedPost); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/PostCategoryCountListCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:list-category-counts'); 15 | $this->setDescription('List post category counts.'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output) 19 | { 20 | /** @var $repository PostCategoryCountRepository */ 21 | $repository = $this->getContainer()->get('superawesome.blog.domain.read_model.post_category_count.repository'); 22 | 23 | foreach ($repository->findAll() as $postCategoryCount) { 24 | $output->writeln(sprintf("%15s%3d", $postCategoryCount->getCategory(), $postCategoryCount->getCount())); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Domain/Model/EventSourcing.php: -------------------------------------------------------------------------------- 1 | recordedEvents; 12 | } 13 | 14 | public function clearRecordedEvents() 15 | { 16 | $this->recordedEvents = []; 17 | } 18 | 19 | protected function recordEvent($event) 20 | { 21 | $this->handle($event); 22 | 23 | $this->recordedEvents[] = $event; 24 | } 25 | 26 | public function applyRecordedEvents(array $events) 27 | { 28 | foreach ($events as $event) { 29 | $this->handle($event); 30 | } 31 | } 32 | 33 | protected function handle($event) 34 | { 35 | $method = $this->getHandleMethod($event); 36 | 37 | if (! method_exists($this, $method)) { 38 | return; 39 | } 40 | 41 | $this->$method($event, $event); 42 | } 43 | 44 | private function getHandleMethod($event) 45 | { 46 | $classParts = explode('\\', get_class($event)); 47 | 48 | return 'apply' . end($classParts); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/PostListCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:list'); 15 | $this->setDescription('List posts.'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output) 19 | { 20 | /** @var $repository PublishedPostRepository */ 21 | $repository = $this->getContainer()->get('superawesome.blog.domain.read_model.published_post.repository'); 22 | 23 | foreach ($repository->findAll() as $publishedPost) { 24 | $output->writeln(sprintf("%-15s%-15s%-15s%15s", 25 | $publishedPost->id, 26 | $publishedPost->title, 27 | $publishedPost->content, 28 | $publishedPost->category 29 | )); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/DependencyInjection/SuperAwesomeBlogExtension.php: -------------------------------------------------------------------------------- 1 | loadModels($loader); 20 | $this->loadReadModels($loader); 21 | } 22 | 23 | public function loadModels(LoaderInterface $loader) 24 | { 25 | $loader->load('model/post.xml'); 26 | } 27 | 28 | public function loadReadModels(LoaderInterface $loader) 29 | { 30 | $loader->load('read-model/post-category-count.xml'); 31 | $loader->load('read-model/post-tag-count.xml'); 32 | $loader->load('read-model/published-post.xml'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/AbstractPostHandlerTest.php: -------------------------------------------------------------------------------- 1 | scenario = new PostHandlerScenario( 25 | $this, 26 | $eventStore, 27 | $this->createCommandHandler($postRepository) 28 | ); 29 | } 30 | 31 | abstract protected function createCommandHandler(PostRepository $postRepository); 32 | } 33 | -------------------------------------------------------------------------------- /app/config/config.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | broadway.event_store.dbal.connection: 'default' 3 | 4 | framework: 5 | secret: %secret% 6 | router: 7 | resource: "%kernel.root_dir%/config/routing_%kernel.environment%.yml" 8 | strict_requirements: %kernel.debug% 9 | templating: 10 | engines: ['twig'] 11 | profiler: 12 | enabled: %kernel.debug% 13 | 14 | monolog: 15 | handlers: 16 | main: 17 | type: fingers_crossed 18 | action_level: %monolog_action_level% 19 | handler: nested 20 | nested: 21 | type: stream 22 | path: "%kernel.logs_dir%/%kernel.environment%.log" 23 | level: debug 24 | 25 | doctrine: 26 | dbal: 27 | default_connection: default 28 | connections: 29 | default: 30 | driver: pdo_sqlite 31 | path: "%kernel.root_dir%/data.sqlite" 32 | #driver: %doctrine_dbal_db_driver% 33 | #dbname: %doctrine_dbal_db_name% 34 | #user: %doctrine_dbal_db_user% 35 | #password: %doctrine_dbal_db_password% 36 | #host: %doctrine_dbal_db_host% 37 | 38 | broadway: 39 | command_handling: 40 | logger: false 41 | event_store: "my_dbal_event_store" 42 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/Adapter/SuperAwesome/Redis/RedisPostTagCountRepository.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 18 | } 19 | 20 | public function increment($tag) 21 | { 22 | $this->redis->hincrby(static::KEY, $tag, 1); 23 | } 24 | 25 | public function decrement($tag) 26 | { 27 | $this->redis->hincrby(static::KEY, $tag, -1); 28 | } 29 | 30 | public function find($tag) 31 | { 32 | $count = $this->redis->hget(static::KEY, $tag); 33 | if (is_null($count)) { 34 | return null; 35 | } 36 | 37 | return new PostTagCount($tag, $count); 38 | } 39 | 40 | public function findAll() 41 | { 42 | $results = []; 43 | foreach ($this->redis->hgetall(static::KEY) as $tag => $count) { 44 | $results[] = new PostTagCount($tag, $count); 45 | } 46 | 47 | return $results; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Event/PostWasPublished.php: -------------------------------------------------------------------------------- 1 | id = $id; 32 | $this->title = $title; 33 | $this->content = $content; 34 | $this->category = $category; 35 | } 36 | 37 | /** 38 | * @return mixed The object instance 39 | */ 40 | public static function deserialize(array $data) 41 | { 42 | return new static( 43 | $data['id'], 44 | $data['title'], 45 | $data['content'], 46 | $data['category'] 47 | ); 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function serialize(): array 54 | { 55 | return [ 56 | 'id' => $this->id, 57 | 'title' => $this->title, 58 | 'content' => $this->content, 59 | 'category' => $this->category, 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PublishedPost/Adapter/Broadway/BroadwayPublishedPostRepository.php: -------------------------------------------------------------------------------- 1 | broadwayRepository = $broadwayRepository; 22 | } 23 | 24 | /** 25 | * @param string $id 26 | * 27 | * @return PublishedPost 28 | */ 29 | public function find($id) 30 | { 31 | return $this->broadwayRepository->find($id); 32 | } 33 | 34 | /** 35 | * @return PublishedPost[] 36 | */ 37 | public function findAll() 38 | { 39 | return $this->broadwayRepository->findAll(); 40 | } 41 | 42 | /** 43 | * @param PublishedPost $publishedPost 44 | * 45 | * @return void 46 | */ 47 | public function save(PublishedPost $publishedPost) 48 | { 49 | $this->broadwayRepository->save($publishedPost); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/PublishPostCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:blog:post:publish'); 15 | $this->setDescription('Publish post.'); 16 | 17 | $this->setDefinition([ 18 | new InputArgument('id', InputArgument::REQUIRED, 'ID for the Post to be published'), 19 | new InputArgument('title', InputArgument::REQUIRED, 'Published title'), 20 | new InputArgument('content', InputArgument::REQUIRED, 'Published content'), 21 | new InputArgument('category', InputArgument::REQUIRED, 'Published category'), 22 | ]); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $id = $input->getArgument('id'); 28 | $title = $input->getArgument('title'); 29 | $content = $input->getArgument('content'); 30 | $category = $input->getArgument('category'); 31 | 32 | $publishPost = new PublishPost($id, $title, $content, $category); 33 | 34 | $this->getCommandBus()->dispatch($publishPost); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Adapter/Broadway/BroadwayPostRepository.php: -------------------------------------------------------------------------------- 1 | eventSourcingRepository = $eventSourcingRepository; 22 | } 23 | 24 | /** 25 | * @param string $id 26 | * 27 | * @return Post 28 | */ 29 | public function find($id) 30 | { 31 | return $this->eventSourcingRepository->load($id); 32 | } 33 | 34 | /** 35 | * @return Post[] 36 | */ 37 | public function findAll() 38 | { 39 | // TODO: Implement findAll() method. 40 | } 41 | 42 | /** 43 | * @param Post $post 44 | */ 45 | public function save(Post $post) 46 | { 47 | $this->eventSourcingRepository->save($post); 48 | } 49 | 50 | protected static function getAggregateRootClass() 51 | { 52 | return Post::class; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostCategoryCount/Adapter/SuperAwesome/Eloquent/EloquentPostCategoryCountRepository.php: -------------------------------------------------------------------------------- 1 | $category, 15 | ]); 16 | } catch (\Exception $e) { 17 | return null; 18 | } 19 | } 20 | 21 | public function findAll() 22 | { 23 | return PostCategoryCount::get(); 24 | } 25 | 26 | public function increment($category) 27 | { 28 | DB::transactional(function () use ($category) { 29 | $post_category_count = PostCategoryCount::firstOrNew([ 30 | 'category' => $category, 31 | ]); 32 | 33 | $post_category_count->category_count++; 34 | $post_category_count->save(); 35 | }); 36 | } 37 | 38 | public function decrement($category) 39 | { 40 | DB::transactional(function () use ($category) { 41 | $post_category_count = PostCategoryCount::firstOrNew([ 42 | 'category' => $category, 43 | ]); 44 | 45 | $post_category_count->category_count--; 46 | $post_category_count->save(); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Domain/Model/Adapter/Broadway/BroadwayModelRepository.php: -------------------------------------------------------------------------------- 1 | id = $id; 35 | $this->title = $title; 36 | $this->content = $content; 37 | $this->category = $category; 38 | } 39 | 40 | /** 41 | * @return mixed The object instance 42 | */ 43 | public static function deserialize(array $data) 44 | { 45 | return new static( 46 | $data['id'], 47 | $data['title'], 48 | $data['content'], 49 | $data['category'] 50 | ); 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function serialize(): array 57 | { 58 | return [ 59 | 'id' => $this->id, 60 | 'title' => $this->title, 61 | 'content' => $this->content, 62 | 'category' => $this->category, 63 | ]; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getId(): string { 70 | return (string) $this->id; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/SuperAwesomePostRepository.php: -------------------------------------------------------------------------------- 1 | eventStore = $eventStore; 25 | $this->eventBus = $eventBus; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function find($id) 32 | { 33 | $recordedEvents = $this->eventStore->getEvents($id); 34 | $post = Post::instantiateForReconstitution(); 35 | $post->applyRecordedEvents($recordedEvents); 36 | 37 | return $post; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function findAll() 44 | { 45 | throw new \RuntimeException('Not implemented. (and will never be implemented!)'); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function save(Post $post) 52 | { 53 | $recordedEvents = $post->getRecordedEvents(); 54 | 55 | $this->eventStore->appendEvents( 56 | $post->getId(), 57 | $recordedEvents 58 | ); 59 | 60 | $this->eventBus->publish($recordedEvents); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/PostTagCount.php: -------------------------------------------------------------------------------- 1 | tag = $tag; 27 | $this->count = $count; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getTag() 34 | { 35 | return $this->tag; 36 | } 37 | 38 | /** 39 | * @param string $tag 40 | */ 41 | public function setTag($tag) 42 | { 43 | $this->tag = $tag; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getCount() 50 | { 51 | return $this->count; 52 | } 53 | 54 | /** 55 | * @param int $count 56 | */ 57 | public function setCount($count) 58 | { 59 | $this->count = $count; 60 | } 61 | 62 | /** 63 | * @return mixed The object instance 64 | */ 65 | public static function deserialize(array $data) 66 | { 67 | return new static($data['tag'], $data['count']); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function serialize(): array 74 | { 75 | return [ 76 | 'tag' => $this->tag, 77 | 'count' => $this->count, 78 | ]; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getId(): string { 85 | return (string) $this->id; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/PostScenario.php: -------------------------------------------------------------------------------- 1 | testCase = $testCase; 25 | } 26 | 27 | /** 28 | * @param array $givens 29 | * 30 | * @return self 31 | */ 32 | public function given(array $givens = []) 33 | { 34 | if (! $givens) { 35 | $this->post = null; 36 | 37 | return $this; 38 | } 39 | 40 | /** @var Post|RecordsEvents|AppliesRecordedEvents $post */ 41 | $post = Post::instantiateForReconstitution(); 42 | $post->applyRecordedEvents($givens); 43 | 44 | $this->post = $post; 45 | 46 | return $this; 47 | } 48 | 49 | public function when($when) 50 | { 51 | if (! is_callable($when)) { 52 | return $this; 53 | } 54 | 55 | if ($this->post) { 56 | $when($this->post); 57 | } else { 58 | $this->post = $when(null); 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | public function then(array $thens) 65 | { 66 | $this->testCase->assertEquals( 67 | $thens, 68 | $this->post->getRecordedEvents() 69 | ); 70 | 71 | $this->post->clearRecordedEvents(); 72 | 73 | return $this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/AppKernel.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), array('dev', 'test'))) { 21 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 22 | } 23 | 24 | return $bundles; 25 | } 26 | 27 | public function registerContainerConfiguration(LoaderInterface $loader) 28 | { 29 | $loader->load(__DIR__ . '/config/config.yml'); 30 | $loader->load(__DIR__ . '/config/services.yml'); 31 | 32 | $envParameters = $this->getEnvParameters(); 33 | $loader->load(function (ContainerBuilder $container) use ($envParameters) { 34 | $container->getParameterBag()->add($envParameters); 35 | }); 36 | 37 | if (in_array($this->getEnvironment(), array('dev', 'test'))) { 38 | $loader->load(function (ContainerBuilder $container) { 39 | $container->loadFromExtension('web_profiler', array( 40 | 'toolbar' => true, 41 | )); 42 | 43 | $container->loadFromExtension('framework', array( 44 | 'test' => true, 45 | )); 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/SchemaInitCommand.php: -------------------------------------------------------------------------------- 1 | setName('superawesome:schema:init') 19 | ->setDescription('Creates the read model schema') 20 | ; 21 | } 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | $connection = $this->getDoctrineConnection('default'); 29 | 30 | $error = false; 31 | try { 32 | $schemaManager = $connection->getSchemaManager(); 33 | $schema = $schemaManager->createSchema(); 34 | 35 | $table = PoorlyDesignedBroadwayDbalRepository::configureSchema($schema); 36 | if (null !== $table) { 37 | $schemaManager->createTable($table); 38 | $output->writeln('Created poorly designed dbal read model schema'); 39 | } else { 40 | $output->writeln('Poorly designed dbal read model schema already exists'); 41 | } 42 | } catch (\Exception $e) { 43 | $output->writeln('Could not create poorly designed dbal read model schema'); 44 | $output->writeln(sprintf('%s', $e->getMessage())); 45 | $error = true; 46 | } 47 | 48 | return $error ? 1 : 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostCategoryCount/PostCategoryCount.php: -------------------------------------------------------------------------------- 1 | category = $category; 27 | $this->count = $count; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getCategory() 34 | { 35 | return $this->category; 36 | } 37 | 38 | /** 39 | * @param string $category 40 | */ 41 | public function setCategory($category) 42 | { 43 | $this->category = $category; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getCount() 50 | { 51 | return $this->count; 52 | } 53 | 54 | /** 55 | * @param int $count 56 | */ 57 | public function setCount($count) 58 | { 59 | $this->count = $count; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getId(): string 66 | { 67 | return $this->category; 68 | } 69 | 70 | /** 71 | * @return mixed The object instance 72 | */ 73 | public static function deserialize(array $data) 74 | { 75 | return new static($data['category'], $data['count']); 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function serialize(): array 82 | { 83 | return [ 84 | 'category' => $this->category, 85 | 'count' => $this->count, 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/PostHandlerScenario.php: -------------------------------------------------------------------------------- 1 | testCase = $testCase; 23 | $this->eventStore = $eventStore; 24 | $this->commandHandler = $commandHandler; 25 | } 26 | 27 | /** 28 | * @param $id 29 | * 30 | * @return PostHandlerScenario 31 | */ 32 | public function withId($id) 33 | { 34 | $this->id = $id; 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @param array $events 41 | * 42 | * @return PostHandlerScenario 43 | */ 44 | public function given(array $events = []) 45 | { 46 | if (! $events) { 47 | return $this; 48 | } 49 | 50 | foreach ($events as $event) { 51 | $this->eventStore->appendEvents($this->id, [$event]); 52 | } 53 | 54 | $this->eventStore->clearRecordedEvents(); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param mixed $command 61 | * 62 | * @return PostHandlerScenario 63 | */ 64 | public function when($command) 65 | { 66 | $this->commandHandler->handle($command); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param array $events 73 | * 74 | * @return PostHandlerScenario 75 | */ 76 | public function then(array $events = []) 77 | { 78 | $this->testCase->assertEquals($events, $this->eventStore->getRecordedEvents()); 79 | $this->eventStore->clearRecordedEvents(); 80 | 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Event Sourcing and CQRS with Broadway Workshop 2 | 3 | ## Tutorial Setup 4 | 5 | Prior to arriving at the event, clone this repository so that you will not have to download it at the venue. Also, make sure to run `composer install` after you have finished cloning. If you are able to do this, you are more likely to have a successful tutorial. 6 | 7 | Keep in mind that there might be last minute changes to the repository. Check the night before the event to see if you need to pull any recent changes. 8 | 9 | To be able to do the exercises in this tutorial, you need to have the following things set-up on your machine: 10 | 11 | 1. Composer 12 | 2. PHP 7.1 (or later) 13 | 14 | It is expected that you know how to edit PHP files, and run them on the command line. All exercises will revolve around writing command line scripts. 15 | 16 | If you have questions related to any of the installation instructions below, please email me at beau@dflydev.com, or find me (simensen) on IRC's Freenode network. 17 | 18 | ### Composer 19 | 20 | Make sure you can call Composer through either just `composer`, or by calling `php /path/to/composer.phar`. I would recommend that you follow the instructions at https://getcomposer.org/download/ by running the 4 PHP commands, and then copy the installed `composer.phar` file to `/usr/local/bin/composer`: 21 | 22 | sudo cp composer.phar /usr/local/bin/composer 23 | 24 | Now verify whether Composer works by running: 25 | 26 | composer --version 27 | 28 | ### PHP 7.1, 7.2 29 | 30 | Please install the PHP package through your package manager. For Debian and Ubuntu, this package is named `php`. 31 | 32 | To verify whether your installation worked, run the following commands: 33 | 34 | php --version 35 | 36 | Running this command should output something like this (pay attention to the version numbers): 37 | 38 | PHP 7.2.4 (cli) (built: Mar 29 2018 15:32:43) ( NTS ) 39 | Copyright (c) 1997-2018 The PHP Group 40 | Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies 41 | with blackfire v1.18.2~mac-x64-non_zts72, https://blackfire.io, by SensioLabs 42 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Post.php: -------------------------------------------------------------------------------- 1 | id = $id; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getId() 34 | { 35 | return $this->id; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getTitle() 42 | { 43 | return $this->title; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getContent() 50 | { 51 | return $this->content; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getCategory() 58 | { 59 | return $this->category; 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getTags() 66 | { 67 | return array_keys($this->tags); 68 | } 69 | 70 | /** 71 | * Publish a post. 72 | * 73 | * @param $title 74 | * @param $content 75 | * @param $category 76 | */ 77 | public function publish($title, $content, $category) 78 | { 79 | $this->title = $title; 80 | $this->content = $content; 81 | $this->category = $category; 82 | } 83 | 84 | /** 85 | * Tag a post. 86 | * 87 | * @param string $tag 88 | */ 89 | public function addTag($tag) 90 | { 91 | $this->tags[$tag] = true; 92 | } 93 | 94 | /** 95 | * Untag a post. 96 | * 97 | * @param string $tag 98 | */ 99 | public function removeTag($tag) 100 | { 101 | if (isset($this->tags[$tag])) { 102 | unset($this->tags[$tag]); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostTagCount/Adapter/Broadway/BroadwayPostTagCountRepository.php: -------------------------------------------------------------------------------- 1 | broadwayRepository = $broadwayRepository; 22 | } 23 | 24 | /** 25 | * @param string $tag 26 | * 27 | * @return PostTagCount 28 | */ 29 | public function find($tag) 30 | { 31 | return $this->broadwayRepository->find($tag); 32 | } 33 | 34 | /** 35 | * @return PostTagCount[] 36 | */ 37 | public function findAll() 38 | { 39 | return $this->broadwayRepository->findAll(); 40 | } 41 | 42 | /** 43 | * @param string $tag 44 | * 45 | * @return void 46 | */ 47 | public function increment($tag) 48 | { 49 | /** @var PostTagCount $postTagCount */ 50 | $postTagCount = $this->broadwayRepository->find($tag); 51 | 52 | if ($postTagCount) { 53 | $postTagCount->setCount($postTagCount->getCount() + 1); 54 | } else { 55 | $postTagCount = new PostTagCount($tag, 1); 56 | } 57 | 58 | $this->broadwayRepository->save($postTagCount); 59 | } 60 | 61 | /** 62 | * @param string $tag 63 | * 64 | * @return void 65 | */ 66 | public function decrement($tag) 67 | { 68 | /** @var PostTagCount $postTagCount */ 69 | $postTagCount = $this->broadwayRepository->find($tag); 70 | 71 | if ($postTagCount) { 72 | $postTagCount->setCount($postTagCount->getCount() - 1); 73 | } else { 74 | $postTagCount = new PostTagCount($tag, 1); 75 | } 76 | 77 | $this->broadwayRepository->save($postTagCount); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Resources/config/read-model/post-tag-count.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | superawesome:blog:post 8 | SuperAwesome\Blog\Domain\ReadModel\PostTagCount\PostTagCount 9 | 10 | 11 | 12 | 13 | SuperAwesome\Blog\Domain\ReadModel\PostTagCount\PostTagCount 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Resources/config/read-model/published-post.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | superawesome:blog:post 8 | SuperAwesome\Blog\Domain\ReadModel\PublishedPost\PublishedPost 9 | 10 | 11 | 12 | 13 | SuperAwesome\Blog\Domain\ReadModel\PublishedPost\PublishedPost 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Resources/config/read-model/post-category-count.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | superawesome:blog:post 8 | SuperAwesome\Blog\Domain\ReadModel\PostCategoryCount\PostCategoryCount 9 | 10 | 11 | 12 | 13 | SuperAwesome\Blog\Domain\ReadModel\PostCategoryCount\PostCategoryCount 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PostCategoryCount/Adapter/Broadway/BroadwayPostCategoryCountRepository.php: -------------------------------------------------------------------------------- 1 | broadwayRepository = $broadwayRepository; 22 | } 23 | 24 | /** 25 | * @param string $category 26 | * 27 | * @return PostCategoryCount 28 | */ 29 | public function find($category) 30 | { 31 | return $this->broadwayRepository->find($category); 32 | } 33 | 34 | /** 35 | * @return PostCategoryCount[] 36 | */ 37 | public function findAll() 38 | { 39 | return $this->broadwayRepository->findAll(); 40 | } 41 | 42 | /** 43 | * @param string $tag 44 | * 45 | * @return void 46 | */ 47 | public function increment($category) 48 | { 49 | /** @var PostCategoryCount $postCategoryCount */ 50 | $postCategoryCount = $this->broadwayRepository->find($category); 51 | 52 | if ($postCategoryCount) { 53 | $postCategoryCount->setCount($postCategoryCount->getCount() + 1); 54 | } else { 55 | $postCategoryCount = new PostCategoryCount($category, 1); 56 | } 57 | 58 | $this->broadwayRepository->save($postCategoryCount); 59 | } 60 | 61 | /** 62 | * @param string $tag 63 | * 64 | * @return void 65 | */ 66 | public function decrement($category) 67 | { 68 | /** @var PostCategoryCount $postCategoryCount */ 69 | $postCategoryCount = $this->broadwayRepository->find($category); 70 | 71 | if ($postCategoryCount) { 72 | $postCategoryCount->setCount($postCategoryCount->getCount() - 1); 73 | } else { 74 | $postCategoryCount = new PostCategoryCount($category, 1); 75 | } 76 | 77 | $this->broadwayRepository->save($postCategoryCount); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/ReadModel/PublishedPost/Adapter/SuperAwesome/Dbal/DbalPublishedPostRepository.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function find($id) 28 | { 29 | $results = array_map([$this, 'unwrapData'], $this->connection->fetchAll( 30 | 'SELECT * FROM published_posts WHERE id = ?', 31 | [$id] 32 | )); 33 | 34 | return count($results) ? reset($results) : null; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function findAll() 41 | { 42 | return array_map([$this, 'unwrapData'], $this->connection->fetchAll( 43 | 'SELECT * FROM published_posts WHERE id = ?' 44 | )); 45 | } 46 | 47 | /** 48 | * @param PublishedPost $publishedPost 49 | * 50 | * @return void 51 | */ 52 | public function save(PublishedPost $publishedPost) 53 | { 54 | $data = [ 55 | 'title' => $publishedPost->title, 56 | 'content' => $publishedPost->content, 57 | 'category' => $publishedPost->category, 58 | ]; 59 | 60 | try { 61 | $this->connection->insert('published_posts', array_merge( 62 | $data, 63 | ['id' => $publishedPost->id] 64 | )); 65 | } catch (\Doctrine\DBAL\DBALException $e) { 66 | $this->connection->update('published_posts', $data, [ 67 | 'id' => $publishedPost->id, 68 | ]); 69 | } 70 | } 71 | 72 | /** 73 | * @param array $row 74 | * @return PublishedPost 75 | */ 76 | protected function unwrapData(array $row) 77 | { 78 | return new PublishedPost( 79 | $row['id'], 80 | $row['title'], 81 | $row['content'], 82 | $row['category'] 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/AbstractSchemaEventStoreCommand.php: -------------------------------------------------------------------------------- 1 | 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 | //namespace Broadway\Bundle\BroadwayBundle\Command; 13 | namespace SuperAwesome\Symfony\BlogBundle\Command; 14 | 15 | use Assert\Assertion; 16 | use Broadway\EventStore\Dbal\DBALEventStore; 17 | use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Class AbstractSchemaEventStoreCommand 24 | */ 25 | abstract class AbstractSchemaEventStoreCommand extends DoctrineCommand 26 | { 27 | /** @var \Doctrine\DBAL\Connection */ 28 | protected $connection; 29 | 30 | /** @var \Exception */ 31 | protected $exception; 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | protected function configure() 37 | { 38 | $this 39 | ->addOption( 40 | 'connection', 41 | 'c', 42 | InputOption::VALUE_OPTIONAL, 43 | 'Specifies the database connection to use.' 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function initialize(InputInterface $input, OutputInterface $output) 51 | { 52 | $databaseConnectionName = $input->getOption('connection') ?: $this->getContainer()->getParameter('broadway.event_store.dbal.connection'); 53 | Assertion::string($databaseConnectionName, 'Input option "connection" must be of type `string`.'); 54 | 55 | try { 56 | $this->connection = $this->getDoctrineConnection($databaseConnectionName); 57 | } catch (\Exception $exception) { 58 | $this->exception = $exception; 59 | } 60 | } 61 | 62 | /** 63 | * @return DBALEventStore 64 | * 65 | * @throws \RuntimeException 66 | */ 67 | protected function getEventStore() 68 | { 69 | $eventStore = $this->getContainer()->get('broadway.event_store'); 70 | 71 | if (!$eventStore instanceof DBALEventStore) { 72 | throw new \RuntimeException("'broadway.event_store' must be configured as an instance of DBALEventStore"); 73 | } 74 | 75 | return $eventStore; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/SchemaEventStoreDropCommand.php: -------------------------------------------------------------------------------- 1 | 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 | //namespace Broadway\Bundle\BroadwayBundle\Command; 13 | namespace SuperAwesome\Symfony\BlogBundle\Command; 14 | 15 | use Exception; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Drops the event store schema. 21 | */ 22 | class SchemaEventStoreDropCommand extends AbstractSchemaEventStoreCommand 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setName('broadway:event-store:schema:drop') 33 | ->setDescription('Drops the event store schema') 34 | ->setHelp( 35 | <<%command.name% command drops the schema in the default 37 | connections database: 38 | 39 | php app/console %command.name% 40 | EOT 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output) 48 | { 49 | if (!$this->connection) { 50 | $output->writeln('Could not drop Broadway event-store schema'); 51 | $output->writeln(sprintf('%s', $this->exception->getMessage())); 52 | 53 | return 1; 54 | } 55 | 56 | $error = false; 57 | try { 58 | $schemaManager = $this->connection->getSchemaManager(); 59 | $eventStore = $this->getEventStore(); 60 | 61 | $table = $eventStore->configureTable(); 62 | if ($schemaManager->tablesExist([$table->getName()])) { 63 | $schemaManager->dropTable($table->getName()); 64 | $output->writeln('Dropped Broadway event-store schema'); 65 | } else { 66 | $output->writeln('Broadway event-store schema does not exist'); 67 | } 68 | } catch (Exception $e) { 69 | $output->writeln('Could not drop Broadway event-store schema'); 70 | $output->writeln(sprintf('%s', $e->getMessage())); 71 | $error = true; 72 | } 73 | 74 | return $error ? 1 : 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Command/SchemaEventStoreCreateCommand.php: -------------------------------------------------------------------------------- 1 | 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 | //namespace Broadway\Bundle\BroadwayBundle\Command; 13 | namespace SuperAwesome\Symfony\BlogBundle\Command; 14 | 15 | use Exception; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Creates the event store schema. 21 | */ 22 | class SchemaEventStoreCreateCommand extends AbstractSchemaEventStoreCommand 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setName('broadway:event-store:schema:init') 33 | ->setDescription('Creates the event store schema') 34 | ->setHelp( 35 | <<%command.name% command creates the schema in the default 37 | connections database: 38 | 39 | php app/console %command.name% 40 | EOT 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output) 48 | { 49 | if (!$this->connection) { 50 | $output->writeln('Could not create Broadway event-store schema'); 51 | $output->writeln(sprintf('%s', $this->exception->getMessage())); 52 | 53 | return 1; 54 | } 55 | 56 | $error = false; 57 | try { 58 | $schemaManager = $this->connection->getSchemaManager(); 59 | $schema = $schemaManager->createSchema(); 60 | $eventStore = $this->getEventStore(); 61 | 62 | $table = $eventStore->configureSchema($schema); 63 | if (null !== $table) { 64 | $schemaManager->createTable($table); 65 | $output->writeln('Created Broadway event-store schema'); 66 | } else { 67 | $output->writeln('Broadway event-store schema already exists'); 68 | } 69 | } catch (Exception $e) { 70 | $output->writeln('Could not create Broadway event-store schema'); 71 | $output->writeln(sprintf('%s', $e->getMessage())); 72 | $error = true; 73 | } 74 | 75 | return $error ? 1 : 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SuperAwesome/Blog/Domain/Model/Post/Adapter/Broadway/BroadwayPostCommandHandler.php: -------------------------------------------------------------------------------- 1 | createPostHandler = $createPostHandler; 48 | $this->publishPostHandler = $publishPostHandler; 49 | $this->tagPostHandler = $tagPostHandler; 50 | $this->untagPostHandler = $untagPostHandler; 51 | } 52 | 53 | /** 54 | * @param CreatePost $command 55 | */ 56 | public function handleCreatePost(CreatePost $command) 57 | { 58 | $this->createPostHandler->handle($command); 59 | } 60 | 61 | /** 62 | * @param PublishPost $command 63 | */ 64 | public function handlePublishPost(PublishPost $command) 65 | { 66 | $this->publishPostHandler->handle($command); 67 | } 68 | 69 | /** 70 | * @param TagPost $command 71 | */ 72 | public function handleTagPost(TagPost $command) 73 | { 74 | $this->tagPostHandler->handle($command); 75 | } 76 | 77 | /** 78 | * @param UntagPost $command 79 | */ 80 | public function handleUntagPost(UntagPost $command) 81 | { 82 | $this->untagPostHandler->handle($command); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/TagPostHandlerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Post cannot be tagged.'); 19 | 20 | $id = 'my-id'; 21 | $title = 'the title'; 22 | $content = 'the content'; 23 | $category = 'draft'; 24 | 25 | $es = 'es'; 26 | $cqrs = 'cqrs'; 27 | $broadway = 'broadway'; 28 | 29 | $this->scenario 30 | ->withId($id) 31 | ->given([ 32 | new PostWasCreated($id), 33 | new PostWasCategorized($id, $category), 34 | new PostWasPublished($id, $title, $content, $category), 35 | ]) 36 | ->when(new TagPost($id, $es)) 37 | ->then([ 38 | new PostWasTagged($id, $es), 39 | ]) 40 | ->when(new TagPost($id, $cqrs)) 41 | ->then([ 42 | new PostWasTagged($id, $cqrs), 43 | ]) 44 | ->when(new TagPost($id, $broadway)) 45 | ->then([ 46 | new PostWasTagged($id, $broadway), 47 | ]) 48 | ; 49 | } 50 | 51 | /** @test */ 52 | public function it_does_not_tag_again() 53 | { 54 | $this->markTestIncomplete('Post cannot be tagged.'); 55 | 56 | $id = 'my-id'; 57 | $title = 'the title'; 58 | $content = 'the content'; 59 | $category = 'draft'; 60 | 61 | $es = 'es'; 62 | $cqrs = 'cqrs'; 63 | $broadway = 'broadway'; 64 | 65 | $this->scenario 66 | ->withId($id) 67 | ->given([ 68 | new PostWasCreated($id), 69 | new PostWasCategorized($id, $category), 70 | new PostWasPublished($id, $title, $content, $category), 71 | new PostWasTagged($id, $es), 72 | new PostWasTagged($id, $broadway), 73 | ]) 74 | ->when(new TagPost($id, $es)) 75 | ->then([ 76 | ]) 77 | ->when(new TagPost($id, $cqrs)) 78 | ->then([ 79 | new PostWasTagged($id, $cqrs), 80 | ]) 81 | ->when(new TagPost($id, $broadway)) 82 | ->then([ 83 | ]) 84 | ; 85 | } 86 | 87 | protected function createCommandHandler(PostRepository $postRepository) 88 | { 89 | return new TagPostHandler($postRepository); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/UntagPostHandlerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 20 | 21 | $id = 'my-id'; 22 | $title = 'the title'; 23 | $content = 'the content'; 24 | $category = 'draft'; 25 | 26 | $es = 'es'; 27 | $cqrs = 'cqrs'; 28 | $broadway = 'broadway'; 29 | 30 | $this->scenario 31 | ->withId($id) 32 | ->given([ 33 | new PostWasCreated($id), 34 | new PostWasCategorized($id, $category), 35 | new PostWasPublished($id, $title, $content, $category), 36 | new PostWasTagged($id, $es), 37 | new PostWasTagged($id, $cqrs), 38 | new PostWasTagged($id, $broadway), 39 | ]) 40 | ->when(new UntagPost($id, $es)) 41 | ->then([ 42 | new PostWasUntagged($id, $es), 43 | ]) 44 | ->when(new UntagPost($id, $cqrs)) 45 | ->then([ 46 | new PostWasUntagged($id, $cqrs), 47 | ]) 48 | ->when(new UntagPost($id, $broadway)) 49 | ->then([ 50 | new PostWasUntagged($id, $broadway), 51 | ]) 52 | ; 53 | } 54 | 55 | /** @test */ 56 | public function it_does_not_untag_again() 57 | { 58 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 59 | 60 | $id = 'my-id'; 61 | $title = 'the title'; 62 | $content = 'the content'; 63 | $category = 'draft'; 64 | 65 | $es = 'es'; 66 | $cqrs = 'cqrs'; 67 | $broadway = 'broadway'; 68 | 69 | $this->scenario 70 | ->withId($id) 71 | ->given([ 72 | new PostWasCreated($id), 73 | new PostWasCategorized($id, $category), 74 | new PostWasPublished($id, $title, $content, $category), 75 | new PostWasTagged($id, $es), 76 | new PostWasTagged($id, $cqrs), 77 | new PostWasTagged($id, $broadway), 78 | new PostWasUntagged($id, $es), 79 | new PostWasUntagged($id, $broadway), 80 | ]) 81 | ->when(new UntagPost($id, $es)) 82 | ->then([ 83 | ]) 84 | ->when(new UntagPost($id, $cqrs)) 85 | ->then([ 86 | new PostWasUntagged($id, $cqrs), 87 | ]) 88 | ->when(new UntagPost($id, $broadway)) 89 | ->then([ 90 | ]) 91 | ; 92 | } 93 | 94 | protected function createCommandHandler(PostRepository $postRepository) 95 | { 96 | return new UntagPostHandler($postRepository); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/SuperAwesome/Symfony/BlogBundle/Resources/config/model/post.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | SuperAwesome\Blog\Domain\Model\Post\Adapter\Broadway\BroadwayPostRepository 7 | SuperAwesome\Blog\Domain\Model\Post\Adapter\Broadway\BroadwayPostRepository 8 | SuperAwesome\Blog\Domain\Model\Post\Adapter\Broadway\BroadwayPostCommandHandler 9 | 10 | SuperAwesome\Blog\Domain\Model\Post\Command\Handler\CreatePostHandler 11 | SuperAwesome\Blog\Domain\Model\Post\Command\Handler\PublishPostHandler 12 | SuperAwesome\Blog\Domain\Model\Post\Command\Handler\TagPostHandler 13 | SuperAwesome\Blog\Domain\Model\Post\Command\Handler\UntagPostHandler 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/SuperAwesome/Common/Domain/ReadModel/Adapter/Broadway/PoorlyDesignedBroadwayDbalRepository.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 36 | $this->serializer = $serializer; 37 | $this->class = $class; 38 | } 39 | 40 | public function save(Identifiable $data) 41 | { 42 | $this->connection->delete(static::TABLE, [ 43 | 'class' => $this->class, 44 | 'id' => $data->getId(), 45 | ]); 46 | 47 | $this->connection->insert(static::TABLE, [ 48 | 'class' => $this->class, 49 | 'id' => $data->getId(), 50 | 'serialized' => json_encode($this->serializer->serialize($data)), 51 | ]); 52 | } 53 | 54 | /** 55 | * @param string $id 56 | * 57 | * @return Identifiable|null 58 | */ 59 | public function find($id) 60 | { 61 | $statement = $this->connection->prepare( 62 | sprintf('SELECT serialized FROM %s WHERE class = :class AND id = :id', static::TABLE) 63 | ); 64 | 65 | $statement->execute([ 66 | 'class' => $this->class, 67 | 'id' => $id, 68 | ]); 69 | 70 | $row = $statement->fetch(\PDO::FETCH_ASSOC); 71 | 72 | if (! $row) { 73 | return null; 74 | } 75 | 76 | return $this->serializer->deserialize(json_decode($row['serialized'], true)); 77 | } 78 | 79 | /** 80 | * @param array $fields 81 | * 82 | * @return Identifiable[] 83 | */ 84 | public function findBy(array $fields): array 85 | { 86 | throw new \RuntimeException('Not implemented.'); 87 | } 88 | 89 | /** 90 | * @return Identifiable[] 91 | */ 92 | public function findAll(): array 93 | { 94 | $statement = $this->connection->prepare( 95 | sprintf('SELECT serialized FROM %s WHERE class = :class', static::TABLE) 96 | ); 97 | 98 | $statement->execute([ 99 | 'class' => $this->class, 100 | ]); 101 | 102 | return array_map(function ($row) { 103 | return $this->serializer->deserialize(json_decode($row['serialized'], true)); 104 | }, $statement->fetchAll(\PDO::FETCH_ASSOC)); 105 | } 106 | 107 | /** 108 | * @param string $id 109 | */ 110 | public function remove($id) 111 | { 112 | $this->connection->delete(static::TABLE, [ 113 | 'class' => $this->class, 114 | 'id' => $id, 115 | ]); 116 | } 117 | 118 | /** 119 | * @return \Doctrine\DBAL\Schema\Table|null 120 | */ 121 | public static function configureSchema(Schema $schema) 122 | { 123 | if ($schema->hasTable(static::TABLE)) { 124 | return null; 125 | } 126 | 127 | return static::configureTable(); 128 | } 129 | 130 | public static function configureTable() 131 | { 132 | $schema = new Schema(); 133 | 134 | $table = $schema->createTable(static::TABLE); 135 | 136 | $table->addColumn('class', 'string'); 137 | $table->addColumn('id', 'string'); 138 | $table->addColumn('serialized', 'text'); 139 | 140 | $table->setPrimaryKey(array('class', 'id')); 141 | 142 | return $table; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/Command/Handler/PublishPostHandlerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Post is not an EventSourcedAggregateRoot.'); 19 | 20 | $id = 'my-id'; 21 | $title = 'the title'; 22 | $content = 'the content'; 23 | $category = 'draft'; 24 | 25 | $this->scenario 26 | ->withId($id) 27 | ->given([ 28 | new PostWasCreated($id), 29 | ]) 30 | ->when(new PublishPost($id, $title, $content, $category)) 31 | ->then([ 32 | new PostWasCategorized($id, $category), 33 | new PostWasPublished($id, $title, $content, $category), 34 | ]) 35 | ; 36 | } 37 | 38 | /** @test */ 39 | public function it_uncategorizes_when_publishing_with_a_different_category() 40 | { 41 | $this->markTestIncomplete('Post is not an EventSourcedAggregateRoot.'); 42 | 43 | $id = 'my-id'; 44 | $title = 'the title'; 45 | $content = 'the content'; 46 | $category = 'live'; 47 | 48 | $originalTitle = 'the original title'; 49 | $originalContent = 'the original content'; 50 | $originalCategory = 'draft'; 51 | 52 | $this->scenario 53 | ->withId($id) 54 | ->given([ 55 | new PostWasCreated($id), 56 | new PostWasCategorized($id, $originalCategory), 57 | new PostWasPublished($id, $originalTitle, $originalContent, $originalCategory), 58 | ]) 59 | ->when(new PublishPost($id, $title, $content, $category)) 60 | ->then([ 61 | new PostWasUncategorized($id, $originalCategory), 62 | new PostWasCategorized($id, $category), 63 | new PostWasPublished($id, $title, $content, $category), 64 | ]) 65 | ; 66 | } 67 | 68 | /** @test */ 69 | public function it_does_not_uncategorize_when_publishing_with_same_category() 70 | { 71 | $this->markTestIncomplete('Post is not an EventSourcedAggregateRoot.'); 72 | 73 | $id = 'my-id'; 74 | $title = 'the title'; 75 | $content = 'the content'; 76 | $category = 'draft'; 77 | 78 | $originalTitle = 'the original title'; 79 | $originalContent = 'the original content'; 80 | $originalCategory = 'draft'; 81 | 82 | $this->scenario 83 | ->withId($id) 84 | ->given([ 85 | new PostWasCreated($id), 86 | new PostWasCategorized($id, $originalCategory), 87 | new PostWasPublished($id, $originalTitle, $originalContent, $originalCategory), 88 | ]) 89 | ->when(new PublishPost($id, $title, $content, $category)) 90 | ->then([ 91 | new PostWasPublished($id, $title, $content, $category), 92 | ]) 93 | ; 94 | } 95 | 96 | /** @test */ 97 | public function it_does_not_publish_if_nothing_changed() 98 | { 99 | $this->markTestIncomplete('Post is not an EventSourcedAggregateRoot.'); 100 | 101 | $id = 'my-id'; 102 | $title = 'the title'; 103 | $content = 'the content'; 104 | $category = 'draft'; 105 | 106 | $this->scenario 107 | ->withId($id) 108 | ->given([ 109 | new PostWasCreated($id), 110 | new PostWasCategorized($id, $category), 111 | new PostWasPublished($id, $title, $content, $category), 112 | ]) 113 | ->when(new PublishPost($id, $title, $content, $category)) 114 | ->then([ 115 | ]) 116 | ; 117 | } 118 | 119 | protected function createCommandHandler(PostRepository $postRepository) 120 | { 121 | return new PublishPostHandler($postRepository); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/SuperAwesome/Blog/Domain/Model/Post/Adapter/SuperAwesome/PostTest.php: -------------------------------------------------------------------------------- 1 | scenario = new PostScenario($this); 23 | } 24 | 25 | /** @test */ 26 | public function it_can_create() 27 | { 28 | $this->markTestIncomplete('Post::create() does not exist.'); 29 | 30 | $id = 'my-id'; 31 | 32 | $this->scenario 33 | ->when(function () use ($id) { 34 | return Post::create($id); 35 | }) 36 | ->then([ 37 | new PostWasCreated($id), 38 | ]) 39 | ; 40 | } 41 | 42 | /** @test */ 43 | public function it_can_publish() 44 | { 45 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 46 | 47 | $id = 'my-id'; 48 | $title = 'the title'; 49 | $content = 'the content'; 50 | $category = 'draft'; 51 | 52 | $this->scenario 53 | ->given([ 54 | new PostWasCreated($id), 55 | ]) 56 | ->when(function (Post $post) use ($title, $content, $category) { 57 | $post->publish($title, $content, $category); 58 | }) 59 | ->then([ 60 | new PostWasCategorized($id, $category), 61 | new PostWasPublished($id, $title, $content, $category), 62 | ]) 63 | ; 64 | } 65 | 66 | /** @test */ 67 | public function it_uncategorizes_when_publishing_with_a_different_category() 68 | { 69 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 70 | 71 | $id = 'my-id'; 72 | $title = 'the title'; 73 | $content = 'the content'; 74 | $category = 'live'; 75 | 76 | $originalTitle = 'the original title'; 77 | $originalContent = 'the original content'; 78 | $originalCategory = 'draft'; 79 | 80 | $this->scenario 81 | ->given([ 82 | new PostWasCreated($id), 83 | new PostWasCategorized($id, $originalCategory), 84 | new PostWasPublished($id, $originalTitle, $originalContent, $originalCategory), 85 | ]) 86 | ->when(function (Post $post) use ($title, $content, $category) { 87 | $post->publish($title, $content, $category); 88 | }) 89 | ->then([ 90 | new PostWasUncategorized($id, $originalCategory), 91 | new PostWasCategorized($id, $category), 92 | new PostWasPublished($id, $title, $content, $category), 93 | ]) 94 | ; 95 | } 96 | 97 | /** @test */ 98 | public function it_does_not_uncategorize_when_publishing_with_same_category() 99 | { 100 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 101 | 102 | $id = 'my-id'; 103 | $title = 'the title'; 104 | $content = 'the content'; 105 | $category = 'draft'; 106 | 107 | $originalTitle = 'the original title'; 108 | $originalContent = 'the original content'; 109 | $originalCategory = 'draft'; 110 | 111 | $this->scenario 112 | ->given([ 113 | new PostWasCreated($id), 114 | new PostWasCategorized($id, $originalCategory), 115 | new PostWasPublished($id, $originalTitle, $originalContent, $originalCategory), 116 | ]) 117 | ->when(function (Post $post) use ($title, $content, $category) { 118 | $post->publish($title, $content, $category); 119 | }) 120 | ->then([ 121 | new PostWasPublished($id, $title, $content, $category), 122 | ]) 123 | ; 124 | } 125 | 126 | /** @test */ 127 | public function it_does_not_publish_if_nothing_changed() 128 | { 129 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 130 | 131 | $id = 'my-id'; 132 | $title = 'the title'; 133 | $content = 'the content'; 134 | $category = 'draft'; 135 | 136 | $this->scenario 137 | ->given([ 138 | new PostWasCreated($id), 139 | new PostWasCategorized($id, $category), 140 | new PostWasPublished($id, $title, $content, $category), 141 | ]) 142 | ->when(function (Post $post) use ($title, $content, $category) { 143 | $post->publish($title, $content, $category); 144 | }) 145 | ->then([ 146 | ]) 147 | ; 148 | } 149 | 150 | /** @test */ 151 | public function it_can_tag() 152 | { 153 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 154 | 155 | $id = 'my-id'; 156 | $title = 'the title'; 157 | $content = 'the content'; 158 | $category = 'draft'; 159 | 160 | $es = 'es'; 161 | $cqrs = 'cqrs'; 162 | $broadway = 'broadway'; 163 | 164 | $this->scenario 165 | ->given([ 166 | new PostWasCreated($id), 167 | new PostWasCategorized($id, $category), 168 | new PostWasPublished($id, $title, $content, $category), 169 | ]) 170 | ->when(function (Post $post) use ($es, $cqrs, $broadway) { 171 | $post->addTag($es); 172 | $post->addTag($cqrs); 173 | $post->addTag($broadway); 174 | }) 175 | ->then([ 176 | new PostWasTagged($id, $es), 177 | new PostWasTagged($id, $cqrs), 178 | new PostWasTagged($id, $broadway), 179 | ]) 180 | ; 181 | } 182 | 183 | /** @test */ 184 | public function it_does_not_tag_again() 185 | { 186 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 187 | 188 | $id = 'my-id'; 189 | $title = 'the title'; 190 | $content = 'the content'; 191 | $category = 'draft'; 192 | 193 | $es = 'es'; 194 | $cqrs = 'cqrs'; 195 | $broadway = 'broadway'; 196 | 197 | $this->scenario 198 | ->given([ 199 | new PostWasCreated($id), 200 | new PostWasCategorized($id, $category), 201 | new PostWasPublished($id, $title, $content, $category), 202 | new PostWasTagged($id, $es), 203 | new PostWasTagged($id, $broadway), 204 | ]) 205 | ->when(function (Post $post) use ($es, $cqrs, $broadway) { 206 | $post->addTag($es); 207 | $post->addTag($cqrs); 208 | $post->addTag($broadway); 209 | }) 210 | ->then([ 211 | new PostWasTagged($id, $cqrs), 212 | ]) 213 | ; 214 | } 215 | 216 | /** @test */ 217 | public function it_can_untag() 218 | { 219 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 220 | 221 | $id = 'my-id'; 222 | $title = 'the title'; 223 | $content = 'the content'; 224 | $category = 'draft'; 225 | 226 | $es = 'es'; 227 | $cqrs = 'cqrs'; 228 | $broadway = 'broadway'; 229 | 230 | $this->scenario 231 | ->given([ 232 | new PostWasCreated($id), 233 | new PostWasCategorized($id, $category), 234 | new PostWasPublished($id, $title, $content, $category), 235 | new PostWasTagged($id, $es), 236 | new PostWasTagged($id, $cqrs), 237 | new PostWasTagged($id, $broadway), 238 | ]) 239 | ->when(function (Post $post) use ($es, $cqrs, $broadway) { 240 | $post->removeTag($es); 241 | $post->removeTag($cqrs); 242 | $post->removeTag($broadway); 243 | }) 244 | ->then([ 245 | new PostWasUntagged($id, $es), 246 | new PostWasUntagged($id, $cqrs), 247 | new PostWasUntagged($id, $broadway), 248 | ]) 249 | ; 250 | } 251 | 252 | /** @test */ 253 | public function it_does_not_untag_again() 254 | { 255 | $this->markTestIncomplete('Post::instantiateForReconstitution does not exist.'); 256 | 257 | $id = 'my-id'; 258 | $title = 'the title'; 259 | $content = 'the content'; 260 | $category = 'draft'; 261 | 262 | $es = 'es'; 263 | $cqrs = 'cqrs'; 264 | $broadway = 'broadway'; 265 | 266 | $this->scenario 267 | ->given([ 268 | new PostWasCreated($id), 269 | new PostWasCategorized($id, $category), 270 | new PostWasPublished($id, $title, $content, $category), 271 | new PostWasTagged($id, $es), 272 | new PostWasTagged($id, $cqrs), 273 | new PostWasTagged($id, $broadway), 274 | new PostWasUntagged($id, $es), 275 | new PostWasUntagged($id, $broadway), 276 | ]) 277 | ->when(function (Post $post) use ($es, $cqrs, $broadway) { 278 | $post->removeTag($es); 279 | $post->removeTag($cqrs); 280 | $post->removeTag($broadway); 281 | }) 282 | ->then([ 283 | new PostWasUntagged($id, $cqrs), 284 | ]) 285 | ; 286 | } 287 | } 288 | --------------------------------------------------------------------------------