Slide 1

Slide 1 text

Introduction to Event Sourcing and CQRS git.io/vUb0C https://github.com/dflydev/es-cqrs-broadway-workshop Beau Simensen • @beausimensen • beausimensen.com Willem-Jan Zijderveld • @willemjanz

Slide 2

Slide 2 text

Strategy is hugely important! But today we'll be looking at tactics...

Slide 3

Slide 3 text

CQRS

Slide 4

Slide 4 text

Command / Query Responsibility Segregation

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Our Model

Slide 8

Slide 8 text

class Post { /** @var string */ private $id; /** @var string */ private $title; /** @var string */ private $content; /** @var string */ private $category; /** @var bool[] */ private $tags = []; }

Slide 9

Slide 9 text

class Post { public function __construct($id) { $this->id = $id; } public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getContent() { return $this->content; } public function getCategory() { return $this->category; } public function getTags() { return array_keys($this->tags); } }

Slide 10

Slide 10 text

class Post { public function publish($title, $content, $category) { $this->title = $title; $this->content = $content; $this->category = $category; } public function addTag($tag) { $this->tags[$tag] = true; } public function removeTag($tag) { if (isset($this->tags[$tag])) { unset($this->tags[$tag]); } } }

Slide 11

Slide 11 text

interface PostRepository { public function find($id); public function findAll(); public function save($post); }

Slide 12

Slide 12 text

Assumption: This model is "Business Correct"

Slide 13

Slide 13 text

Back to reality

Slide 14

Slide 14 text

UI Requirement #1 We MUST be able to see a count of the number of posts with each category.

Slide 15

Slide 15 text

// Raw SQL SELECT COUNT(*) FROM post GROUP BY category ... or using the (No)SQL-Like language or query builder thingy for your ORM/ODM of choice

Slide 16

Slide 16 text

UI Requirement #2 We MUST be able to see a count of the number of posts with each tag.

Slide 17

Slide 17 text

// Raw SQL (maybe?) SELECT COUNT(*) FROM post_tags WHERE tag = :tag GROUP BY tag ... since tags is array-ish, this depends quite a bit on the underlying implementation...

Slide 18

Slide 18 text

// Raw SQL (maybe?) SELECT COUNT(*) FROM post_tags WHERE tag = :tag GROUP BY tag Oh, btw, did you serialize a raw array into your column? You're probably out of luck!

Slide 19

Slide 19 text

Also... where should this code go?

Slide 20

Slide 20 text

interface PostRepository { // ... public function getNumberOfPostsWithCategory($category); public function getNumberOfPostsWithTag($tag); }

Slide 21

Slide 21 text

Exposing a query builder could turn out to be a lot of work And would likely leak implementation details (Think: post_tags)

Slide 22

Slide 22 text

UI starts to influence the domain model

Slide 23

Slide 23 text

Optimize for... Read?

Slide 24

Slide 24 text

Optimize for... Write?

Slide 25

Slide 25 text

Optimize for... BOTH!

Slide 26

Slide 26 text

Introduce Read Models with a little help from events

Slide 27

Slide 27 text

Model -> ??? -> Read Model?

Slide 28

Slide 28 text

Events describe interesting things that have already happened

Slide 29

Slide 29 text

Use past tense names AccountWasCharged, PricingLevelChanged, PilotEjectedFromPlane

Slide 30

Slide 30 text

What events describe interesting things that have happened to our Post?

Slide 31

Slide 31 text

class Post { public function publish($title, $content, $category) { /** */ } public function addTag($tag) { /** */ } public function removeTag($tag) { /** */ } }

Slide 32

Slide 32 text

class Post { // PostWasPublished, PostWasCategorized, PostWasUncategorized public function publish($title, $content, $category) { /** */ } public function addTag($tag) { /** */ } public function removeTag($tag) { /** */ } }

Slide 33

Slide 33 text

class Post { // PostWasPublished, PostWasCategorized, PostWasUncategorized public function publish($title, $content, $category) { /** */ } // PostWasTagged public function addTag($tag) { /** */ } public function removeTag($tag) { /** */ } }

Slide 34

Slide 34 text

class Post { // PostWasPublished, PostWasCategorized, PostWasUncategorized public function publish($title, $content, $category) { /** */ } // PostWasTagged public function addTag($tag) { /** */ } // PostWasUntagged public function removeTag($tag) { /** */ } }

Slide 35

Slide 35 text

interface RecordsEvents { public function getRecordedEvents(); }

Slide 36

Slide 36 text

class Post implements RecordsEvents { // // Could be implemented as a base class / trait // private $recordedEvents = []; public function getRecordedEvents() { return $this->recordedEvents; } protected function recordEvent($event) { $this->recordedEvents[] = $event; } }

Slide 37

Slide 37 text

class Post { public function addTag($tag) { if (isset($this->tags[$tag])) { return; } $this->tags[$tag] = true; $this->recordEvent(new PostWasTagged( $this->id, $tag )); } }

Slide 38

Slide 38 text

class Post { public function removeTag($tag) { if (! isset($this->tags[$tag])) { return; } unset($this->tags[$tag]); $this->recordEvent(new PostWasUntagged( $this->id, $tag )); } }

Slide 39

Slide 39 text

class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->title = $title; $this->content = $content; $this->category = $category; } private uncategorizeIfCategoryChanged($category) { if ($category === $this->category || ! $this->category) { return; } $this->recordEvent(new PostWasUncategorized( $this->id, $this->category )); } }

Slide 40

Slide 40 text

class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); $this->title = $title; $this->content = $content; $this->category = $category; } private categorizeIfCatagoryChanged($category) { if ($category === $this->category) { return; } $this->recordEvent(new PostWasCategorized( $this->id, $this->category )); } }

Slide 41

Slide 41 text

Model -> Events -> ??? -> Read Model?

Slide 42

Slide 42 text

The Goal Every time an object is saved its recorded events are dispatched

Slide 43

Slide 43 text

Event Bus (... or event dispatcher or whatever)

Slide 44

Slide 44 text

Infrastructure Listener

Slide 45

Slide 45 text

use Doctrine\Common\EventSubscriber; class DoctrinePostSubscriber implements EventSubscriber { private $eventBus; public function __construct($eventBus) { $this->eventBus = $eventBus; } public function getSubscribedEvents() { return ['postPersist']; } public function postPersist(EventArgs $eventArgs) { $object = $eventArgs->getObject(); if ($object instanceof RecordsEvents) { $this->eventBus->dispatchAll($object->getRecordedEvents()); } } }

Slide 46

Slide 46 text

Post::saving(function (Post $post) use ($eventBus) { $eventBus->dispatchAll($post->getRecordedEvents()); });

Slide 47

Slide 47 text

Repository

Slide 48

Slide 48 text

class SomePostRepository implements PostRepository { private $eventBus; public function __construct(/** ... */, $eventBus) { // ... $this->eventBus = $eventBus; } public function save($post) { // ... $this->eventBus->dispatchAll($post->getRecordedEvents()); } }

Slide 49

Slide 49 text

class RecordedEventDispatchingPostRepository implements PostRepository { private $postRepository; private $eventBus; public function __construct(PostRepository $postRepository, $eventBus) { $this->postRepository = $postRepository; $this->eventBus = $eventBus; } public function find($id) { return $this->postRepository->find($id); } public function findAll() { return $this->postRepository->findAll(); } public function save($post) { $this->postRepository->save($post); $this->eventBus->dispatchAll($post->getRecordedEvents()); } }

Slide 50

Slide 50 text

Model -> Events -> Event Bus -> ??? -> Read Model?

Slide 51

Slide 51 text

Read Model

Slide 52

Slide 52 text

class PostTagCount { private $tag; private $count; public function __construct($tag, $count) { $this->tag = $tag; $this->count = $count; } public function getTag() { return $this->tag; } public function getCount() { return $this->count; } }

Slide 53

Slide 53 text

interface PostTagCountRepository { public function find($tag); public function findAll(); public function increment($tag); public function decrement($tag); }

Slide 54

Slide 54 text

class RedisPostTagCountRepository implements PostTagCountRepository { const KEY = 'post_tag_count'; private $redis; public function __construct($redis) { $this->redis = $redis; } }

Slide 55

Slide 55 text

class RedisPostTagCountRepository implements PostTagCountRepository { public function increment($tag) { $this->redis->hincrby(static::KEY, $tag, 1); } public function decrement($tag) { $this->redis->hincrby(static::KEY, $tag, -1); } }

Slide 56

Slide 56 text

class RedisPostTagCountRepository implements PostTagCountRepository { public function find($tag) { $count = $this->redis->hget(static::KEY, $tag); if (is_null($count)) { return null; } return new PostTagCount($tag, $count); } public function findAll() { $results = []; foreach ($this->redis->hgetall(static::KEY) as $tag => $count) { $results[] = new PostTagCount($tag, $count); } return $results; } }

Slide 57

Slide 57 text

class PostCategoryCount { private $category; private $count; public function __construct($category, $count) { $this->category = $category; $this->count = $count; } public function getCategory() { return $this->category; } public function getCount() { return $this->count; } }

Slide 58

Slide 58 text

interface PostCategoryCountRepository { public function find($category); public function findAll(); public function increment($category); public function decrement($category); }

Slide 59

Slide 59 text

class EloquentPostCategoryCountRepository implements PostCategoryCountRepository { public function find($category) { try { $post_category_count = PostCategoryCount::firstOrFail([ 'category' => $category, ]); return new PostCategoryCount( $category, $post_category_count->category_count ); } catch (\Exception $e) { return null; } } }

Slide 60

Slide 60 text

class EloquentPostCategoryCountRepository implements PostCategoryCountRepository { public function increment($category) { DB::transactional(function () { $post_category_count = PostCategoryCount::firstOrNew([ 'category' => $category, ]); $post_category_count->category_count++; $post_category_count->save(); }); } }

Slide 61

Slide 61 text

class EloquentPostCategoryCountRepository implements PostCategoryCountRepository { public function decrement($category) { DB::transactional(function () { $post_category_count = PostCategoryCount::firstOrNew([ 'category' => $category, ]); $post_category_count->category_count--; $post_category_count->save(); }); } }

Slide 62

Slide 62 text

Read Model is not bound in any way to Model's persistence layer (though it could be...)

Slide 63

Slide 63 text

Read Model can be optimized for speed and specific query requirements

Slide 64

Slide 64 text

Model -> Events -> Event Bus -> ??? -> Read Model!

Slide 65

Slide 65 text

Projector

Slide 66

Slide 66 text

interface Projector { public function handle($event); }

Slide 67

Slide 67 text

abstract class ConventionBasedProjector implements Projector { public function handle($event) { $method = $this->getHandleMethod($event); if (! method_exists($this, $method)) { return; } $this->$method($event, $event); } private function getHandleMethod($event) { $classParts = explode('\\', get_class($event)); return 'apply' . end($classParts); } }

Slide 68

Slide 68 text

class PostCategoryCountProjector extends ConventionBasedProjector { private $repository; public function __construct(PostCategoryCountRepository $repository) { $this->repository = $repository; } public function applyPostWasCategorized(PostWasCategorized $event) { $this->repository->increment($event->category); } public function applyPostWasUncategorized(PostWasUncategorized $event) { $this->repository->decrement($event->category); } }

Slide 69

Slide 69 text

Model -> Events -> Event Bus -> Projector -> Read Model!

Slide 70

Slide 70 text

You could stop here...

Slide 71

Slide 71 text

We've augmented state based Model persistence with event driven Read Models to account for specialized query requirements

Slide 72

Slide 72 text

But have we achieved Command / Query Segregation?

Slide 73

Slide 73 text

Not really...

Slide 74

Slide 74 text

We've created a Read Model ... but remember these getters?

Slide 75

Slide 75 text

class Post { public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getContent() { return $this->content; } public function getCategory() { return $this->category; } public function getTags() { return array_keys($this->tags); } }

Slide 76

Slide 76 text

class Post { public function getId() { return $this->id; } //public function getTitle() { return $this->title; } //public function getContent() { return $this->content; } //public function getCategory() { return $this->category; } //public function getTags() { // return array_keys($this->tags); //} }

Slide 77

Slide 77 text

?!?!?!

Slide 78

Slide 78 text

Time to introduce Yet Another Read Model

Slide 79

Slide 79 text

class PublishedPost { public $id; public $title; public $content; public $category; public function __construct($id) { $this->id = $id; } }

Slide 80

Slide 80 text

interface PublishedPostRepository { public function find($id); public function findAll(); public function save($publishedPost); }

Slide 81

Slide 81 text

class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); $this->title = $title; $this->content = $content; $this->category = $category; $this->recordEvent(new PostWasPublished( $this->id, $title, $content, $category )); } }

