Slide 1

Slide 1 text

EventSourcing in PHP Olaf Gałązka

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Traditional way • CRUD • current state based

Slide 5

Slide 5 text

Current state concept • saving only state of all objects • no ways to investigate what was happening

Slide 6

Slide 6 text

Before After

Slide 7

Slide 7 text

Before After

Slide 8

Slide 8 text

Workaround? Saving history records into log

Slide 9

Slide 9 text

Good solution?

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Switch your mindset

Slide 12

Slide 12 text

Focus on actions that happen in your application

Slide 13

Slide 13 text

The domain events

Slide 14

Slide 14 text

The domain event • a fact • something that happened in the past • a state transition

Slide 15

Slide 15 text

Event = data • event keeps all information about itself • it’s the main and only source of information • think of all data needed

Slide 16

Slide 16 text

Events naming • if events are facts from the past • then use verbs • and use past tense

Slide 17

Slide 17 text

Events naming • ArticleWasPublished • ProductWasAddedToBasket • NewUserHasRegistered

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text


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

Slide 20

Slide 20 text

Storing events

Slide 21

Slide 21 text

Event storage • persistence layer for events • append only • never ever delete nor update

Slide 22

Slide 22 text

Stored event • event type • aggregate id • domain event metadata • optional useful information, e.g. date

Slide 23

Slide 23 text

Aggregates • objects that are affected by events • it’s like a document / entity

Slide 24

Slide 24 text

Post PostWasPublished PostWasUpdated Event Event Aggregate

Slide 25

Slide 25 text

interface Aggregate
 {
 /**
 * @return DomainEvent[]
 */
 public function getEvents();
 
 /**
 * @return AggregateId
 */
 public function getAggregateId();
 
 /**
 * @return Aggregate
 */
 public static function reconstituteFrom( AggregateHistory $aggregateHistory );
 }

Slide 26

Slide 26 text

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 ) {/**/}
 }

Slide 27

Slide 27 text

Product ProductWasAddedToBasket Basket What is an aggregate here? ?

Slide 28

Slide 28 text

Product ProductWasAddedToBasket Basket Basket is the affected object

Slide 29

Slide 29 text

Aggregate ID • every aggregate has defined its aggregateID • every event has defined aggregateID

Slide 30

Slide 30 text


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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Reconstituted aggregate • use reconstituted object in your operations • never save it’s state • let the garbage collector remove it

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

// 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);

Slide 38

Slide 38 text

// 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()
 );

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

// 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());

Slide 42

Slide 42 text

Dispatching events

Slide 43

Slide 43 text

When event was produced • it should be saved in the event storage • it should be dispatched • so any interested listeners could use it

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

interface DomainEventListener
 {
 /**
 * @param DomainEvent $event
 */
 public function when(DomainEvent $event);
 }

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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);

Slide 48

Slide 48 text

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;

Slide 49

Slide 49 text

Read model

Slide 50

Slide 50 text

Projecting Projecting is process of converting (or aggregating) a stream of events into a structural representation - persistent read model.

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Projections • persisted (db, files, cache) • can always be cleared • and then can be replayed from event stream

Slide 54

Slide 54 text

Projections • projections are extremely fast to load • possible minimal latency of update • eventually consistent with event storage

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

CQRS Command Query Responsibility Segregation

Slide 58

Slide 58 text

–Greg Young „You can use CQRS without event sourcing, but with event sourcing you must use CQRS.”

Slide 59

Slide 59 text

CQRS in event sourcing UI Command (write) Query (read) Event Model Event Storage Projection Storage Event Bus save() dispatch() Read Model Listeners Transaction scope

Slide 60

Slide 60 text

Snapshots • allow reconstituting object not from zero state • to avoid processing large numbers of events

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

Is event sourcing better model?

Slide 66

Slide 66 text

Event sourcing • simplifies modelling • focuses on facts and actions that happen • gives better overview

Slide 67

Slide 67 text

Better overview

Slide 68

Slide 68 text

Event sourced application is more real-life like

Slide 69

Slide 69 text

–Greg Young „A state transition is not Update table set column equals value. It’s a fact”

Slide 70

Slide 70 text

Event source is used • in banking • in finance • in insurance • in bookkeeping

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

With event sourcing you will avoid • deleting something accidentally • problems with missing data that could have been saved

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Questions?

Slide 75

Slide 75 text

Thank you! Presented sources are part of demo application: https://github.com/ulff/phpers-demo