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

Event Sourcing in PHP

Event Sourcing in PHP

Presenting basics of Event Sourcing on local PHPers meetup in Gdańsk, Poland.

Olaf Gałązka

February 22, 2016
Tweet

More Decks by Olaf Gałązka

Other Decks in Programming

Transcript

  1. Current state concept • saving only state of all objects

    • no ways to investigate what was happening
  2. The domain event • a fact • something that happened

    in the past • a state transition
  3. Event = data • event keeps all information about itself

    • it’s the main and only source of information • think of all data needed
  4. Events naming • if events are facts from the past

    • then use verbs • and use past tense
  5. 
 class PostWasPublished implements DomainEvent
 {
 private $postId;
 
 private

    $title;
 
 private $content;
 
 private $publishingDate;
 
 public function __construct(
 PostId $postId,
 $title,
 $content,
 \DateTime $publishingDate
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 $this->publishingDate = $publishingDate;
 }
 
 // ...
 }
  6. Stored event • event type • aggregate id • domain

    event metadata • optional useful information, e.g. date
  7. interface Aggregate
 {
 /**
 * @return DomainEvent[]
 */
 public function

    getEvents();
 
 /**
 * @return AggregateId
 */
 public function getAggregateId();
 
 /**
 * @return Aggregate
 */
 public static function reconstituteFrom( AggregateHistory $aggregateHistory );
 }
  8. class Post implements Aggregate
 {
 use EventSourced;
 
 private $postId;


    private $title;
 private $content;
 private $publishingDate;
 
 public function __construct(PostId $postId) {/**/}
 
 // getters/setters… 
 public function getAggregateId() {/**/}
 public function update($title, $content) {/**/}
 private function applyPostWasPublished(PostWasPublished $event) {/**/}
 private function applyPostWasUpdated(PostWasUpdated $event) {/**/}
 public static function create($title, $content) {/**/}
 public static function reconstituteFrom( AggregateHistory $postAggregateHistory ) {/**/}
 }
  9. 
 class PostWasPublished implements DomainEvent
 {
 private $postId;
 
 private

    $title;
 
 private $content;
 
 private $publishingDate;
 
 public function __construct(
 PostId $postId,
 $title,
 $content,
 \DateTime $publishingDate
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 $this->publishingDate = $publishingDate;
 }
 
 // ...
 }
  10. class PostWasUpdated implements DomainEvent
 {
 private $postId;
 
 private $title;


    
 private $content;
 
 public function __construct(
 PostId $postId,
 $title,
 $content
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 }
 
 // ...
 }
  11. Reconstituting aggregates • get aggregate history from the event storage

    • aggregate history is an ordered set of events affecting particular aggregate • reconstitute aggregate’s state: ‣ create new instance of the aggregate class ‣ replay events from aggregate history on it
  12. class Post implements Aggregate
 {
 public function __construct(PostId $postId)
 {


    $this->postId = $postId;
 } 
 
 // ...
 
 public static function reconstituteFrom(PostAggregateHistory $aggregateHistory)
 {
 $post = new self($aggregateHistory->getAggregateId());
 
 foreach ($aggregateHistory->getEvents() as $event) {
 $applyMethod = explode('\\', get_class($event));
 $applyMethod = 'apply' . end($applyMethod);
 $post->$applyMethod($event);
 }
 
 return $post;
 }
 }
  13. class Post implements Aggregate
 {
 // ...
 
 private function

    applyPostWasPublished(PostWasPublished $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 $this->setPublishingDate($event->getPublishingDate());
 }
 
 private function applyPostWasUpdated(PostWasUpdated $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 }
 
 // ... 
 }
  14. Reconstituted aggregate • use reconstituted object in your operations •

    never save it’s state • let the garbage collector remove it
  15. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 }
  16. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
  17. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $post->update(
 $command->getTitle(),
 $command->getContent()
 );
  18. class Post implements Aggregate
 {
 use EventSourced;
 
 // ...


    public function update($title, $content)
 {
 $this->recordThat($event = new PostWasUpdated( $this->getAggregateId(), $title, $content ));
 $this->apply($event);
 }
 
 private function applyPostWasUpdated(PostWasUpdated $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 }
 // ...
 }
  19. trait EventSourced
 {
 protected $events = [];
 
 public function

    getEvents()
 {
 return $this->events;
 }
 
 protected function recordThat(DomainEvent $event)
 {
 $this->events[] = $event;
 }
 
 private function apply(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'apply' . end($method);
 $this->$method($event);
 }
 
 // ...
 }
  20. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post->getEvents());
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post->getEvents());
  21. When event was produced • it should be saved in

    the event storage • it should be dispatched • so any interested listeners could use it
  22. class EventBus
 {
 private $listeners = [];
 
 public function

    registerListener(
 DomainEventListener $domainEventListener
 )
 {
 $this->listeners[] = $domainEventListener;
 }
 
 public function dispatch(array $events)
 {
 foreach ($events as $event) {
 foreach ($this->listeners as $listener) {
 $listener->when($event);
 }
 }
 }
 }
  23. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 }
  24. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } $eventBus->registerListener($this);
  25. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } $this->projectionStorage = $projectionStorage;
  26. Projecting Projecting is process of converting (or aggregating) a stream

    of events into a structural representation - persistent read model.
  27. Projections • data stored in the way it can be

    easily used for reading purposes • very simple structure • updated on every event affecting any of its data
  28. class PostListProjection implements Projection
 {
 const PROJECTION_NAME = 'post-list';
 private

    $postId;
 public $title;
 public $publishingDate;
 
 public function __construct(PostId $postId, $title, \DateTime $publishingDate)
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->publishingDate = $publishingDate;
 }
 
 public function getProjectionName()
 {
 return self::PROJECTION_NAME;
 }
 
 public function getAggregateId()
 {
 return $this->postId;
 }
 // ...
 }
  29. Projections • persisted (db, files, cache) • can always be

    cleared • and then can be replayed from event stream
  30. Projections • projections are extremely fast to load • possible

    minimal latency of update • eventually consistent with event storage
  31. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
  32. class PostListListener 
 extends AbstractDomainEventListener 
 implements DomainEventListener
 {
 public

    function onPostWasPublished(PostWasPublished $event)
 {
 $postListProjection = new PostListProjection(
 $event->getAggregateId(),
 $event->getTitle(),
 $event->getPublishingDate()
 );
 $this->projectionStorage->save($postListProjection);
 }
 
 public function onPostWasUpdated(PostWasUpdated $event)
 {
 $postListProjection = $this->projectionStorage->findById(
 'post-list',
 $event->getAggregateId()
 );
 $postListProjection->title = $event->getTitle();
 $postListProjection->content = $event->getContent();
 $this->projectionStorage->save($postListProjection);
 }
 }
  33. –Greg Young „You can use CQRS without event sourcing, but

    with event sourcing you must use CQRS.”
  34. CQRS in event sourcing UI Command (write) Query (read) Event

    Model Event Storage Projection Storage Event Bus save() dispatch() Read Model Listeners Transaction scope
  35. Snapshots • allow reconstituting object not from zero state •

    to avoid processing large numbers of events
  36. 1 2 n n+1 n+2 n+3 n+4 n+5 n+6 n+7

    … Reconstituting Snapshots
  37. 1 2 n n+1 n+2 n+3 n+4 n+5 n+6 n+7

    … snapshot Reconstituting Snapshots
  38. Event sourcing • simplifies modelling • focuses on facts and

    actions that happen • gives better overview
  39. –Greg Young „A state transition is not Update table set

    column equals value. It’s a fact”
  40. Event source is used • in banking • in finance

    • in insurance • in bookkeeping
  41. With event sourcing you can • go back to any

    point of time • analyze and compare data from any periods • you can delete and replay any read model data
  42. With event sourcing you will avoid • deleting something accidentally

    • problems with missing data that could have been saved
  43. TL;DR Event sourcing principles • do not store state of

    objects • store only facts that happened (events) • reconstitute state of aggregates from event storage • dispatch events to update projections • use projections for reading