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 full-size slide

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

    View full-size slide

  3. Command / Query
    Responsibility Segregation

    View full-size slide

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

    View full-size slide

  5. 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 full-size slide

  6. 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 full-size slide

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

    View full-size slide

  8. Assumption: This model is
    "Business Correct"

    View full-size slide

  9. Back to reality

    View full-size slide

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

    View full-size slide

  11. // 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 full-size slide

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

    View full-size slide

  13. // 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 full-size slide

  14. // 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. UI starts to
    influence
    the domain model

    View full-size slide

  19. Optimize for...
    Read?

    View full-size slide

  20. Optimize for...
    Write?

    View full-size slide

  21. Optimize for...
    BOTH!

    View full-size slide

  22. Introduce
    Read Models
    with a little help from events

    View full-size slide

  23. Model -> ??? -> Read Model?

    View full-size slide

  24. Events describe interesting things
    that have already happened

    View full-size slide

  25. Use past tense names
    AccountWasCharged, PricingLevelChanged, PilotEjectedFromPlane

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  32. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

  37. Model -> Events -> ??? -> Read Model?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. Infrastructure
    Listener

    View full-size slide

  41. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  44. 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 full-size slide

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

    View full-size slide

  46. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

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

    View full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

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

    View full-size slide

  63. You could stop here...

    View full-size slide

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

    View full-size slide

  65. But have we achieved
    Command / Query
    Segregation?

    View full-size slide

  66. Not really...

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. Time to introduce
    Yet Another Read Model

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

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

    View full-size slide

  76. 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 full-size slide

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

    View full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

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

    View full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

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

    View full-size slide

  88. But the read model data should
    be considered volatile

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. Step 1
    Make Post capable of handling events

    View full-size slide

  97. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  100. Step 3
    Move state changes into event handler methods

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  104. 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 full-size slide

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

    View full-size slide

  106. 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 full-size slide

  107. 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 full-size slide

  108. 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 full-size slide

  109. Step 4
    Initializing State from previously recorded events

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  115. $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 full-size slide

  116. Event Store
    Load existing events and append new events

    View full-size slide

  117. Identity
    An event stream should exist for each object

    View full-size slide

  118. 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 full-size slide

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

    View full-size slide

  120. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  129. Event Sourcing!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  132. And we can still TEST it!

    View full-size slide

  133. Given, When, Then.

    View full-size slide

  134. $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 full-size slide

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

    View full-size slide

  136. 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 full-size slide

  137. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  141. Let's make Commands
    EXPLICIT
    in our domain

    View full-size slide

  142. Controversial
    Not everyone thinks commands belong in the domain

    View full-size slide

  143. Events represent activities that
    happened in the past

    View full-size slide

  144. Commands represent things that
    should happen in the future

    View full-size slide

  145. Use imperative names
    ChargeAccount, ChangePricingLevel, EjectPilotFromPlane

    View full-size slide

  146. 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 full-size slide

  147. 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 full-size slide

  148. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  151. 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 full-size slide

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

    View full-size slide

  153. Command Handler

    View full-size slide

  154. Responsible for running the
    Command on the model

    View full-size slide

  155. Only one Command Handler for
    each Command

    View full-size slide

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

    View full-size slide

  157. 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 full-size slide

  158. 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 full-size slide

  159. 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 full-size slide

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

    View full-size slide

  161. We can test command handlers, too!

    View full-size slide

  162. Given, When, Then.

    View full-size slide

  163. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

  166. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  169. 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 full-size slide

  170. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide