CQRS and Event Sourcing in a Symfony application

59
C QRS and Event Sour cing in a Symfony applica tion

Transcript of CQRS and Event Sourcing in a Symfony application

CQRS and Event Sourcingin 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

user1

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 domainA deployment

1. Build Docker images

2. Display the progress

3. Send a notification

An eventinterface DeploymentEvent{ public function getDeploymentUuid() : UuidInterface;}

Event capabilitytrait 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 eventsfinal 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 statefinal 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 eventfinal 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 deploymentfinal 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 Storeinterface EventStore{ public function findByDeploymentUuid(UuidInterface $uuid) : array;

public function add(DeploymentEvent $event);}

Implementation detail: InMemory / Doctrine / Custom / ...

Our repository contractinterface DeploymentRepository{ public function find(UuidInterface $uuid) : Deployment;}

The event-based implementationfinal class EventBasedDeploymentRepository implements DeploymentRepository{ public function __construct(EventStore $eventStore) { /** .. **/ }

public function find(UuidInterface $uuid) : Deployment { return Deployment::fromEvents( $this->eventStore->findByDeploymentUuid($uuid) ); }}

The plumbingMessage Buses

SimpleBus4 Written by Matthias Noback

http://simplebus.github.io/SymfonyBridge/

# app/config/config.ymlevent_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 Handlerfinal 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="AppBundle\Controller\DeploymentController"> <argument type="service" id="command_bus" /></service>

<service id="app.handler.create_deployment" class="App\Deployment\Handler\CreateDeploymentHandler"> <argument type="service" id="event_bus" />

<tag name="command_handler" handles="App\Command\CreateDeployment" /></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 eventsfinal 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="App\EventBus\Middleware\StoreEvents"> <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 handlerfinal 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="App\Deployment\AutoStart\StartsWhenCreated"> <argument type="service" id="command_bus" />

<tag name="event_subscriber" subscribes_to="App\Event\DeploymentCreated" /></service>

<service id="app.deployment.handler.start_deployment" class="App\Deployment\Handler\StartDeploymentHandler"> <argument type="service" id="app.deployment_repository" /> <argument type="service" id="event_bus" />

<tag name="command_handler" handles="App\Command\StartDeployment" /></service>

What happened?[...]4. A dispatched DeploymentCreated event5. A listener created a StartDeployment command6. The command handler called the start method on the Deployment7. The domain validated and raised a DeploymentStarted event8. 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 wayfinal class Deployment{ private $notifier;

public function __construct(NotifierInterface $notifier) { /* .. */ }

public function notify() { $this->notifier->notify($this); }}

Dependencies... the right wayfinal 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 achieved1. 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