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

Event Sourcing

Event Sourcing

A pragmatic talk about ES principles and how to implement in php.

Available code at https://github.com/shouze/parkedLife

Sébastien HOUZÉ

September 28, 2016
Tweet

Other Decks in Programming

Transcript

  1. 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™
  2. 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!
  3. Event Sourcing core principles Storing all the changes to the

    system, rather than just its current state. ∫
  4. 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
  5. 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
  6. Event Sourcing event stream(s) Registered Vehicle Parked Vehicle … Vehicle

    AM069GG Registered Vehicle Parked Vehicle … Vehicle CX897BC
  7. Event Sourcing es in your domain Let’s build the next

    unicorn! parkedLife™ PRAGMATIC USE CASE
  8. 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.
  9. 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.
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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!
  16. 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
  17. 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
  18. Event Sourcing es in your domain We’re done with our

    domain! let’s talk about how to persist our events.
  19. Event Sourcing persistence Let’s try with one of the simplest

    implementations: filesystem event store.
  20. 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)
  21. 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
  22. 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
  23. 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());
  24. 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());
  25. 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);
  26. 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);
  27. 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!
  28. 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. ∫
  29. 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?
  30. 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.
  31. 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.
  32. Event Sourcing projections So you quickly need Read Models, very

    simple objects far from you root aggregate.
  33. 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']); } } }