Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Event Sourcing with 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™

Slide 3

Slide 3 text

Event Sourcing with 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!

Slide 4

Slide 4 text

Event Sourcing core principles Storing all the changes to the system, rather than just its current state. ∫

Slide 5

Slide 5 text

Event Sourcing core principles change state /

Slide 6

Slide 6 text

Event Sourcing core principles change state vs something that happened result of some event processing result of some command handling snapshot at a given time what we store in databases what you probably don’t store? usually

Slide 7

Slide 7 text

Event Sourcing core 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

Slide 8

Slide 8 text

Event Sourcing event stream Registered Vehicle Parked Vehicle … Vehicle CX897BC

Slide 9

Slide 9 text

Event Sourcing event stream(s) Registered Vehicle Parked Vehicle … Vehicle AM069GG Registered Vehicle Parked Vehicle … Vehicle CX897BC

Slide 10

Slide 10 text

Event Sourcing es in your domain Disclaimer: forget setters/reflection made by your ORM on your entities. ∫

Slide 11

Slide 11 text

Event Sourcing es in your domain Let’s build the next unicorn! parkedLife™ PRAGMATIC USE CASE

Slide 12

Slide 12 text

Event Sourcing es 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.

Slide 13

Slide 13 text

Event Sourcing es 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.

Slide 14

Slide 14 text

Event Sourcing es in your domain 1. Emit change(s) 2. Persist them in a stream 3. Reconstitute state from stream Our goal: endless thing

Slide 15

Slide 15 text

Event Sourcing es 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

Slide 16

Slide 16 text

Event Sourcing es in your domain READY?

Slide 17

Slide 17 text

Event Sourcing es 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

Slide 18

Slide 18 text

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 Sourcing es in your domain AND THEN (VERY BASIC) ES very basic local event stream very basic sourcing of stream

Slide 19

Slide 19 text

1. Emit change(s) 2. Persist them in a stream 3. Reconstitute state from stream Event Sourcing es in your domain Our goal: endless thing MISSION COMPLETE

Slide 20

Slide 20 text

Event Sourcing es 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!

Slide 21

Slide 21 text

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 Sourcing es in your domain Look ‘ma, I’m an Aggregate root! Generic logic managed by AggregateRoot

Slide 22

Slide 22 text

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 Sourcing es 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

Slide 23

Slide 23 text

Event Sourcing es in your domain We’re done with our domain! let’s talk about how to persist our events.

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Event Sourcing persistence Let’s try with one of the simplest implementations: filesystem event store.

Slide 26

Slide 26 text

Event Sourcing persistence Pretty easy from the very deep nature of events - Append only: as events happen - One file per stream (so by aggregate root)

Slide 27

Slide 27 text

Event Sourcing persistence 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

Slide 28

Slide 28 text

Event Sourcing persistence 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 IMPLEMENTATION stream name to file name association

Slide 29

Slide 29 text

Event Sourcing persistence APP.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());

Slide 30

Slide 30 text

Event Sourcing persistence APP.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());

Slide 31

Slide 31 text

Event Sourcing persistence APP.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);

Slide 32

Slide 32 text

Event Sourcing persistence APP.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);

Slide 33

Slide 33 text

Event Sourcing persistence $ 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!

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Event Sourcing projections 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?

Slide 36

Slide 36 text

Event Sourcing projections 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.

Slide 37

Slide 37 text

Event Sourcing projections 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.

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Event Sourcing projections EVENT BUS IMPLEMENTATION class 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']); } } }

Slide 40

Slide 40 text

Event Sourcing with php QUESTIONS?

Slide 41

Slide 41 text

Event Sourcing with php https://github.com/shouze/parkedLife.git MORE CODE AT: