CQRS and Event Sourcing
in a Symfony application
Samuel Roze
Software Enginner @ Inviqa
4 twitter.com/samuelroze
4 github.com/sroze
4 sroze.io
The heart of software is its
ability to solve domain-
related problems for its
user
1
Eric Evans
Command Query
Responsibility Segregation
CQRS & Event Sourcing
How are we going to build that?
1. Our domain
2. Repository and persistence
3. Message buses
4. Automation via "services"
5. Projections
Our domain
A deployment
1. Build Docker images
2. Display the progress
3. Send a notification
An event
interface DeploymentEvent
{
public function getDeploymentUuid() : UuidInterface;
}
Event capability
trait EventsCapability
{
private $events = [];
protected function raise(DeploymentEvent $event)
{
$this->events[] = $event;
}
public function eraseEvents() : void
{
$this->events = [];
}
public function raisedEvents() : array
{
return $this->events;
}
}
Creating the object from events
final class Deployment
{
use RaiseEventsCapability;
private function __construct()
{
}
public static function fromEvents(array $events)
{
$deployment = new self();
foreach ($events as $event) {
$deployment->apply($event);
}
return $deployment;
}
}
Building the object state
final class Deployment
{
private $uuid;
// ...
private function apply(DeploymentEvent $event)
{
if ($event instanceof DeploymentCreated) {
$this->uuid = $event->getUuid();
}
}
}
You know... testing!
Scenario:
When I create a deployment
Then a deployment should be created
You know... testing!
Scenario: A deployment need to have at least one image
When I create a deployment with 0 image
Then the deployment should not be valid
Scenario: Deployment with 1 image
When I create a deployment with 1 image
Then a deployment should be created
@When I create a deployment with :number image
public function iCreateADeploymentWithImage($count)
{
try {
$this->deployment = Deployment::create(
Uuid::uuid4(),
array_fill(0, $count, 'image')
);
} catch (Throwable $e) {
$this->exception = $e;
}
}
@Then the deployment should not be valid
public function theDeploymentShouldNotBeValid()
{
if (!$this->exception instanceof InvalidArgumentException) {
throw new RuntimeException(
'The exception found, if any, is not matching'
);
}
}
@Then a deployment should be created
public function aDeploymentShouldBeCreated()
{
$events = $this->deployment->raisedEvents();
$matchingEvents = array_filter($events, function(DeploymentEvent $event) {
return $event instanceof DeploymentCreated;
});
if (count($matchingEvents) === 0) {
throw new RuntimeException('No deployment created found');
}
}
Create... from the beginning!
final class Deployment
{
// ...
public static function create(Uuid $uuid, array $images)
{
if (count($images) == 0) {
throw new InvalidArgumentException('What do you deploy then?');
}
$createdEvent = new DeploymentCreated($uuid, $images);
$deployment = self::fromEvents([$createdEvent]);
$deployment->raise($createdEvent);
return $deployment;
}
}
DeploymentCreated event
final class DeploymentCreated implements DeploymentEvent
{
public function __construct(UuidInterface $uuid, array $images)
{ /* .. */ }
public function getDeploymentUuid()
{
return $this->uuid;
}
public function getImages()
{
return $this->images;
}
}
Wourah!
$ bin/behat -fprogress
....
2 scenarios (2 passed)
4 steps (4 passed)
0m0.12s (40.89Mb)
Starting a deployment?
Scenario: A successfully created deployment can be started
Given a deployment was created
When I start the deployment
Then the deployment should be started
Scenario: A deployment can be started only once
Given a deployment was created and started
When I start the deployment
Then the deployment should be invalid
@Given a deployment was created and started
public function aDeploymentWasCreatedAndStarted()
{
try {
$uuid = Uuid::uuid4();
$this->deployment = Deployment::fromEvents([
new DeploymentCreated($uuid, ['image']),
new DeploymentStarted($uuid),
]);
} catch (Throwable $e) {
$this->exception = $e;
}
}
@When I start the deployment
public function iStartTheDeployment()
{
try {
$this->deployment->start();
} catch (Throwable $e) {
$this->exception = $e;
}
}
starting a deployment
final class Deployment
{
private $uuid;
private $started = false;
// ...
public function start()
{
if ($this->started) {
throw new InvalidArgumentException('Deployment already started');
}
$this->raise(new DeploymentStarted($this->uuid));
}
public function apply(DeploymentEvent $event)
{
// ...
if ($event instanceof DeploymentStarted) {
$this->started = true;
}
}
}
That's too fast...
$ bin/behat -fprogress
.........
4 scenarios (4 passed)
10 steps (10 passed)
0m0.31s (41.22Mb)
We are done!
...with your domain
Repositories & Persistence
Event Store
interface EventStore
{
public function findByDeploymentUuid(UuidInterface $uuid) : array;
public function add(DeploymentEvent $event);
}
Implementation detail: InMemory / Doctrine /
Custom / ...
Our repository contract
interface DeploymentRepository
{
public function find(UuidInterface $uuid) : Deployment;
}
The event-based implementation
final class EventBasedDeploymentRepository implements DeploymentRepository
{
public function __construct(EventStore $eventStore)
{ /** .. **/ }
public function find(UuidInterface $uuid) : Deployment
{
return Deployment::fromEvents(
$this->eventStore->findByDeploymentUuid($uuid)
);
}
}
The plumbing
Message Buses
SimpleBus
4 Written by Matthias Noback
http://simplebus.github.io/SymfonyBridge/
# app/config/config.yml
event_bus:
logging: ~
command_bus:
logging: ~
Our HTTP interface (without commands)
final class DeploymentController
{
private $eventBus;
public function __construct(MessageBus $eventBus)
{ /* ... */ }
public function createAction(Request $request)
{
$deployment = Deployment::create(
Uuid::uuid4(),
$request->request->get('docker-images')
);
foreach ($deployment->raisedEvents() as $event) {
$this->eventBus->handle($event);
}
return new Response(Response::HTTP_CREATED);
}
}
Our HTTP interface (with commands)
final class DeploymentController
{
private $commandBus;
public function __construct(MessageBus $commandBus)
{ /* ... */ }
public function createAction(Request $request)
{
$uuid = Uuid::uuid4();
$this->commandBus->handle(new CreateDeployment(
$uuid,
$request->request->get('docker-images')
));
return new Response(Response::HTTP_CREATED);
}
}
Command Handler
final class CreateDeploymentHandler
{
private $eventBus;
public function __construct(MessageBus $eventBus)
{ /* ... */ }
public function handle(CreateDeployment $command)
{
$deployment = Deployment::create(
$command->getUuid(),
$command->getImages()
);
foreach ($deployment->raisedEvents() as $event) {
$this->eventBus->handle($event);
}
}
}
The plumbing
<service id="app.controller.deployment"
class="AppBundleControllerDeploymentController">
<argument type="service" id="command_bus" />
</service>
<service id="app.handler.create_deployment"
class="AppDeploymentHandlerCreateDeploymentHandler">
<argument type="service" id="event_bus" />
<tag name="command_handler" handles="AppCommandCreateDeployment" />
</service>
What do we have right now?
1. Send a command from an HTTP API
2. The command handler talks to our domain
3. Domain raise an event
4. The event is dispatched to the event bus
Storing our events
final class DeploymentEventStoreMiddleware implements MessageBusMiddleware
{
private $eventStore;
public function __construct(EventStore $eventStore)
{
$this->eventStore = $eventStore;
}
public function handle($message, callable $next)
{
if ($message instanceof DeploymentEvent) {
$this->eventStore->add($message);
}
$next($message);
}
}
We <3 XML
<service id="app.event_bus.middleware.store_events"
class="AppEventBusMiddlewareStoreEvents">
<argument type="service" id="event_store" />
<tag name="event_bus_middleware" />
</service>
Our events are stored!
...so we can get our Deployment from
the repository
Let's start our deployment!
final class StartDeploymentWhenCreated
{
private $commandBus;
public function __construct(MessageBus $commandBus)
{ /* ... */ }
public function notify(DeploymentCreated $event)
{
// There will be conditions here...
$this->commandBus->handle(new StartDeployment(
$event->getDeploymentUuid()
));
}
}
The handler
final class StartDeploymentHandler
{
public function __construct(DeploymentRepository $repository, MessageBus $eventBus)
{ /* ... */ }
public function handle(StartDeployment $command)
{
$deployment = $this->repository->find($command->getDeploymentUuid());
$deployment->start();
foreach ($deployment->raisedEvents() as $event) {
$this->eventBus->handle($event);
}
}
}
The plumbing
<service id="app.deployment.auto_start.starts_when_created"
class="AppDeploymentAutoStartStartsWhenCreated">
<argument type="service" id="command_bus" />
<tag name="event_subscriber"
subscribes_to="AppEventDeploymentCreated" />
</service>
<service id="app.deployment.handler.start_deployment"
class="AppDeploymentHandlerStartDeploymentHandler">
<argument type="service" id="app.deployment_repository" />
<argument type="service" id="event_bus" />
<tag name="command_handler" handles="AppCommandStartDeployment" />
</service>
What happened?
[...]
4. A dispatched DeploymentCreated event
5. A listener created a StartDeployment command
6. The command handler called the start method on the
Deployment
7. The domain validated and raised a DeploymentStarted
event
8. The DeploymentStarted was dispatched on the event-
You'll go further...
final class Deployment
{
// ...
public function finishedBuild(Build $build)
{
if ($build->isFailure()) {
return $this->raise(new DeploymentFailed($this->uuid));
}
$this->builtImages[] = $build->getImage();
if (count($this->builtImages) == count($this->images)) {
$this->raise(new DeploymentSuccessful($this->uuid));
}
}
}
Dependencies... the wrong way
final class Deployment
{
private $notifier;
public function __construct(NotifierInterface $notifier)
{ /* .. */ }
public function notify()
{
$this->notifier->notify($this);
}
}
Dependencies... the right way
final class Deployment
{
public function notify(NotifierInterface $notifier)
{
$notifier->notify($this);
}
}
Projections!
final class DeploymentStatusProjector
{
public function __construct(
DeploymentRepository $repository,
DeploymentStatusProjectionStorage $storage
) { /* ... */ }
public function notify(DeploymentEvent $event)
{
$uuid = $event->getDeploymentUuid();
$deployment = $this->repository->find($uuid);
$percentage = count($deployment->getBuiltImages())
/ count($deployment->getImages());
$this->storage->store($uuid, [
'started' => $deployment->isStarted(),
'percentage' => $percentage,
]);
}
}
You can have many
projections and storage
backends for just one
aggregate.
Testing! (layers)
1. Use your domain objects
2. Create commands and read your event store
3. Uses your API and projections
What we just achieved
1. Incoming HTTP requests
2. Commands to the command bus
3. Handlers talk to your domain
4. Domain produces events
5. Events are stored and dispatched
6. Projections built for fast query
Thank you!
@samuelroze
continuouspipe.io
https://joind.in/talk/62c40

CQRS and Event Sourcing in a Symfony application

  • 1.
    CQRS and EventSourcing in a Symfony application
  • 2.
    Samuel Roze Software Enginner@ Inviqa 4 twitter.com/samuelroze 4 github.com/sroze 4 sroze.io
  • 5.
    The heart ofsoftware is its ability to solve domain- related problems for its user 1 Eric Evans
  • 7.
  • 9.
    CQRS & EventSourcing
  • 11.
    How are wegoing to build that? 1. Our domain 2. Repository and persistence 3. Message buses 4. Automation via "services" 5. Projections
  • 12.
    Our domain A deployment 1.Build Docker images 2. Display the progress 3. Send a notification
  • 13.
    An event interface DeploymentEvent { publicfunction getDeploymentUuid() : UuidInterface; }
  • 14.
    Event capability trait EventsCapability { private$events = []; protected function raise(DeploymentEvent $event) { $this->events[] = $event; } public function eraseEvents() : void { $this->events = []; } public function raisedEvents() : array { return $this->events; } }
  • 15.
    Creating the objectfrom events final class Deployment { use RaiseEventsCapability; private function __construct() { } public static function fromEvents(array $events) { $deployment = new self(); foreach ($events as $event) { $deployment->apply($event); } return $deployment; } }
  • 16.
    Building the objectstate final class Deployment { private $uuid; // ... private function apply(DeploymentEvent $event) { if ($event instanceof DeploymentCreated) { $this->uuid = $event->getUuid(); } } }
  • 18.
    You know... testing! Scenario: WhenI create a deployment Then a deployment should be created
  • 19.
    You know... testing! Scenario:A deployment need to have at least one image When I create a deployment with 0 image Then the deployment should not be valid Scenario: Deployment with 1 image When I create a deployment with 1 image Then a deployment should be created
  • 20.
    @When I createa deployment with :number image public function iCreateADeploymentWithImage($count) { try { $this->deployment = Deployment::create( Uuid::uuid4(), array_fill(0, $count, 'image') ); } catch (Throwable $e) { $this->exception = $e; } }
  • 21.
    @Then the deploymentshould not be valid public function theDeploymentShouldNotBeValid() { if (!$this->exception instanceof InvalidArgumentException) { throw new RuntimeException( 'The exception found, if any, is not matching' ); } }
  • 22.
    @Then a deploymentshould be created public function aDeploymentShouldBeCreated() { $events = $this->deployment->raisedEvents(); $matchingEvents = array_filter($events, function(DeploymentEvent $event) { return $event instanceof DeploymentCreated; }); if (count($matchingEvents) === 0) { throw new RuntimeException('No deployment created found'); } }
  • 23.
    Create... from thebeginning! final class Deployment { // ... public static function create(Uuid $uuid, array $images) { if (count($images) == 0) { throw new InvalidArgumentException('What do you deploy then?'); } $createdEvent = new DeploymentCreated($uuid, $images); $deployment = self::fromEvents([$createdEvent]); $deployment->raise($createdEvent); return $deployment; } }
  • 24.
    DeploymentCreated event final classDeploymentCreated implements DeploymentEvent { public function __construct(UuidInterface $uuid, array $images) { /* .. */ } public function getDeploymentUuid() { return $this->uuid; } public function getImages() { return $this->images; } }
  • 25.
    Wourah! $ bin/behat -fprogress .... 2scenarios (2 passed) 4 steps (4 passed) 0m0.12s (40.89Mb)
  • 26.
    Starting a deployment? Scenario:A successfully created deployment can be started Given a deployment was created When I start the deployment Then the deployment should be started Scenario: A deployment can be started only once Given a deployment was created and started When I start the deployment Then the deployment should be invalid
  • 27.
    @Given a deploymentwas created and started public function aDeploymentWasCreatedAndStarted() { try { $uuid = Uuid::uuid4(); $this->deployment = Deployment::fromEvents([ new DeploymentCreated($uuid, ['image']), new DeploymentStarted($uuid), ]); } catch (Throwable $e) { $this->exception = $e; } }
  • 28.
    @When I startthe deployment public function iStartTheDeployment() { try { $this->deployment->start(); } catch (Throwable $e) { $this->exception = $e; } }
  • 29.
    starting a deployment finalclass Deployment { private $uuid; private $started = false; // ... public function start() { if ($this->started) { throw new InvalidArgumentException('Deployment already started'); } $this->raise(new DeploymentStarted($this->uuid)); } public function apply(DeploymentEvent $event) { // ... if ($event instanceof DeploymentStarted) { $this->started = true; } } }
  • 30.
    That's too fast... $bin/behat -fprogress ......... 4 scenarios (4 passed) 10 steps (10 passed) 0m0.31s (41.22Mb)
  • 31.
  • 32.
  • 33.
    Event Store interface EventStore { publicfunction findByDeploymentUuid(UuidInterface $uuid) : array; public function add(DeploymentEvent $event); } Implementation detail: InMemory / Doctrine / Custom / ...
  • 34.
    Our repository contract interfaceDeploymentRepository { public function find(UuidInterface $uuid) : Deployment; }
  • 35.
    The event-based implementation finalclass EventBasedDeploymentRepository implements DeploymentRepository { public function __construct(EventStore $eventStore) { /** .. **/ } public function find(UuidInterface $uuid) : Deployment { return Deployment::fromEvents( $this->eventStore->findByDeploymentUuid($uuid) ); } }
  • 36.
  • 37.
    SimpleBus 4 Written byMatthias Noback http://simplebus.github.io/SymfonyBridge/ # app/config/config.yml event_bus: logging: ~ command_bus: logging: ~
  • 38.
    Our HTTP interface(without commands) final class DeploymentController { private $eventBus; public function __construct(MessageBus $eventBus) { /* ... */ } public function createAction(Request $request) { $deployment = Deployment::create( Uuid::uuid4(), $request->request->get('docker-images') ); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } return new Response(Response::HTTP_CREATED); } }
  • 39.
    Our HTTP interface(with commands) final class DeploymentController { private $commandBus; public function __construct(MessageBus $commandBus) { /* ... */ } public function createAction(Request $request) { $uuid = Uuid::uuid4(); $this->commandBus->handle(new CreateDeployment( $uuid, $request->request->get('docker-images') )); return new Response(Response::HTTP_CREATED); } }
  • 40.
    Command Handler final classCreateDeploymentHandler { private $eventBus; public function __construct(MessageBus $eventBus) { /* ... */ } public function handle(CreateDeployment $command) { $deployment = Deployment::create( $command->getUuid(), $command->getImages() ); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } } }
  • 41.
    The plumbing <service id="app.controller.deployment" class="AppBundleControllerDeploymentController"> <argumenttype="service" id="command_bus" /> </service> <service id="app.handler.create_deployment" class="AppDeploymentHandlerCreateDeploymentHandler"> <argument type="service" id="event_bus" /> <tag name="command_handler" handles="AppCommandCreateDeployment" /> </service>
  • 42.
    What do wehave right now? 1. Send a command from an HTTP API 2. The command handler talks to our domain 3. Domain raise an event 4. The event is dispatched to the event bus
  • 43.
    Storing our events finalclass DeploymentEventStoreMiddleware implements MessageBusMiddleware { private $eventStore; public function __construct(EventStore $eventStore) { $this->eventStore = $eventStore; } public function handle($message, callable $next) { if ($message instanceof DeploymentEvent) { $this->eventStore->add($message); } $next($message); } }
  • 44.
    We <3 XML <serviceid="app.event_bus.middleware.store_events" class="AppEventBusMiddlewareStoreEvents"> <argument type="service" id="event_store" /> <tag name="event_bus_middleware" /> </service>
  • 45.
    Our events arestored! ...so we can get our Deployment from the repository
  • 46.
    Let's start ourdeployment! final class StartDeploymentWhenCreated { private $commandBus; public function __construct(MessageBus $commandBus) { /* ... */ } public function notify(DeploymentCreated $event) { // There will be conditions here... $this->commandBus->handle(new StartDeployment( $event->getDeploymentUuid() )); } }
  • 47.
    The handler final classStartDeploymentHandler { public function __construct(DeploymentRepository $repository, MessageBus $eventBus) { /* ... */ } public function handle(StartDeployment $command) { $deployment = $this->repository->find($command->getDeploymentUuid()); $deployment->start(); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } } }
  • 48.
    The plumbing <service id="app.deployment.auto_start.starts_when_created" class="AppDeploymentAutoStartStartsWhenCreated"> <argumenttype="service" id="command_bus" /> <tag name="event_subscriber" subscribes_to="AppEventDeploymentCreated" /> </service> <service id="app.deployment.handler.start_deployment" class="AppDeploymentHandlerStartDeploymentHandler"> <argument type="service" id="app.deployment_repository" /> <argument type="service" id="event_bus" /> <tag name="command_handler" handles="AppCommandStartDeployment" /> </service>
  • 49.
    What happened? [...] 4. Adispatched DeploymentCreated event 5. A listener created a StartDeployment command 6. The command handler called the start method on the Deployment 7. The domain validated and raised a DeploymentStarted event 8. The DeploymentStarted was dispatched on the event-
  • 50.
  • 51.
    final class Deployment { //... public function finishedBuild(Build $build) { if ($build->isFailure()) { return $this->raise(new DeploymentFailed($this->uuid)); } $this->builtImages[] = $build->getImage(); if (count($this->builtImages) == count($this->images)) { $this->raise(new DeploymentSuccessful($this->uuid)); } } }
  • 52.
    Dependencies... the wrongway final class Deployment { private $notifier; public function __construct(NotifierInterface $notifier) { /* .. */ } public function notify() { $this->notifier->notify($this); } }
  • 53.
    Dependencies... the rightway final class Deployment { public function notify(NotifierInterface $notifier) { $notifier->notify($this); } }
  • 54.
  • 55.
    final class DeploymentStatusProjector { publicfunction __construct( DeploymentRepository $repository, DeploymentStatusProjectionStorage $storage ) { /* ... */ } public function notify(DeploymentEvent $event) { $uuid = $event->getDeploymentUuid(); $deployment = $this->repository->find($uuid); $percentage = count($deployment->getBuiltImages()) / count($deployment->getImages()); $this->storage->store($uuid, [ 'started' => $deployment->isStarted(), 'percentage' => $percentage, ]); } }
  • 56.
    You can havemany projections and storage backends for just one aggregate.
  • 57.
    Testing! (layers) 1. Useyour domain objects 2. Create commands and read your event store 3. Uses your API and projections
  • 58.
    What we justachieved 1. Incoming HTTP requests 2. Commands to the command bus 3. Handlers talk to your domain 4. Domain produces events 5. Events are stored and dispatched 6. Projections built for fast query
  • 59.