Event Sourcing with php

41
Event Sourcing with php #sfPot @VeryLastRoom 2016/09 Speaker: @sebastienHouze

Transcript of Event Sourcing with php

Page 1: Event Sourcing with php

Event Sourcingwith php

#sfPot @VeryLastRoom 2016/09Speaker: @sebastienHouze

Page 2: Event Sourcing with php

Event Sourcingwith php

This talk is not yet another talk to:- Convince you to use ES because its qualities.- Let you listen me or having some rest, stay

aware!- Just talk about theoretical things, we will put

ES in practice (and in php, not that usual).- Sell you a had hoc solution, because as

everything in computer engineering - it depends™

Page 3: Event Sourcing with php

Event Sourcingwith php

- core principles

- es in your domain

- projections

- persistence

main concepts definitions

use es in your root aggregate lifecycle to restore it at any state

how to adapt streams persistence in your infra

cherry on the cake!

Page 4: Event Sourcing with php

Event Sourcingcore principles

Storing all the changes to the system, rather than just its current state.∫

Page 5: Event Sourcing with php

Event Sourcingcore principles

change state/

Page 6: Event Sourcing with php

Event Sourcingcore principles

change statevs

something that happenedresult of some event processingresult of some command handlingsnapshot at a given time

what we store in databaseswhat you probably don’t store?usually

Page 7: Event Sourcing with php

Event Sourcingcore principles

a change is the result of an action on some entity / root aggregate

in an event sourced system

Changes of each root aggregate are persisted in a dedicated event stream

Page 8: Event Sourcing with php

Event Sourcingevent stream

RegisteredVehicle

ParkedVehicle …

Vehicle CX897BC

Page 9: Event Sourcing with php

Event Sourcingevent stream(s)

RegisteredVehicle

ParkedVehicle …

Vehicle AM069GG

RegisteredVehicle

ParkedVehicle …

Vehicle CX897BC

Page 10: Event Sourcing with php

Event Sourcinges in your domain

Disclaimer: forget setters/reflection made by your ORM on your entities.∫

Page 11: Event Sourcing with php

Event Sourcinges in your domain

Let’s build the next unicorn!

parkedLife™

PRAGMATIC USE CASE

Page 12: Event Sourcing with php

Event Sourcinges in your domain

Let’s start with parkedLife app, a service which offer a pretty simple way to locate where your vehicle(s) (car, truck, ...) has been parked.

Your vehicle(s) need to be registered on the service the first time with a plate number.

When you have many vehicles you own a vehicle fleet.

Page 13: Event Sourcing with php

Event Sourcinges in your domain

Let’s start with parkedLife app, a service which offer a pretty simple way to locate where your vehicle(s) (car, truck, ...) has been parked.

Your vehicle(s) need to be registered on the service the first time with a plate number.

When you have many vehicles you own a vehicle fleet.

Page 14: Event Sourcing with php

Event Sourcinges in your domain

1. Emit change(s)2. Persist them in a stream3. Reconstitute state from

stream

Our goal: endless thing

Page 15: Event Sourcing with php

Event Sourcinges in your domain

class VehicleFleet{ public function registerVehicle(string $platenumber, string $description) { $vehicle = Vehicle::register($platenumber, $this->userId); $vehicle->describe($description);

$this->vehicles[] = $vehicle;

return $vehicle; }}

FROM THE BASICS

Page 16: Event Sourcing with php

Event Sourcinges in your domain

READY?

Page 17: Event Sourcing with php

Event Sourcinges in your domain

class VehicleFleet{ public function registerVehicle(string $platenumber, string $description) { $this->whenVehicleWasRegistered(new VehicleWasRegistered($platenumber, (string)$this->userId)); $this->whenVehicleWasDescribed(new VehicleWasDescribed($platenumber, $description));

return $this->vehicleWithPlatenumber($platenumber); }

protected function whenVehicleWasRegistered($change) { $this->vehicles[] = Vehicle::register( $change->getPlatenumber(), new UserId($change->getUserId()) ); }

protected function describeVehicle(string $platenumber, string $description) { $this->whenVehicleWasDescribed(new VehicleWasDescribed($platenumber, $description)); }

public function whenVehicleWasDescribed($change) { $vehicle = $this->vehicleWithPlatenumber($change->getPlatenumber()); $vehicle->describe($change->getDescription()); }}

LET’S INTRODUCE EVENTS

event

event handler

Page 18: Event Sourcing with php

class VehicleFleet{ public function registerVehicle(string $platenumber, string $description) { $changes = [ new VehicleWasRegistered($platenumber, (string)$this->userId), new VehicleWasDescribed($platenumber, $description) ];

foreach ($changes as $change) { $handler = sprintf('when%s', implode('', array_slice(explode('\\', get_class($change)), -1))); $this->{$handler}($change); }

return $this->vehicleWithPlatenumber($platenumber); }}

Event Sourcinges in your domain

AND THEN (VERY BASIC) ES

very basic local event stream

very basic sourcing of stream

Page 19: Event Sourcing with php

1. Emit change(s)2. Persist them in a stream3. Reconstitute state from

stream

Event Sourcinges in your domain

Our goal: endless thing

MISSION

COMPLETE

Page 20: Event Sourcing with php

Event Sourcinges in your domain

Well… no we don’t really permit to reconstitute the state from some event stream from the outside of the root aggregate, let’s refine that!

Page 21: Event Sourcing with php

final class VehicleFleet extends AggregateRoot{ public function registerVehicle(string $platenumber, string $description) { $this->record(new VehicleWasRegistered($this->getAggregateId(), $platenumber)); $this->record(new VehicleWasDescribed($this->getAggregateId(), $platenumber, $description));

return $this->vehicleWithPlatenumber($platenumber); }

public function whenVehicleWasRegistered(VehicleWasRegistered $change) { $this->vehicles[] = Vehicle::register($change->getPlatenumber(), new UserId($change->getAggregateId())); }

public function describeVehicle(string $platenumber, string $description) { $this->record(new VehicleWasDescribed($this->getAggregateId(), $platenumber, $description)); }

public function whenVehicleWasDescribed(VehicleWasDescribed $change) { $vehicle = $this->vehicleWithPlatenumber($change->getPlatenumber()); $vehicle->describe($change->getDescription()); }}

Event Sourcinges in your domain

Look ‘ma, I’m an Aggregate root!

Generic logic managed by AggregateRoot

Page 22: Event Sourcing with php

abstract class AggregateRoot{ private $aggregateId;

private $recordedChanges = [];

protected function __construct(string $aggregateId)

public function getAggregateId(): string

public static function reconstituteFromHistory(\Iterator $history)

public function popRecordedChanges(): \Iterator

protected function record(Change $change)}

Event Sourcinges in your domain

Prefer (explicit) named constructor if you’re applying DDD

That simple, we’re ready to source events, from an event store for example

Page 23: Event Sourcing with php

Event Sourcinges in your domain

We’re done with our domain!let’s talk about how to persist our

events.

Page 24: Event Sourcing with php

Event Sourcingpersistence

You just have to adapt your domain, choose your infra weapon!∫

Page 25: Event Sourcing with php

Event Sourcingpersistence

Let’s try with one of the simplest implementations: filesystem event

store.

Page 26: Event Sourcing with php

Event Sourcingpersistence

Pretty easy from the very deep nature of events

- Append only: as events happen- One file per stream (so by aggregate

root)

Page 27: Event Sourcing with php

Event Sourcingpersistence

EVENT STORE INTERFACE

interface EventStore{ public function commit(Stream $eventStream);

public function fetch(StreamName $streamName): Stream;}

Advanced ES introduce at least a $version arg not covered by this talk

Page 28: Event Sourcing with php

Event Sourcingpersistence

class FilesystemEventStore implements EventStore{ public function commit(Stream $eventStream) { $filename = $this->filename($eventStream->getStreamName()); $content = ''; foreach ($eventStream->getChanges() as $change) { $content .= $this->eventSerializer->serialize($change).PHP_EOL; }

$this->fileHelper->appendSecurely($filename, $content); }

public function fetch(StreamName $streamName): Stream { $filename = $this->filename($streamName); $lines = $this->fileHelper->readIterator($this->filename($streamName)); $events = new ArrayIterator();

foreach ($lines as $serializedEvent) { $events->append($this->eventSerializer->deserialize($serializedEvent)); }

$lines = null; // immediately removes the descriptor.

return new Stream($streamName, $events); }}

EVENT STORE IMPLEMENTATIONstream name to file name association

Page 29: Event Sourcing with php

Event SourcingpersistenceAPP.PHP

use Shouze\ParkedLife\Domain\{Domain, EventSourcing, Adapters, Ports};

// 1. We start from pure domain code$userId = new Domain\UserId('shouze');$fleet = Domain\VehicleFleet::ofUser($userId);$platenumber = 'AM 069 GG';$fleet->registerVehicle($platenumber, 'My benz');$fleet->parkVehicle($platenumber, Domain\Location::fromString('4.1, 3.12'), new \DateTimeImmutable());

Page 30: Event Sourcing with php

Event SourcingpersistenceAPP.PHP

use Shouze\ParkedLife\Domain\{Domain, EventSourcing, Adapters, Ports};

// 1. We start from pure domain code$userId = new Domain\UserId('shouze');$fleet = Domain\VehicleFleet::ofUser($userId);$platenumber = 'AM 069 GG';$fleet->registerVehicle($platenumber, 'My benz');$fleet->parkVehicle($platenumber, Domain\Location::fromString('4.1, 3.12'), new \DateTimeImmutable());

// 2. We build our sourceable stream$streamName = new EventSourcing\StreamName(sprintf('vehicle_fleet-%s', $userId));$stream = new EventSourcing\Stream($streamName, $fleet->popRecordedChanges());

Page 31: Event Sourcing with php

Event SourcingpersistenceAPP.PHP

use Shouze\ParkedLife\Domain\{Domain, EventSourcing, Adapters, Ports};

// 1. We start from pure domain code$userId = new Domain\UserId('shouze');$fleet = Domain\VehicleFleet::ofUser($userId);$platenumber = 'AM 069 GG';$fleet->registerVehicle($platenumber, 'My benz');$fleet->parkVehicle($platenumber, Domain\Location::fromString('4.1, 3.12'), new \DateTimeImmutable());

// 2. We build our sourceable stream$streamName = new EventSourcing\StreamName(sprintf('vehicle_fleet-%s', $userId));$stream = new EventSourcing\Stream($streamName, $fleet->popRecordedChanges());

// 3. We adapt the domain to the infra through event sourcing$serializer = new EventSourcing\EventSerializer( new Domain\EventMapping, new Symfony\Component\Serializer\Serializer( [ new Symfony\Component\Serializer\Normalizer\PropertyNormalizer( null, new Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter ) ], [ new Symfony\Component\Serializer\Encoder\JsonEncoder ] ));$eventStore = new Adapters\FilesystemEventStore(__DIR__.'/var/eventstore', $serializer, new Ports\FileHelper);$eventStore->commit($stream);

Page 32: Event Sourcing with php

Event SourcingpersistenceAPP.PHP

use Shouze\ParkedLife\Domain\{Domain, EventSourcing, Adapters, Ports};

// 1. We start from pure domain code$userId = new Domain\UserId('shouze');$fleet = Domain\VehicleFleet::ofUser($userId);$platenumber = 'AM 069 GG';$fleet->registerVehicle($platenumber, 'My benz');$fleet->parkVehicle($platenumber, Domain\Location::fromString('4.1, 3.12'), new \DateTimeImmutable());

// 2. We build our sourceable stream$streamName = new EventSourcing\StreamName(sprintf('vehicle_fleet-%s', $userId));$stream = new EventSourcing\Stream($streamName, $fleet->popRecordedChanges());

// 3. We adapt the domain to the infra through event sourcing$serializer = …$eventStore = new Adapters\FilesystemEventStore(__DIR__.'/var/eventstore', $serializer, new Ports\FileHelper);$eventStore->commit($stream);

Page 33: Event Sourcing with php

Event Sourcingpersistence

$ docker run -w /app --rm -v $(pwd):/app -it php:zts-alpine php app.php$ docker run -w /app --rm -v $(pwd):/app -it php:zts-alpine sh -c 'find var -type f | xargs cat' {"event_name":"vehicle_was_registered.fleet.parkedlife", "data":{"user_id":"shouze","platenumber":"AM 069 GG"}}{"event_name":"vehicle_was_described.fleet.parkedlife", "data":{"user_id":"shouze","platenumber":"AM 069 GG", "description":"My benz"}}{"event_name":"vehicle_was_parked.fleet.parkedlife", "data":{"user_id":"shouze","platenumber":"AM 069 GG","latitude":4.1,"longitude":3.12,"timestamp":1474838529}}

LET’S RUN IT

YEAH, IT’S OUR FIRST TRULY PERSISTED EVENT

STREAM!

Page 34: Event Sourcing with php

Event Sourcingprojections

How to produce state representation(s) at any time from the very first event of your stream to any point of your stream.

Page 35: Event Sourcing with php

Event Sourcingprojections

Ok, we saw that actions on your system produce state (through

events)

But when the time come to read that state, did you notice that we often have use cases where we want to express it through many

representations?

Page 36: Event Sourcing with php

Event Sourcingprojections

Projection is about deriving state from the stream of events.

As we can produce any state representation from the very first emitted event, we can

produce every up to date state representation derivation.

Page 37: Event Sourcing with php

Event Sourcingprojections

Projections deserve an event bus as it permit to introduce eventual consistency (async build) of projection(s) and loose

coupling of course.

For projections of an aggregate you will (soon) need a projector.

Page 38: Event Sourcing with php

Event Sourcingprojections

So you quickly need Read Models, very simple objects far from you root aggregate.

Page 39: Event Sourcing with php

Event Sourcingprojections

EVENT BUS IMPLEMENTATIONclass InMemoryEventBus implements EventSourcing\EventBus{ public function __construct(EventSourcing\EventSerializer $eventSerializer, Symfony\Component\Serializer\Serializer $serializer) { $this->eventSerializer = $eventSerializer; $this->serializer = $serializer; $this->mapping = [ 'vehicle_was_registered.fleet.parkedlife' => 'VehicleWasRegistred', 'vehicle_was_described.fleet.parkedlife' => 'VehicleWasDescribed', 'vehicle_was_parked.fleet.parkedlife' => 'VehicleWasParked' ]; }

public function publish(EventSourcing\Change $change) { $eventNormalized = $this->eventSerializer->normalize($change); $projector = new Domain\ReadModel\VehicleFleetProjector(new Adapters\JsonProjector(__DIR__.'/var/eventstore', $this->serializer)); if (array_key_exists($eventNormalized['event_name'], $this->mapping)) { $handler = $this->mapping[$eventNormalized['event_name']]; $projector->{'project'.$handler}($eventNormalized['data']); } }}

Page 40: Event Sourcing with php

Event Sourcingwith php

QUESTIONS?

Page 41: Event Sourcing with php

Event Sourcingwith php

https://github.com/shouze/parkedLife.gitMORE CODE AT: