Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

CQRS and Event Sourcing in a Symfony application

7,840 views

Published on

How to apply CQRS and Event Sourcing in a Symfony application.

Published in: Engineering
  • Thanks for slides. What is meant by slide 53? In symfony and many others frameworks it seems handy to use dependency injection? Why you prefer not to use it?
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Thanks for slides. What is meant by slide 3? In symfony and many others frameworks it seems handy to use dependency injection? Why you prefer not to use it?
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

CQRS and Event Sourcing in a Symfony application

  1. 1. CQRS and Event Sourcing in a Symfony application
  2. 2. Samuel Roze Software Enginner @ Inviqa 4 twitter.com/samuelroze 4 github.com/sroze 4 sroze.io
  3. 3. The heart of software is its ability to solve domain- related problems for its user 1 Eric Evans
  4. 4. Command Query Responsibility Segregation
  5. 5. CQRS & Event Sourcing
  6. 6. How are we going to build that? 1. Our domain 2. Repository and persistence 3. Message buses 4. Automation via "services" 5. Projections
  7. 7. Our domain A deployment 1. Build Docker images 2. Display the progress 3. Send a notification
  8. 8. An event interface DeploymentEvent { public function getDeploymentUuid() : UuidInterface; }
  9. 9. 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; } }
  10. 10. 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; } }
  11. 11. Building the object state final class Deployment { private $uuid; // ... private function apply(DeploymentEvent $event) { if ($event instanceof DeploymentCreated) { $this->uuid = $event->getUuid(); } } }
  12. 12. You know... testing! Scenario: When I create a deployment Then a deployment should be created
  13. 13. 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
  14. 14. @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; } }
  15. 15. @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' ); } }
  16. 16. @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'); } }
  17. 17. 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; } }
  18. 18. 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; } }
  19. 19. Wourah! $ bin/behat -fprogress .... 2 scenarios (2 passed) 4 steps (4 passed) 0m0.12s (40.89Mb)
  20. 20. 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
  21. 21. @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; } }
  22. 22. @When I start the deployment public function iStartTheDeployment() { try { $this->deployment->start(); } catch (Throwable $e) { $this->exception = $e; } }
  23. 23. 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; } } }
  24. 24. That's too fast... $ bin/behat -fprogress ......... 4 scenarios (4 passed) 10 steps (10 passed) 0m0.31s (41.22Mb)
  25. 25. We are done! ...with your domain
  26. 26. Repositories & Persistence
  27. 27. Event Store interface EventStore { public function findByDeploymentUuid(UuidInterface $uuid) : array; public function add(DeploymentEvent $event); } Implementation detail: InMemory / Doctrine / Custom / ...
  28. 28. Our repository contract interface DeploymentRepository { public function find(UuidInterface $uuid) : Deployment; }
  29. 29. 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) ); } }
  30. 30. The plumbing Message Buses
  31. 31. SimpleBus 4 Written by Matthias Noback http://simplebus.github.io/SymfonyBridge/ # app/config/config.yml event_bus: logging: ~ command_bus: logging: ~
  32. 32. 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); } }
  33. 33. 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); } }
  34. 34. 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); } } }
  35. 35. 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>
  36. 36. 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
  37. 37. 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); } }
  38. 38. 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>
  39. 39. Our events are stored! ...so we can get our Deployment from the repository
  40. 40. 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() )); } }
  41. 41. 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); } } }
  42. 42. 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>
  43. 43. 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-
  44. 44. You'll go further...
  45. 45. 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)); } } }
  46. 46. Dependencies... the wrong way final class Deployment { private $notifier; public function __construct(NotifierInterface $notifier) { /* .. */ } public function notify() { $this->notifier->notify($this); } }
  47. 47. Dependencies... the right way final class Deployment { public function notify(NotifierInterface $notifier) { $notifier->notify($this); } }
  48. 48. Projections!
  49. 49. 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, ]); } }
  50. 50. You can have many projections and storage backends for just one aggregate.
  51. 51. Testing! (layers) 1. Use your domain objects 2. Create commands and read your event store 3. Uses your API and projections
  52. 52. 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
  53. 53. Thank you! @samuelroze continuouspipe.io https://joind.in/talk/62c40

×