Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduction to Event Sourcing and CQRS (php[tek] 2018)

Introduction to Event Sourcing and CQRS (php[tek] 2018)

Have you heard about Event Sourcing and wondered what it is all about? Have you looked into it and wondered what sort of sorcery is going on behind the scenes that makes this magical technology work? Are you convinced you cannot possibly move your existing applications to be event sourced? Take a step back and learn how Event Sourcing can be applied to a simple database-backed object model with little to no fuss. From there, see how you can start adding read models and begin to see how Event Sourcing and CQRS (Command Query Responsibility Segregation) go hand in hand!

Beau Simensen

May 30, 2018
Tweet

More Decks by Beau Simensen

Other Decks in Programming

Transcript

  1. 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

    View Slide

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

    View Slide

  3. CQRS

    View Slide

  4. Command / Query
    Responsibility Segregation

    View Slide

  5. View Slide

  6. View Slide

  7. Our Model

    View Slide

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

    View Slide

  9. 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);
    }
    }

    View Slide

  10. 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]);
    }
    }
    }

    View Slide

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

    View Slide

  12. Assumption: This model is
    "Business Correct"

    View Slide

  13. Back to reality

    View Slide

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

    View Slide

  15. // 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

    View Slide

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

    View Slide

  17. // 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...

    View Slide

  18. // 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!

    View Slide

  19. Also... where should
    this code go?

    View Slide

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

    View Slide

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

    View Slide

  22. UI starts to
    influence
    the domain model

    View Slide

  23. Optimize for...
    Read?

    View Slide

  24. Optimize for...
    Write?

    View Slide

  25. Optimize for...
    BOTH!

    View Slide

  26. Introduce
    Read Models
    with a little help from events

    View Slide

  27. Model -> ??? -> Read Model?

    View Slide

  28. Events describe interesting things
    that have already happened

    View Slide

  29. Use past tense names
    AccountWasCharged, PricingLevelChanged, PilotEjectedFromPlane

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. interface RecordsEvents {
    public function getRecordedEvents();
    }

    View Slide

  36. 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;
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  39. 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
    ));
    }
    }

    View Slide

  40. 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
    ));
    }
    }

    View Slide

  41. Model -> Events -> ??? -> Read Model?

    View Slide

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

    View Slide

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

    View Slide

  44. Infrastructure
    Listener

    View Slide

  45. 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());
    }
    }
    }

    View Slide

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

    View Slide

  47. Repository

    View Slide

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

    View Slide

  49. 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());
    }
    }

    View Slide

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

    View Slide

  51. Read Model

    View Slide

  52. 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; }
    }

    View Slide

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

    View Slide

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

    View Slide

  55. 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);
    }
    }

    View Slide

  56. 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;
    }
    }

    View Slide

  57. 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; }
    }

    View Slide

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

    View Slide

  59. 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;
    }
    }
    }

    View Slide

  60. 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();
    });
    }
    }

    View Slide

  61. 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();
    });
    }
    }

    View Slide

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

    View Slide

  63. Read Model can be optimized for speed
    and specific query requirements

    View Slide

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

    View Slide

  65. Projector

    View Slide

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

    View Slide

  67. 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);
    }
    }

    View Slide

  68. 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);
    }
    }

    View Slide

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

    View Slide

  70. You could stop here...

    View Slide

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

    View Slide

  72. But have we achieved
    Command / Query
    Segregation?

    View Slide

  73. Not really...

    View Slide

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

    View Slide

  75. 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);
    }
    }

    View Slide

  76. 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);
    //}
    }

    View Slide

  77. ?!?!?!

    View Slide

  78. Time to introduce
    Yet Another Read Model

    View Slide

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

    View Slide

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

    View Slide

  81. 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
    ));
    }
    }

    View Slide

  82. 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);
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  86. 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);
    }
    }

    View Slide

  87. 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);
    );
    });

    View Slide

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

    View Slide

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

    View Slide

  90. 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
    ));
    }
    }

    View Slide

  91. 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
    ));
    }
    }

    View Slide

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

    View Slide

  93. 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);
    }
    }

    View Slide

  94. 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);
    }
    }

    View Slide

  95. We have the data in
    the read model...

    View Slide

  96. But the read model data should
    be considered volatile

    View Slide

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

    View Slide

  98. What if Redis crashes?
    We lose the data altogether.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  104. Step 1
    Make Post capable of handling events

    View Slide

  105. 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);
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  108. Step 3
    Move state changes into event handler methods

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  112. 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;
    }
    }

    View Slide

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

    View Slide

  114. 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]);
    }
    }

    View Slide

  115. 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
    ));
    }
    }

    View Slide

  116. 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;
    }
    }

    View Slide

  117. Step 4
    Initializing State from previously recorded events

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  121. class Post {
    private function __construct() { }
    }

    View Slide

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

    View Slide

  123. $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);

    View Slide

  124. Event Store
    Load existing events and append new events

    View Slide

  125. Identity
    An event stream should exist for each object

    View Slide

  126. 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);
    }

    View Slide

  127. WARNING
    This is an extremely over simplified event store interfaces

    View Slide

  128. $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
    );

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  137. Event Sourcing!

    View Slide

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

    View Slide

  139. So have we now achieved
    Command / Query
    Segregation?

    View Slide

  140. Yes!

    View Slide

  141. And we can still TEST it!

    View Slide

  142. Given, When, Then.

    View Slide

  143. $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'),
    ])
    ;

    View Slide

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

    View Slide

  145. 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;
    }
    }

    View Slide

  146. 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;
    }
    }

    View Slide

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

    View Slide

  148. [live code]

    View Slide

  149. So we have now achieved
    Command / Query
    Segregation!

    View Slide

  150. But there is another
    thing we can do...

    View Slide

  151. Let's make Commands
    EXPLICIT
    in our domain

    View Slide

  152. Controversial
    Not everyone thinks commands belong in the domain

    View Slide

  153. Events represent activities that
    happened in the past

    View Slide

  154. Commands represent things that
    should happen in the future

    View Slide

  155. Use imperative names
    ChargeAccount, ChangePricingLevel, EjectPilotFromPlane

    View Slide

  156. 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');
    });

    View Slide

  157. 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');
    });

    View Slide

  158. 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;
    }
    }

    View Slide

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

    View Slide

  160. Command Bus

    View Slide

  161. Similar to Event Bus
    But used for Commands :)

    View Slide

  162. 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');
    });

    View Slide

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

    View Slide

  164. Command Handler

    View Slide

  165. Responsible for running the
    Command on the model

    View Slide

  166. Only one Command Handler for
    each Command

    View Slide

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

    View Slide

  168. 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);
    }
    }

    View Slide

  169. 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);
    }
    }

    View Slide

  170. 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);
    }
    }

    View Slide

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

    View Slide

  172. We can test command handlers, too!

    View Slide

  173. Given, When, Then.

    View Slide

  174. $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'),
    ])
    ;

    View Slide

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

    View Slide

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

    View Slide

  177. 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;
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  180. 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);
    }

    View Slide

  181. 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);
    }
    }

    View Slide

  182. [live code]

    View Slide

  183. View Slide

  184. View Slide

  185. View Slide

  186. View Slide

  187. Be Practical
    Do you need to implement this yourself?

    View Slide

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

    View Slide

  189. Prooph
    getprooph.org
    CQRS and EventSourcing Infrastructure for PHP

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide