$30 off During Our Annual Pro Sale. View Details »

Introduction to Event Sourcing and CQRS (Sunshine PHP 2017)

Introduction to Event Sourcing and CQRS (Sunshine PHP 2017)

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 that 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

February 02, 2017
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>
    Willem-Jan Zijderveld <@willemjanz>
    joind.in/talk/d3f0d

    View Slide

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

    View Slide

  3. My Story

    View Slide

  4. Domain-Driven Design
    and finding "purity"
    (I believe the latter has contributed greatly to my occasional grumpiness)

    View Slide

  5. I was stuck in the land of
    persisting last known state

    View Slide

  6. Event Sourcing & CQRS

    View Slide

  7. CQRS

    View Slide

  8. Command / Query
    Responsibility Segregation

    View Slide

  9. View Slide

  10. View Slide

  11. Our Model

    View Slide

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

    View Slide

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

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

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

    View Slide

  16. Assumption: This model is
    "Business Correct"

    View Slide

  17. Back to reality

    View Slide

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

    View Slide

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

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

    View Slide

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

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

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

    View Slide

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

    View Slide

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

    View Slide

  26. UI starts to
    influence
    the domain model

    View Slide

  27. Optimize for...

    View Slide

  28. Read?

    View Slide

  29. Write?

    View Slide

  30. BOTH!

    View Slide

  31. Introduce
    Read Models
    with a little help from events

    View Slide

  32. Model -> ??? -> Read Model?

    View Slide

  33. Events describe interesting things
    that have already happened

    View Slide

  34. Use past tense names
    AccountWasCharged, PricingLevelChanged, PilotEjectedFromPlane

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

  46. Model -> Events -> ??? -> Read Model?

    View Slide

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

    View Slide

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

    View Slide

  49. Infrastructure
    Listener

    View Slide

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

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

    View Slide

  52. Repository

    View Slide

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

    View Slide

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

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

    View Slide

  56. Read Model

    View Slide

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

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

    View Slide

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

    View Slide

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

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

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

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

    View Slide

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

  70. Projector

    View Slide

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

    View Slide

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

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

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

    View Slide

  75. You could stop here...

    View Slide

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

    View Slide

  77. But have we achieved
    Command / Query
    Segregation?

    View Slide

  78. Not really...

    View Slide

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

    View Slide

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

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

  82. ?!?!?!

    View Slide

  83. Time to introduce
    Yet Another Read Model

    View Slide

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

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

    View Slide

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

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

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

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

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

    View Slide

  101. But the read model data should
    be considered volatile

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  109. Step 1
    Make Post capable of handling events

    View Slide

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

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

    View Slide

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

    View Slide

  113. Step 3
    Move state changes into event handler methods

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

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

  122. Step 4
    Initializing State from previously recorded events

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

  129. Event Store
    Load existing events and append new events

    View Slide

  130. Identity
    An event stream should exist for each object

    View Slide

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

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

    View Slide

  133. $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

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  142. Event Sourcing!

    View Slide

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

    View Slide

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

    View Slide

  145. Yes!

    View Slide

  146. And we can still TEST it!

    View Slide

  147. Given, When, Then.

    View Slide

  148. $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

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

    View Slide

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

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

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

    View Slide

  153. [live code]

    View Slide

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

    View Slide

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

    View Slide

  156. Let's make Commands
    EXPLICIT
    in our domain

    View Slide

  157. Controversial
    Not everyone thinks commands belong in the domain

    View Slide

  158. Events represent activities that
    happened in the past

    View Slide

  159. Commands represent things that
    should happen in the future

    View Slide

  160. Use imperative names
    ChargeAccount, ChangePricingLevel, EjectPilotFromPlane

    View Slide

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

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

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

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

    View Slide

  165. Command Bus

    View Slide

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

    View Slide

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

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

    View Slide

  169. Command Handler

    View Slide

  170. Responsible for running the
    Command on the model

    View Slide

  171. Only one Command Handler for
    each Command

    View Slide

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

    View Slide

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

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

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

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

    View Slide

  177. We can test command handlers, too!

    View Slide

  178. Given, When, Then.

    View Slide

  179. $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

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

  187. [live code]

    View Slide

  188. View Slide

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

    View Slide

  190. Broadway
    labs.qandidate.com

    View Slide


  191. View Slide

  192. Thanks!
    git.io/vUb0C
    @sensiolabs • @Qandidate • @thatpodcast
    Beau Simensen <@beausimensen>
    Willem-Jan Zijderveld <@willemjanz>
    joind.in/talk/d3f0d

    View Slide