event sourcing with php

Post on 16-Apr-2017

159 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Event Sourcingwith php

#sfPot @VeryLastRoom 2016/09Speaker: @sebastienHouze

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™

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!

Event Sourcingcore principles

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

Event Sourcingcore principles

change state/

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

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

Event Sourcingevent stream

RegisteredVehicle

ParkedVehicle …

Vehicle CX897BC

Event Sourcingevent stream(s)

RegisteredVehicle

ParkedVehicle …

Vehicle AM069GG

RegisteredVehicle

ParkedVehicle …

Vehicle CX897BC

Event Sourcinges in your domain

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

Event Sourcinges in your domain

Let’s build the next unicorn!

parkedLife™

PRAGMATIC USE CASE

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.

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.

Event Sourcinges in your domain

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

stream

Our goal: endless thing

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

Event Sourcinges in your domain

READY?

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

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

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

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!

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

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

Event Sourcinges in your domain

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

events.

Event Sourcingpersistence

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

Event Sourcingpersistence

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

store.

Event Sourcingpersistence

Pretty easy from the very deep nature of events

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

root)

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

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

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

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

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

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

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!

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.

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?

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.

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.

Event Sourcingprojections

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

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

Event Sourcingwith php

QUESTIONS?

Event Sourcingwith php

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

top related