2. 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™
3. 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!
6. Event Sourcing
core principles
change statevs
something that happened
result of some event processingresult of some command handling
snapshot at a given time
what we store in databaseswhat you probably don’t store?
usually
7. 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
10. Event Sourcing
es in your domain
Disclaimer: forget setters/reflection made
by your ORM on your entities.∫
11. Event Sourcing
es in your domain
Let’s build the next unicorn!
parkedLife™
PRAGMATIC USE CASE
12. 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.
13. 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.
14. 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
15. 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
17. 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
18. 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
19. 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
20. 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!
21. 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->getAggregat
}
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
22. 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
23. Event Sourcing
es in your domain
We’re done with our domain!
let’s talk about how to persist our events.
27. 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
28. 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
29. Event Sourcing
persistenceAPP.PH
Puse ShouzeParkedLifeDomain{Domain, EventSourcing, Adapters, Ports};
// 1. We start from pure domain code
$userId = new DomainUserId('shouze');
$fleet = DomainVehicleFleet::ofUser($userId);
$platenumber = 'AM 069 GG';
$fleet->registerVehicle($platenumber, 'My benz');
$fleet->parkVehicle($platenumber, DomainLocation::fromString('4.1, 3.12'), new DateTimeImmutable());
30. Event Sourcing
persistenceAPP.PH
Puse ShouzeParkedLifeDomain{Domain, EventSourcing, Adapters, Ports};
// 1. We start from pure domain code
$userId = new DomainUserId('shouze');
$fleet = DomainVehicleFleet::ofUser($userId);
$platenumber = 'AM 069 GG';
$fleet->registerVehicle($platenumber, 'My benz');
$fleet->parkVehicle($platenumber, DomainLocation::fromString('4.1, 3.12'), new DateTimeImmutable());
// 2. We build our sourceable stream
$streamName = new EventSourcingStreamName(sprintf('vehicle_fleet-%s', $userId));
$stream = new EventSourcingStream($streamName, $fleet->popRecordedChanges());
31. Event Sourcing
persistenceAPP.PH
Puse ShouzeParkedLifeDomain{Domain, EventSourcing, Adapters, Ports};
// 1. We start from pure domain code
$userId = new DomainUserId('shouze');
$fleet = DomainVehicleFleet::ofUser($userId);
$platenumber = 'AM 069 GG';
$fleet->registerVehicle($platenumber, 'My benz');
$fleet->parkVehicle($platenumber, DomainLocation::fromString('4.1, 3.12'), new DateTimeImmutable());
// 2. We build our sourceable stream
$streamName = new EventSourcingStreamName(sprintf('vehicle_fleet-%s', $userId));
$stream = new EventSourcingStream($streamName, $fleet->popRecordedChanges());
// 3. We adapt the domain to the infra through event sourcing
$serializer = new EventSourcingEventSerializer(
new DomainEventMapping,
new SymfonyComponentSerializerSerializer(
[
new SymfonyComponentSerializerNormalizerPropertyNormalizer(
null,
new SymfonyComponentSerializerNameConverterCamelCaseToSnakeCaseNameConverter
)
],
[ new SymfonyComponentSerializerEncoderJsonEncoder ]
)
);
$eventStore = new AdaptersFilesystemEventStore(__DIR__.'/var/eventstore', $serializer, new
PortsFileHelper);
$eventStore->commit($stream);
32. Event Sourcing
persistenceAPP.PH
Puse ShouzeParkedLifeDomain{Domain, EventSourcing, Adapters, Ports};
// 1. We start from pure domain code
$userId = new DomainUserId('shouze');
$fleet = DomainVehicleFleet::ofUser($userId);
$platenumber = 'AM 069 GG';
$fleet->registerVehicle($platenumber, 'My benz');
$fleet->parkVehicle($platenumber, DomainLocation::fromString('4.1, 3.12'), new DateTimeImmutable());
// 2. We build our sourceable stream
$streamName = new EventSourcingStreamName(sprintf('vehicle_fleet-%s', $userId));
$stream = new EventSourcingStream($streamName, $fleet->popRecordedChanges());
// 3. We adapt the domain to the infra through event sourcing
$serializer = …
$eventStore = new AdaptersFilesystemEventStore(__DIR__.'/var/eventstore', $serializer, new
PortsFileHelper);
$eventStore->commit($stream);
33. 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!
34. 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.∫
35. 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?
36. 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.
37. 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.
This is not so common to have pragmatic talk on how to implement event sourcing with languages like php, you can find a lot of literature about implementations in Java, .Net stack (C# or event F#), and some more exotic ones about implementations in GO or Javascript.
To sum up aside of this talk, some of Event Sourcing benefits are:
Better traceability than most of your current systems implementations.
Easier debugging because event streams can be used like breakpoints and step by step like in a good debugging tool.
Very big read performance as projections are always ready & loose coupling against event streams writes.
Very bug write performance as event streams are append only.
Here we are follow the 1st part with attention and the 3 following ones will be piece of cake.
This is the only thing to remember about event sourcing. Everything else is about the way to implement that once you’ve understood what it means.
Let’s fight, you will learn that you’ve probably made the bad choice in your system since the beginning.
Ok if you store states in your system, keep reading. Otherwise you system is already event sourced I guess.
I talk about root aggregates here. It’s more a pure DDD (Domain Driven Design) concept but DDD fit very well with ES (Event Sourcing). If you don’t apply DDD principles in your project replace Aggregate Root by Entity everywhere as en Aggregate Root is an entity that just have the particularity to be the most essential entity of your bounded context. In other terms it’s the entity that create a clear boundary of one part of the system you focus on.
1 Identified Vehicle = 1 Event Stream for this Vehicle
N Identified Vehicle(s) = N Event Stream(s)
You can find out a lot of talks/posts about anemic models requesting your favorite search engine.
Actions, Subjects and everything that help understand what our app should manage is highlighted. You designate a user, as a user can have many vehicles we guess that the Vehicle Fleet will be the boundary of our app so the Root Aggregate.
It’s an early reminder, we express like a simple algorithm what we said at the beginning: « storing all the changes rather than the state ». Storing the changes and be able to restore the state from the changes.
Here’s a very basic Vehicle Fleet implementation extract that we could have coded - event sourcing concepts free. Let’s see how to apply our recipe seen on the previous slide.
Tada! Notice that this part is almost something that you can do in any of your legacy systems if you want to make an easy step by step transition to ES. It involves discussion with your domain experts to find out the events, this kind of talks are called event storming sessions.
Ok another intermediate step that hilight the recipe steps, at leeast 1, 2 (emit changes, apply them) in a very basic stream way (stream implie loops in the implementation).
Sure?
In fact we missed point 3. I guess. We should be able to reconstitute.
Ok this time it’s almost the perfect step to see the first move you can make on your own legacy code. It won’t break and you can even start to use event sourcing in your tests instead of involving heavy/difficult fixtures loading solutions (hello ORM users).
Here’s the abstract class interface inherited by our Aggregate Root. Not so complex, the main thing is reconstituteFromHistory and propRecordedChanges, this is the key of real event sourcing on our model and the only publicly exposed thing, keeping our model interface very clear and the most domain oriented possible.
Yes of course we just saw how to have transient event sourcing, nothing is persisted anywhere at the moment but we don’t have to worry about that, it’s very easy since we can pop changes applied and then reconstitute our model at any state.
That simple indeed. ES guide us to apply some of the Hexagonal Architecture or DDD principles: the infra implementation should be something we don’t have to worry at the beginning of our project, it should be easily swappable and we can start with very simple implementation that we can change when we want to deliver the final product or when we face to a big interest/load from users in our project. It won’t change the design of our domain objects at all.
I told you: we choose the one we consider the fastest/easier to implement at the beginning of the project. We will see later if we need to replace it.
It will be piece of cake to implement!
Ok commit here will effectively write in the file in our implementation. Quite clear this store as an operation and the mutual one, this way we can read or write in the right stream, designated by a name that in our implementation will drive us to a filename.
As we said! Very few lines of codes, we’re done!
Ok so we can start our minimal app demo and we will see that it will persist events in a file after they’ve been applied on our domain object.
We source events in a stream, we’re ready to write that in our store!
Ok… please go the the next slide, it’s infra code so serializer instantiation is quite boring and confusing to read.
Yes, 2 more lines and we’re done, we can write our stream, we’re safe now, our changes are not transient anymore :)
It event works for real <3
Ok looks complicated but let’s see it’s very powerful & simple.
The same events can be used to produce many data representations, like PKI/dashboards, detailed listings and so on. If as usual you persist the state, it’s very acrobatic then to produce this myriad of representations. Worse - sometimes you can’t as state don’t preserve history of changes!
Yes, from the very first emitted event, so when your marketing guy comes to you for a new PKI, you can impress him he will have the PKI not only from data produced after you’ve coded & deployed this PKI feature, but from the origins of the system, pretty cool no?
You can choose to implement your way, I just tell you here that you probably soon need to implement that.
I won’t show you read models here, you can go by yourself in the github repository linked at the end of the talk to see what they looks like. You only need to remember that a read model as nothing in common with your root aggregate. ES fit well by nature with another concept named CQRS (Command Query Responsibility Segregation). In other terms, the entity you use to trigger actions in your system is the Write (command) part of your system, and the Read Model the read (query) part.
Here’s a very basic event bus implementation example. It’s not async here but it’s easy to understand the main responsibility of this kind of bus.
Ok in fact in this ES talk you’ve seen some SOLID, DDD & CQRS principles applied too. You can embrace all of this principles - or not - in your system implementation. SOLID is not really an option and probably the first thing to check in your own apps. ES is easy to apply, even partially to make a transition.
Do you have to use ES in your project? How can you know if it’s useful for you? The main answer is: are you still ok in your system with the fact to store the state rather than the changes? If yes then don’t implement ES on this project.
Code shown in this talk can vary a little from the repository.