Slide 82

Slide 82 text

class PublishedPostProjector extends ConventionBasedProjector { private $repository; public function __construct(PublishedPostRepository $repository) { $this->repository = $repository; } public function applyPostWasPublished(PostWasPublished $event) { $publishedPost = $this->repository->find($event->id); $publishedPost->title = $event->title; $publishedPost->content = $event->content; $publishedPost->category = $event->category; $this->repository->save($publishedPost); } }

Slide 83

Slide 83 text

Why would this be problematic? class Post { public function __construct($id) { $this->id = $id; $this->recordEvent(new PostWasCreated($id)); } }

Slide 84

Slide 84 text

Every time a new Post is instantiated it would result in recording a new PostWasCreated event (not really what we are going for here...)

Slide 85

Slide 85 text

class Post { public static function create($id) { $instance = new static($id); $instance->recordEvent(new PostWasCreated($id)); return $instance; } }

Slide 86

Slide 86 text

class PublishedPostProjector extends ConventionBasedProjector { public function applyPostWasCreated(PostWasCreated $event) { $publishedPost = new PublishedPost($event->id); $this->repository->save($publishedPost); } public function applyPostWasPublished(PostWasPublished $event) { $publishedPost = $this->repository->find($event->id); $publishedPost->title = $event->title; $publishedPost->content = $event->content; $publishedPost->category = $event->category; $this->repository->save($publishedPost); } }

Slide 87

Slide 87 text

Impact on a controller? // before... Route::get('/post/{id}', function ($id) { return view('post')->withPost( $postRepository->find($id) ); }); // after... Route::get('/post/{id}', function ($id) { return view('post')->withPost( $publishedPostRepository->find($id); ); });

Slide 88

Slide 88 text

Controversial Getters for Event Sourced Aggregate Roots might not need to be purged

Slide 89

Slide 89 text

What is our stateful model doing for us? Keep in mind that Post no longer has getters!

Slide 90

Slide 90 text

Is state important here? class Post { public function addTag($tag) { if (isset($this->tags[$tag])) { return; } $this->tags[$tag] = true; $this->recordEvent(new PostWasTagged( $this->id, $tag )); } }

Slide 91

Slide 91 text

Is state important here? class Post { public function removeTag($tag) { if (! isset($this->tags[$tag])) { return; } unset($this->tags[$tag]); $this->recordEvent(new PostWasUntagged( $this->id, $tag )); } }

Slide 92

Slide 92 text

Is state important here? class Post { public function __construct($id) { $this->id = $id; } }

Slide 93

Slide 93 text

Is state important here? class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); $this->title = $title; $this->content = $content; $this->category = $category; $event = new PostWasPublished($this->id, $title, $content, $category); $this->recordEvent($event); } }

Slide 94

Slide 94 text

Is state important here? class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); //$this->title = $title; //$this->content = $content; $this->category = $category; $event = new PostWasPublished($this->id, $title, $content, $category); $this->recordEvent($event); } }

Slide 95

Slide 95 text

We have the data in the read model...

Slide 96

Slide 96 text

But the read model data should be considered volatile

Slide 97

Slide 97 text

What if we find a bug in the projections? Our source of truth would be tainted.

Slide 98

Slide 98 text

What if Redis crashes? We lose the data altogether.

Slide 99

Slide 99 text

Potential solution? What if we store all of the published events...

Slide 100

Slide 100 text

Potential solution? ... so we could replay them through the projectors if needed?

Slide 101

Slide 101 text

Potential solution? ... which would mean we could replay them through NEW projectors?

Slide 102

Slide 102 text

Potential solution? ... wouldn't that mean we should be able to rebuild the model itself from past events?

Slide 103

Slide 103 text

Model -> Events -> ??? -> Event Bus -> Projector -> Read Model!

Slide 104

Slide 104 text

Step 1 Make Post capable of handling events

Slide 105

Slide 105 text

class Post { protected function handle($event) { $method = $this->getHandleMethod($event); if (! method_exists($this, $method)) { return; } $this->$method($event); } private function getHandleMethod($event) { $classParts = explode('\\', get_class($event)); return 'apply' . end($classParts); } }

Slide 106

Slide 106 text

Step 2 Make recordEvent() handle events (ideally we'd rename this method)

Slide 107

Slide 107 text

class Post { protected function recordEvent($event) { $this->handle($event); $this->recordedEvents[] = $event; } }

Slide 108

Slide 108 text

Step 3 Move state changes into event handler methods

Slide 109

Slide 109 text

class Post { public function __construct($id) { $this->id = $id; } }

Slide 110

Slide 110 text

class Post { private function __construct() { // $this->id = $id; } private function applyPostWasCreated(PostWasCreated $event) { $this->id = $event->id; } }

Slide 111

Slide 111 text

class Post { public function addTag($tag) { if (isset($this->tags[$tag])) { return; } $this->tags[$tag] = true; $this->recordEvent(new PostWasTagged( $this->id, $tag )); } }

Slide 112

Slide 112 text

class Post { public function addTag($tag) { if (isset($this->tags[$tag])) { return; } // $this->tags[$tag] = true; $this->recordEvent(new PostWasTagged( $this->id, $tag )); } private function applyPostWasTagged(PostWasTagged $event) { $this->tags[$event->tag] = true; } }

Slide 113

Slide 113 text

class Post { public function removeTag($tag) { if (! isset($this->tags[$tag])) { return; } unset($this->tags[$tag]); $this->recordEvent(new PostWasUntagged( $this->id, $tag )); } }

Slide 114

Slide 114 text

class Post { public function removeTag($tag) { if (! isset($this->tags[$tag])) { return; } // unset($this->tags[$tag]); $this->recordEvent(new PostWasUntagged( $this->id, $tag )); } private function applyPostWasUntagged(PostWasUntagged $event) { unset($this->tags[$event->tag]); } }

Slide 115

Slide 115 text

class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); $this->title = $title; $this->content = $content; $this->category = $category; $this->recordEvent(new PostWasPublished( $this->id, $title, $content, $category )); } }

Slide 116

Slide 116 text

class Post { public function publish($title, $content, $category) { $this->uncategorizeIfCategoryChanged($category); $this->categorizeIfCatagoryChanged($category); //$this->title = $title; //$this->content = $content; //$this->category = $category; $this->recordEvent(new PostWasPublished( $this->id, $title, $content, $category )); } private function applyPostWasCategorized(PostWasCategorized $event) { $this->category = $event->category; } }

Slide 117

Slide 117 text

Step 4 Initializing State from previously recorded events

Slide 118

Slide 118 text

interface AppliesRecordedEvents { public function applyRecordedEvents(array $events); }

Slide 119

Slide 119 text

class Post implements AppliesRecordedEvents { public function applyRecordedEvents(array $events) { foreach ($events as $event) { $this->handle($event); } } }

Slide 120

Slide 120 text

Oh noes! How do we instantiate a new instance without specifying an ID?

Slide 121

Slide 121 text

class Post { private function __construct() { } }

Slide 122

Slide 122 text

class Post { private function __construct() { } public static function instantiateForReconstitution() { return new static(); } }

Slide 123

Slide 123 text

$post1 = Post::create(1); $post1->publish('hello', 'world', 'draft'); $post1->addTag('es'); $post1->addTag('cqrs'); $post1->removeTag('es'); $recordedEvents = $post1->getRecordedEvents(); // $recordedEvents = [ // new PostWasCreated(1), // new PostWasPublished(1, 'hello', 'world', 'draft'), // new PostWasTagged(1, 'es'), // new PostWasTagged(1, 'cqrs'), // new PostWasUntagged(1, 'es'), // ]; $post2 = Post::instantiateForReconstitution(); $post2->applyRecordedEvents($recordedEvents);

Slide 124

Slide 124 text

Event Store Load existing events and append new events

Slide 125

Slide 125 text

Identity An event stream should exist for each object

Slide 126

Slide 126 text

interface EventStore { /** * @param string $identity * @return array Previously recorded events */ public function load($identity); /** * @param string $identity * @param array Newly recorded events * @return void */ public function append($identity, array $events); }

Slide 127

Slide 127 text

WARNING This is an extremely over simplified event store interfaces

Slide 128

Slide 128 text

$post = Post::create('some-identity'); $post->publish('hello', 'world', 'draft'); $post->addTag('es'); $post->addTag('cqrs'); $post->removeTag('es'); $recordedEvents = $post->getRecordedEvents(); $eventStore->append( 'some-identity', $recordedEvents );

Slide 129

Slide 129 text

$recordedEvents = $eventStore->load('some-identity'); $post = Post::instantiateForReconstitution(); $post->applyRecordedEvents($recordedEvents);

Slide 130

Slide 130 text

EventStoreAndDispatchingPostRepository implements PostRepository { public function __construct($eventStore, $eventBus) { $this->eventStore = $eventStore; $this->eventBus = $eventBus; } }

Slide 131

Slide 131 text

EventStoreAndDispatchingPostRepository implements PostRepository { public function save(Post $post) { $recordedEvents = $post->getRecordedEvents(); $this->eventStore->append( $post->getId(), $recordedEvents ); $this->eventBus->dispatchAll($recordedEvents); } }

Slide 132

Slide 132 text

EventStoreAndDispatchingPostRepository implements PostRepository { public function find($id) { $recordedEvents = $this->eventStore->load($id); $post = Post::instantiateForReconstitution(); $post->applyRecordedEvents($recordedEvents); return $post; } public function findAll() { // ??? } }

Slide 133

Slide 133 text

Queries will likely be slow So operations like findAll() may be problematic depending on Event Store implementation

Slide 134

Slide 134 text

Take advantage of CQRS Queries like "find all" should be coming from your read model anyway!

Slide 135

Slide 135 text

interface PostRepository { public function find($id); public function findAll(); public function save($post); }

Slide 136

Slide 136 text

interface PostRepository { public function find($id); //public function findAll(); public function save($post); }

Slide 137

Slide 137 text

Event Sourcing!

Slide 138

Slide 138 text

Model -> Events -> Event Store -> Event Bus -> Projector -> Read Model!

Slide 139

Slide 139 text

So have we now achieved Command / Query Segregation?

Slide 140

Slide 140 text

Yes!

Slide 141

Slide 141 text

And we can still TEST it!

Slide 142

Slide 142 text

Given, When, Then.

Slide 143

Slide 143 text

$this->scenario ->given([ new PostWasCreated($id), new PostWasCategorized($id, 'news'), new PostWasPublished($id, 'title', 'content', 'news'), new PostWasTagged($id, 'event-sourcing'), new PostWasTagged($id, 'broadway'), ]) ->when(function (Post $post) { $post->addTag('cqrs'); }) ->then([ new PostWasTagged($id, 'cqrs'), ]) ;

Slide 144

Slide 144 text

class PostScenario { public function __construct(TestCase $testCase) { $this->testCase = $testCase; } }

Slide 145

Slide 145 text

class PostScenario { public function given(array $givens = []) { if (! $givens) { $this->post = null; return $this; } $post = Post::instantiateForReconstitution(); $post->applyRecordedEvents($givens); $this->post = $post; return $this; } }

Slide 146

Slide 146 text

class PostScenario { public function when($when) { if (! is_callable($when)) { return $this; } if ($this->post) { $when($this->post); } else { $this->post = $when(null); } return $this; } }

Slide 147

Slide 147 text

class PostScenario { public function then(array $thens) { $this->testCase->assertEquals( $thens, $this->post->getRecordedEvents() ); $this->post->clearRecordedEvents(); return $this; } }

Slide 148

Slide 148 text

[live code]

Slide 149

Slide 149 text

So we have now achieved Command / Query Segregation!

Slide 150

Slide 150 text

But there is another thing we can do...

Slide 151

Slide 151 text

Let's make Commands EXPLICIT in our domain

Slide 152

Slide 152 text

Controversial Not everyone thinks commands belong in the domain

Slide 153

Slide 153 text

Events represent activities that happened in the past

Slide 154

Slide 154 text

Commands represent things that should happen in the future

Slide 155

Slide 155 text

Use imperative names ChargeAccount, ChangePricingLevel, EjectPilotFromPlane

Slide 156

Slide 156 text

What do we need to know in order to be able to publish a Post? Route::put('/post/{id}', function ($request, $postRepository, $id) { $post = $postRepository->find($id); $post->publish($request->title, $request->content, $request->category); $postRepository->save($post); return view('post.published'); });

Slide 157

Slide 157 text

What name should we use for the Command to publish a Post? Route::put('/post/{id}', function ($request, $postRepository, $id) { $post = $postRepository->find($id); $post->publish($request->title, $request->content, $request->category); $postRepository->save($post); return view('post.published'); });

Slide 158

Slide 158 text

class PublishPost { public $id; public $title; public $content; public $category; public function __construct($id, $title, $content, $category) { $this->id = $id; $this->title = $title; $this->content = $content; $this->category = $category; } }

Slide 159

Slide 159 text

Command -> ??? -> Model -> Events -> Event Store -> Event Bus -> Projector -> Read Model!

Slide 160

Slide 160 text

Command Bus

Slide 161

Slide 161 text

Similar to Event Bus But used for Commands :)

Slide 162

Slide 162 text

Route::put('/post/{id}', function ($request, $commandBus, $id) { // $post = $postRepository->find($id); // $post->publish($request->title, $request->content, $request->category); // $postRepository->save($post); $commandBus->dispatch(new PublishPost( $id, $request->title, $request->content, $request->category )); return view('post.published'); });

Slide 163

Slide 163 text

Command -> Command Bus -> ??? -> Model -> Events -> Event Store -> Event Bus -> Projector -> Read Model!

Slide 164

Slide 164 text

Command Handler

Slide 165

Slide 165 text

Responsible for running the Command on the model

Slide 166

Slide 166 text

Only one Command Handler for each Command

Slide 167

Slide 167 text

interface CommandHandler { public function handle($command); }

Slide 168

Slide 168 text

abstract class ConventionBasedCommandHandler implements CommandHandler { public function handle($command) { $method = $this->getHandleMethod($command); if (! method_exists($this, $method)) { return; } $this->$method($command); } private function getHandleMethod($command) { if (! is_object($command)) { throw new CommandNotAnObjectException(); } $classParts = explode('\\', get_class($command)); return 'handle' . end($classParts); } }

Slide 169

Slide 169 text

class PublishPostHandler extends ConventionBasedCommandHandler { private $postRepository; public function __construct($postRepository) { $this->postRepository = $postRepository; } public function handlePublishPost(PublishPost $command) { $post = $this->postRepository->find($command->id); $post->publish( $command->title, $command->content, $command->category ); $this->postRepository->save($post); } }

Slide 170

Slide 170 text

Route::put('/post/{id}', function ($request, $commandBus, $id) { $commandBus->dispatch(new PublishPost( $id, $request->title, $request->content, $request->category )); return view('post.published'); }); class PublishPostHandler { public function handlePublishPost(PublishPost $command) { $post = $this->postRepository->find($command->id); $post->publish( $command->title, $command->content, $command->category ); $this->postRepository->save($post); } }

Slide 171

Slide 171 text

Command -> Command Bus -> Command Handler -> Model -> Events -> Event Store -> Event Bus -> Projector -> Read Model!

Slide 172

Slide 172 text

We can test command handlers, too!

Slide 173

Slide 173 text

Given, When, Then.

Slide 174

Slide 174 text

$this->scenario ->given([ new PostWasCreated($id), new PostWasCategorized($id, 'news'), new PostWasPublished($id, 'title', 'content', 'news'), new PostWasTagged($id, 'event-sourcing'), new PostWasTagged($id, 'broadway'), ]) ->when(new TagPost($id, 'cqrs')) ->then([ new PostWasTagged($id, 'cqrs'), ]) ;

Slide 175

Slide 175 text

class PostHandlerScenario { public function __construct( TestCase $testCase, SpyingEventStore $eventStore, $commandHandler ) { $this->testCase = $testCase; $this->eventStore = $eventStore; $this->commandHandler = $commandHandler; } }

Slide 176

Slide 176 text

class PostHandlerScenario { public function withId($id) { $this->id = $id; return $this; } }

Slide 177

Slide 177 text

class PostHandlerScenario { public function given(array $events = []) { if (! $events) { return $this; } foreach ($events as $event) { $this->eventStore->appendEvents($this->id, [$event]); } $this->eventStore->clearRecordedEvents(); return $this; } }

Slide 178

Slide 178 text

class PostHandlerScenario { public function when($command) { $this->commandHandler->handle($command); return $this; } }

Slide 179

Slide 179 text

class PostHandlerScenario { public function then(array $events = []) { $this->testCase->assertEquals( $events, $this->eventStore->getRecordedEvents() ); $this->eventStore->clearRecordedEvents(); return $this; } }

Slide 180

Slide 180 text

abstract class AbstractPostHandlerTest extends \PHPUnit_Framework_TestCase { protected $scenario; public function setUp() { $eventStore = new SpyingEventStore(new InMemoryEventStore()); $eventBus = new SimpleEventBus(); $postRepository = new SuperAwesomePostRepository($eventStore, $eventBus); $this->scenario = new PostHandlerScenario( $this, $eventStore, $this->createCommandHandler($postRepository) ); } abstract protected function createCommandHandler(PostRepository $postRepository); }

Slide 181

Slide 181 text

class CreatePostHandlerTest extends AbstractPostHandlerTest { /** @test */ public function it_can_create() { $id = 'my-id'; $this->scenario ->when(new CreatePost($id)) ->then([ new PostWasCreated($id), ]) ; } protected function createCommandHandler(PostRepository $postRepository) { return new CreatePostHandler($postRepository); } }

Slide 182

Slide 182 text

[live code]

Slide 183

Slide 183 text

No content

Slide 184

Slide 184 text

No content

Slide 185

Slide 185 text

No content

Slide 186

Slide 186 text

No content

Slide 187

Slide 187 text

Be Practical Do you need to implement this yourself?

Slide 188

Slide 188 text

Broadway github.com/broadway Infrastructure and testing helpers for creating CQRS and event sourced applications.

Slide 189

Slide 189 text

Prooph getprooph.org CQRS and EventSourcing Infrastructure for PHP

Slide 190

Slide 190 text

EventSauce eventsauce.io An event sourcing library (not framework) for PHP.

Slide 191

Slide 191 text

Event Sourcery github.com/event-sourcery A PHP CQRS/ES library with a core principle of keeping it simple

Slide 192

Slide 192 text

Thanks! @beausimensen • beausimensen.com @blackfireio • @Qandidate • @thatpodcast Beau Simensen <@beausimensen> Willem-Jan Zijderveld <@willemjanz>