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

DDD & CQRS — PHP Tour 2018

DDD & CQRS — PHP Tour 2018

Beb422437c1dfb5366f197919e41ac50?s=128

Arnaud LEMAIRE
PRO

May 17, 2018
Tweet

Transcript

  1. DDD & CQRS THE BUZZWORD CONFERENCE @lilobase https://lgo.group

  2. VS DATA-DRIVEN DOMAIN-DRIVEN WHAT DATA ? WHAT BEHAVIOR ?

  3. VS UPDATE ADDRESS MISTAKE CORRECTION RELOCATION CRUD ARCHITECTURE TASK BASED

    UI STAND FOR USER INTERACTION USER INTENT IS LOST :-(
  4. DO YOU WANT TO HAVE AS MUCH ADDED VALUE THAN

    AN ACCESS DATABASE ?
  5. WHAT DO THEY NEED ? WELCOME TO SPORTLAND Subscription plan

    Membership management Revenues from membership Pay-as-you-gym option
  6. HOW DO WE START ? SubscriptionController UserController DashboardController SessionController MCD

    CONTROLLERS SAME AS HAVING ANEMIC DOCTRINE ENTITIES
  7. CreateSubscriptionPlan Subscribe SubscriptionGrossRevenue StartSession ENTITIES USE CASES Subscription Plan Subscription

    Membership Session YOU DON’T NEED TO HAVE THE FULL CORRECT LISTS, JUST ENOUGH TO GET STARTED HOW DO WE START ?
  8. THE RULE YAGNI Never write code that is not required

    to fulfill a use case « »
  9. IT WAS JUST IN CASE

  10. ENTITY & AGGREGATE ROOT

  11. ENTITIES & VALUE OBJECTS DateTime Money BillingInterval ENTITIES VALUE OBJECTS

    Subscription Membership Session THE DOMAIN IS EXPRESS THROUGH PLAIN OLD PHP OBJECTS CATEGORIZED BETWEEN ENTITIES & VALUE OBJECTS EQUALITY ON IDENTITY, HAS LIFECYCLE EQUALITY ON VALUES, HAS NO LIFECYCLE
  12. AGGREGATE ROOT MEMBERSHIP SUBSCRIPTION PLAN SUBSCRIBED ACTIVITIES MEMBER ID OPERATIONS

    CAN ONLY BE PERFORMED THROUGH THE ROOT ENFORCE INTEGRITY REFERENCE TO COLLABORATOR WITH ≠ LIFECYCLE AGGREGATE OBJECT’S SHARE A COMMON LIFECYCLE: THEY CAN BE SAVED & FETCHED AS A WHOLE
  13. USE CASES COMMAND VS QUERY

  14. COMMAND VS QUERY COMMAND WRITE, MUTATION QUERY READ EXPRESS INTENT

    TO PERFORM NEEDED INFORMATIONS TO TAKE A DECISION USE CASES ARE DISCRIMINATED BETWEEN COMMANDS & QUERIES
  15. LET’S CREATE A COMMAND namespace App\Subscription\Command; class CreateSubscriptionPlanCommand implements Command

    { public $price; public $name; public $annualDiscount; public $expirationDate; public $includedActivities; public function __construct(/* ... */) { $this->price = $price; $this->name = $name; $this->annualDiscount = $annualDiscount; $this->expirationDate = $expirationDate; $this->includedActivities = $includedActivities; } } A COMMAND IS A SIMPLE DTO IN THE COMMAND NAMESPACE
  16. AND ITS ASSOCIATED HANDLER namespace App\Subscription\Command; class CreateSubscriptionPlanCommandHandler implements CommandHandler

    { function __construct(SubscriptionPlanRepository $repository) { $this->repository = $repository; } function handle(CreateSubscriptionPlanCommand $command) { /* ... */ } function listenTo(): string { return CreateSubscriptionPlanCommand::class; } } THE ASSOCIATED COMMAND
  17. AND ITS ASSOCIATED HANDLER namespace App\Subscription\Command; class CreateSubscriptionPlanCommandHandler implements CommandHandler

    { function __construct(SubscriptionPlanRepository $repository) { $this->repository = $repository; } function handle(CreateSubscriptionPlanCommand $command) { /* ... */ } function listenTo(): string { return CreateSubscriptionPlanCommand::class; } } COLLABORATOR INTERFACE
  18. DI / THE HEXAGONE DOMAIN DOMAIN INTERFACE CONCRETE IMPLEMENTATION OF

    THE DOMAIN INTERFACE TECHNICAL COLLABORATOR
  19. AND ITS ASSOCIATED HANDLER class CreateSubscriptionPlanCommandHandler implements CommandHandler { function

    handle(Command $command): CommandResponse { $subscription = new SubscriptionPlan( $command->name, Money::EURO($command->price), $command->annualDiscount, new \DateTime($command->expirationDate), $command->includedActivities ); $this->repository->add($subscription); return CommandResponse::withValue($subscription->id()); } AN AGGREGATE ROOT
  20. AND ITS ASSOCIATED HANDLER class CreateSubscriptionPlanCommandHandler implements CommandHandler { function

    handle(Command $command): CommandResponse { $subscription = new SubscriptionPlan( $command->name, Money::EURO($command->price), $command->annualDiscount, new \DateTime($command->expirationDate), $command->includedActivities ); $this->repository->add($subscription); return CommandResponse::withValue($subscription->id()); } PERSISTENCE
  21. AND ITS ASSOCIATED HANDLER class CreateSubscriptionPlanCommandHandler implements CommandHandler { function

    handle(Command $command): CommandResponse { $subscription = new SubscriptionPlan( $command->name, Money::EURO($command->price), $command->annualDiscount, new \DateTime($command->expirationDate), $command->includedActivities ); $this->repository->add($subscription); return CommandResponse::withValue($subscription->id()); } ACK/NAK RESPONSE (NO ‘DATA’)
  22. USING IT $repository = new SubscriptionPlanInMemoryRepository(); $handler = new CreateSubscriptionPlanCommandHandler($repository);

    $response = $handler->handle( new CreateSubscriptionPlanCommand( 'label', 12, 20, '2018-10-02', ['gym', ‘tennis’] )); //we get the created subscription plan Uuid $response->value();
  23. CQS THE SHARED REPOSITORY

  24. CQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

  25. CQS - THE REPOSITORY REPOSITORY DOMAIN INTERFACE IMPLEMENTATION CAN BE

    IMPLEMENTED USING DOCTRINE DOMAIN ENTITIES NOT ORM ENTITIES
  26. CQS REPOSITORY INTERFACE namespace App\Subscription\Domain; use App\Common\DDD\Repository; use Ramsey\Uuid\Uuid; interface

    SubscriptionPlanRepository extends Repository { public function get(Uuid $id): SubscriptionPlan; public function add(SubscriptionPlan $entity): void; public function delete(Uuid $id): void; public function getAllPlan(): array; public function findActivePlan(): array; } IN THE DOMAIN NAMESPACE
  27. CQS REPOSITORY INTERFACE namespace App\Subscription\Domain; use App\Common\DDD\Repository; use Ramsey\Uuid\Uuid; interface

    SubscriptionPlanRepository extends Repository { public function get(Uuid $id): SubscriptionPlan; public function add(SubscriptionPlan $entity): void; public function delete(Uuid $id): void; public function getAllPlan(): SubscriptionPlan[]; public function findActivePlan(): SubscriptionPlan[]; } RETURNS DOMAIN ENTITY
  28. CQS REPOSITORY INTERFACE namespace App\Subscription\Domain; use App\Common\DDD\Repository; use Ramsey\Uuid\Uuid; interface

    SubscriptionPlanRepository extends Repository { public function get(Uuid $id): SubscriptionPlan; public function add(SubscriptionPlan $entity): void; public function delete(Uuid $id): void; public function getAllPlan(): array; public function findActivePlan(): array; } WE KEEP THE CONTROL OVER OUR ENTITY ID
  29. LET’S CREATE A QUERY namespace App\Subscription\Query; use App\Common\DDD\Query; class FindActiveSubscriptionPlansQuery

    implements Query { } A QUERY IS ALSO A SIMPLE DTO IN THE QUERY NAMESPACE
  30. AND ITS ASSOCIATED HANDLER namespace App\Subscription\Query; class FindActiveSubscriptionPlansQueryHandler implements QueryHandler

    { function __construct(SubscriptionPlanRepository $repository) { $this->repository = $repository; } function handle(Query $query): array { /* ... */ } function listenTo(): string { FindActiveSubscriptionPlansQuery::class; } } COLLABORATOR INTERFACE THE ASSOCIATED QUERY
  31. AND ITS ASSOCIATED HANDLER class FindActiveSubscriptionPlansQueryHandler implements QueryHandler { function

    handle( FindActiveSubscriptionPlansQuery $query ):SubscriptionPlanViewModel[] { return array_map(function(SubscriptionPlan $plan) { return SubscriptionPlanViewModel::fromEntity($plan); }, $this->repository->findActivePlan()); } } MAP ENTITIES TO VIEW MODEL RETURNS A VIEW MODEL
  32. THE BUS & MIDDLEWARE

  33. THE BUS COMMAND HANDLER WHAT IF WE NEED TO APPLY

    COMMON BEHAVIOR ON ALL COMMAND OR QUERIES ? DISPATCHER BUS MIDDLEWARE MIDDLEWARE LOGGING ERROR HANDLING ACK/NACK
  34. THE BUS WE HAVE A SEPARATE BUS FOR COMMANDS &

    QUERIES HANDLER DISPATCHER BUS LOGGING ERRORS HANDLER DISPATCHER BUS LOGGING CACHE WE CAN CONFIGURE SPECIFIC MIDDLEWARE ACK/NACK QUERY VIEWMODEL COMMAND
  35. LET’S CREATE A MIDDLEWARE class LoggerMiddleware implements CommandBusMiddleware { function

    __construct( CommandBusMiddleware $next, LoggerInterface $logger ) { $this->next = $next; $this->logger = $logger; } function dispatch(Command $command): CommandResponse { /* ... */ } } NEXT MIDDLEWARE (CAN BE THE DISPATCHER) COLLABORATOR
  36. LET’S CREATE A MIDDLEWARE class LoggerMiddleware implements CommandBusMiddleware { function

    dispatch(Command $command): CommandResponse { $startTime = microtime(true); $response = $this->next->dispatch($command); $endTime = microtime(true); $elapsed = $endTime - $startTime; $message = 'Command '.get_class($command).' took: '.$elapsed; $this->logger->info($message); return $response; } } BEFORE THE COMMAND IS EXECUTED
  37. LET’S CREATE A MIDDLEWARE class LoggerMiddleware implements CommandBusMiddleware { function

    dispatch(Command $command): CommandResponse { $startTime = microtime(true); $response = $this->next->dispatch($command); $endTime = microtime(true); $elapsed = $endTime - $startTime; $message = 'Command '.get_class($command).' took: '.$elapsed; $this->logger->info($message); return $response; } } AFTER THE COMMAND EXECUTION
  38. LET’S CREATE A MIDDLEWARE class LoggerMiddleware implements CommandBusMiddleware { function

    dispatch(Command $command): CommandResponse { $startTime = microtime(true); $response = $this->next->dispatch($command); $endTime = microtime(true); $elapsed = $endTime - $startTime; $message = 'Command '.get_class($command).' took: '.$elapsed; $this->logger->info($message); return $response; } } RETURNS THE COMMAND RESPONSE
  39. AND NOW THE DISPATCHER class CommandBusDispatcher implements CommandBusMiddleware { public

    function __construct(iterable $handlers) { foreach ($handlers as $handler) { $this->handlers[$handler->listenTo()] = $handler; } } public function dispatch(Command $command): CommandResponse { $commandClass = get_class($command); $handler = $this->handlers[$commandClass]; if($handler == null) throw new \LogicException( "Handler for command $commandClass not found"); return $handler->handle($command); } } WE REGISTER EACH HANDLER WITH ITS ASSOCIATED COMMAND
  40. AND NOW THE DISPATCHER class CommandBusDispatcher implements CommandBusMiddleware { public

    function __construct(iterable $handlers) { foreach ($handlers as $handler) { $this->handlers[$handler->listenTo()] = $handler; } } public function dispatch(Command $command): CommandResponse { $commandClass = get_class($command); $handler = $this->handlers[$commandClass]; if($handler == null) throw new \LogicException( "Handler for command $commandClass not found"); return $handler->handle($command); } } WE DISPATCH THE RECEIVED COMMAND TO ITS HANDLER
  41. FINALLY, THE BUS class CommandBusFactory { static function build( iterable

    $handler Logger $logger ): CommandBus { return new LoggerBusMiddleware( new CommandBusDispatcher($handler), $logger); } }
  42. USING THE SF DIC services: # ... App\Common\Infrastructure\CommandBus: factory: 'App\Service\CommandBusFactory:build'

    arguments: [!tagged ddd.command_handler, @logger] lazy: true src/Kernel.php protected function build(ContainerBuilder $container) { $container ->registerForAutoconfiguration(CommandHandler::class) ->addTag(‘ddd.command_handler’); } config/services.yaml
  43. USAGE /** * Class PaymentResource * @Route(“/plans”) */ class PlansResource

    { public function __construct( CommandBus $commandBus, SerializeInterface $serializer ) { $this->commandBus = $commandBus; $this->serializer = $serializer; } /** * @Route("/", methods={POST}) */ public function create(Request $request) { /* ... */ WE RECEIVE THE COMMAND BUS FROM THE DIC
  44. USAGE /** * Class PaymentResource * @Route(“/plans”) */ class PlansResource

    { /** * @Route("/", methods={POST}) */ public function create(Request $request) { $command = $this->serializer->deserialize( $request->getContent(), CreateSubscriptionPlanCommand::class, 'json' ); $response = $this->commandBus->dispatch($command); return Response::create($response->value(), 201); } } FIRST WE GET THE COMMAND
  45. USAGE /** * Class PaymentResource * @Route(“/plans”) */ class PlansResource

    { /** * @Route("/", methods={POST}) */ public function create(Request $request) { $command = $this->serializer->deserialize( $request->getContent(), CreateSubscriptionPlanCommand::class, 'json' ); $response = $this->commandBus->dispatch($command); return Response::create($response->value(), 201); } } THEN WE DISPATCH IT
  46. USAGE /** * Class PaymentResource * @Route(“/plans”) */ class PlansResource

    { /** * @Route("/", methods={POST}) */ public function create(Request $request) { $command = $this->serializer->deserialize( $request->getContent(), CreateSubscriptionPlanCommand::class, 'json' ); $response = $this->commandBus->dispatch($command); return Response::create($response->value(), 201); } } WE SEND THE RESULT
  47. THE BUS COMMAND HANDLER A GREAT PLACE TO PUT A

    TRANSACTIONAL MIDDLEWARE DISPATCHER BUS MIDDLEWARE TRANSACTION ACK/NACK
  48. DOCTRINE FLUSH MIDDLEWARE class DoctrineFlushBusMiddleware { function __construct(CommandBus $next, 


    EntityManagerInterface $entityManager) { $this->next = $next; $this->entityManager = $entityManager; } function dispatch(Command $command): CommandResponse { $this->entityManager->getConnection()->beginTransaction(); try{ $commandResponse = $this->next->dispatch($command); $this->entityManager->flush(); $this->entityManager->getConnection()->commit(); }catch(){ $this->entityManager->getConnection()->rollBack(); } return $commandResponse; } } AUTO FLUSH AFTER COMMAND EXECUTION
  49. DOMAIN EVENT BUSINESS SIDE-EFFECTS

  50. DOMAIN EVENT COMMAND HANDLER ACK/NACK DOMAIN EVENTS REPRESENT A FACT

    AND NOT AN INTENT LIKE A COMMAND
  51. THE RULE LOCAL EVENT ≠ GLOBAL These events are not

    for a global messaging bus like kafka « »
  52. WHAT IS AN EVENT ? namespace App\Membership\Domain; class MemberJoined implements

    Event { public $newMemberId; public $choosenPlanId; public function __construct( Uuid $newMemberId, Uuid $choosenPlanId ) { $this->newMemberId = $newMemberId; $this->choosenPlanId = $choosenPlanId; } } IT IS ALSO A DTO ! IN THE DOMAIN NAMESPACE
  53. ADDING EVENTS TO OUR HANDLERS class JoinMembershipCommandHandler implements CommandHandler {

    public function handle(Command $command): CommandResponse { /* ... */ return CommandResponse::withValue( $membership->id(), new MemberJoined($membership->id(), $plan->id()) ); } WE RETURN AN EVENT static function withValue($value, Event... $events): CommandResponse { return new CommandResponse($value, $events); } WITHVALUE SIGNATURE
  54. EVENT HANDLERS class SendWelcomeMailOnMemberJoined implements EventHandler { function __construct( Mailer

    $mailer, SubscriptionPlanRepository $planRepository, MemberRepository $memberRepository ) { $this->mailer = $mailer; $this->planRepository = $planRepository; $this->memberRepository = $memberRepository; } function handle(MemberJoined $event): void { /* ... */ } function listenTo(): string { return MemberJoined::class; } } THE ASSOCIATED EVENT COLLABORATORS
  55. EVENT HANDLERS class SendWelcomeMailOnMemberJoined implements EventHandler { function __construct(/* ...

    */) {} function handle(MemberJoined $event): void { $member = $this->memberRepository->get($event->memberId); $plan = $this->memberRepository->get($event->choosenPlanId); $message = “Hello .$member->firstname(),welcome in SportLand. We hope you'll enjoy your $plan->name() subscription”; $mailer->send($member->email(), $message); } function listenTo(): string {} } RETURNS VOID THEY ARE A GREAT PLACE TO EXPRESS “BUSINESS SIDE EFFECTS”
  56. EVENT HANDLING COMMAND HANDLER UNLIKE COMMANDS OR QUERIES, EVENTS CAN

    HAVE MULTIPLE HANDLERS DISPATCHER BUS EVENTS MIDDLEWARE MIDDLEWARE EVENT ACK/NACK EVENT DISPATCHER EVENT HANDLER EVENT HANDLER EVENT HANDLER
  57. EVENT DISPATCHER MIDDLEWARE class EventDispatcherBusMiddleware implements CommandBusMiddleware { function __construct(CommandBus

    $next, EventBus $eventBus) { $this->bus = $bus; $this->eventBus = $eventBus; } public function dispatch(Command $command): CommandResponse { $commandResponse = $this->bus->dispatch($command); if($commandResponse->hasEvents()) { foreach ($commandResponse->events() as $event) { $this->eventBus->dispatch($event); } } return $commandResponse; } } ITS ALSO A BUS ! A HANDLER CAN EMIT MULTIPLE EVENTS
  58. AND THE EVENT DISPATCHER class EventBus implements \App\Common\DDD\EventBus { public

    function __construct(iterable $handlers) { foreach ($handlers as $handler) { $this->handlers[] = $handler; } } public function dispatch(Event $event): void { $eventClass = get_class($event); $matchingHandlers = array_filter($this->handlers, function($handler) use ($eventClass) { return $handler->listenTo() === $eventClass; }); foreach ($matchingHandlers as $handler) { $handler->handle($event); } } } WE CAN ASK THE SF DIC FOR CLASS THAT IMPLEMENTS EVENTHANDLER
  59. AND THE EVENT DISPATCHER class EventBus implements \App\Common\DDD\EventBus { public

    function __construct(iterable $handlers) { foreach ($handlers as $handler) { $this->handlers[] = $handler; } } public function dispatch(Event $event): void { $eventClass = get_class($event); $matchingHandlers = array_filter($this->handlers, function($handler) use ($eventClass) { return $handler->listenTo() === $eventClass; }); foreach ($matchingHandlers as $handler) { $handler->handle($event); } } } AN EVENT CAN HAVE MULTIPLE HANDLERS
  60. PROJECTION & SOME CQRS

  61. PROJECTION COMMAND HANDLER DISPATCHER BUS EVENTS MIDDLEWARE MIDDLEWARE ACK/NACK EVENT

    DISPATCHER EVENT HANDLER AN EVENT HANDLER CAN UPDATE ANOTHER DATASTORE
  62. PROJECTION class UpdateReferralProgramOnMemberJoined implements EventHandler { function handle(MemberJoined $event): void

    { $findReferrerQuery = $this->connection->prepare(" SELECT rt1.referral_id r1, rt2.referral_id r2 FROM referral rt1 LEFT JOIN referral rt2 ON r1.referral_id = r2.id WHERE r1.id = :referrer "); /* then we update the corresponding referrer counter */ } public function listenTo(): string { return MemberJoined::class; } }
  63. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER DATABASE REPOSITORY MEMBERSHIP

    REFERRAL YOU CAN UPDATE ANOTHER TABLE IN THE SAME DATABASE PROJECTOR SINGLE SOURCE OF TRUTH
  64. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER DATABASE REPOSITORY MEMBERSHIP

    LIKE ANONYMIZED STATS (FREE RGPD COMPLIANCE) REFERRAL PROJECTOR
  65. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY MEMBERSHIP YOU

    CAN UPDATE DATA IN ANOTHER DATABASE PROJECTOR
  66. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY MEMBERSHIP LIKE

    A LEGACY DATABASE (FREE LEGACY MIGRATION) PROJECTOR
  67. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY MEMBERSHIP OR

    IN COMPLETELY DIFFERENT DATASTORE (GIS, SEARCH, …) PROJECTOR
  68. PROJECTIONS COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY AND YOU

    CAN MIX THEM PROJECTOR PROJECTOR PROJECTOR
  69. CQRS COMMAND QUERY RESPONSABILITY SEGREGATION

  70. CQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

  71. CRQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

  72. CQRS REPOSITORY INTERFACE namespace App\Subscription\Domain; use App\Common\DDD\Repository; use Ramsey\Uuid\Uuid; interface

    SubscriptionPlanRepository extends Repository { public function get(Uuid $id): SubscriptionPlan; public function add(SubscriptionPlan $entity): void; public function delete(Uuid $id): void; } NO MORE SPECIFIC QUERY METHOD IT SHOULD LOOK LIKE A MAP INTERFACE
  73. CQRS REPOSITORY class BasketDoctrineRepository implements Repository { public function __construct(EntityManagerInterface

    $entityManager) { $this->doctrineEntityManager = $entityManager; } public function get(Uuid $uuid): Basket { $doctrineEntity = $this->doctrineEntityManager ->getRepository(BasketDoctrineEntity::class) ->find($uuid); if($doctrineEntity == null) throw new EntityNotFoundException(); return Basket::mapFromDoctrine($doctrineEntity); } public function add(Basket $entity): void { $this->doctrineEntityManager ->merge(BasketDoctrineEntity::mapFromPayment($entity)); }
  74. CQRS REPOSITORY class BasketDoctrineRepository implements Repository { public function __construct(EntityManagerInterface

    $entityManager) { $this->doctrineEntityManager = $entityManager; } public function get(Uuid $uuid): Basket { $doctrineEntity = $this->doctrineEntityManager ->getRepository(BasketDoctrineEntity::class) ->find($uuid); if($doctrineEntity == null) throw new EntityNotFoundException(); return Basket::mapFromDoctrine($doctrineEntity); } public function add(Basket $entity): void { $this->doctrineEntityManager ->merge(BasketDoctrineEntity::mapFromPayment($entity)); } IMPLEMENTED WITH A TRAIT
  75. CQRS REPOSITORY class BasketDoctrineRepository implements Repository { public function __construct(EntityManagerInterface

    $entityManager) { $this->doctrineEntityManager = $entityManager; } public function get(Uuid $uuid): Basket { $doctrineEntity = $this->doctrineEntityManager ->getRepository(BasketDoctrineEntity::class) ->find($uuid); if($doctrineEntity == null) throw new EntityNotFoundException(); return Basket::mapFromDoctrine($doctrineEntity); } public function add(Basket $entity): void { $this->doctrineEntityManager ->merge(BasketDoctrineEntity::mapFromPayment($entity)); } IMPLEMENTED WITH A TRAIT
  76. CRQS - THE QUERY COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT

    VIEW MODEL
  77. A QUERY class FindActiveSubscriptionPlansQueryHandler implements QueryHandler { private $connection; public

    function __construct(EntityManager $connection) { $this->connection = $connection; } public function handle(Query $query): array { $query = $this->connection->createQuery(" SELECT NEW SubscriptionViewModel(s.name, s.price) FROM Subscription s "); return $query->getResult(); } public function listenTo(): string { FindAllActiveSubscriptionPlansQuery::class; } } WE ASK FOR A DIRECT CONNEXION TO THE DATASTORE
  78. A QUERY class FindActiveSubscriptionPlansQueryHandler implements QueryHandler { private $connection; public

    function __construct(EntityManager $connection) { $this->connection = $connection; } public function handle(Query $query): array { $query = $this->connection->createQuery(" SELECT NEW SubscriptionViewModel(s.name, s.price) FROM Subscription s "); return $query->getResult(); } public function listenTo(): string { FindAllActiveSubscriptionPlansQuery::class; } } WE EXECUTE THE QUERY IN THE HANDLER BODY
  79. THE BIG PICTURE COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY

    YOU CAN TAKE YOUR DATA FROM THE MOST ADEQUATE STORAGE PROJECTOR PROJECTOR PROJECTOR QUERY HANDLER BUS VIEWMODEL
  80. SCALING FREE SCALING QUERY HANDLER BUS VIEWMODEL QUERY HANDLER BUS

    VIEWMODEL QUERY HANDLER BUS VIEWMODEL
  81. EVENT SOURCING AT LAST…

  82. OUR AGGREGATE RETURNS EVENTS class Basket implements AggregateRoot { use

    BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } WE BUILD THE CORRESPONDING EVENT
  83. OUR AGGREGATE RETURNS EVENTS class Basket implements AggregateRoot { use

    BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } WE APPLY IT
  84. OUR AGGREGATE RETURNS EVENTS class Basket implements AggregateRoot { use

    BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } THEN WE RETURN A TUPLE
  85. trait BasketEventApplier { private function productAdded(ProductAdded $event) { $instance =

    $this->createInstance($this); $instance->products[] = new BasketProduct($event); return $instance; } public function apply($event) { $applier = $this->eventMap[get_class($event)]; return $this->$applier($event); } private $eventMap = [ ProductAdded::class => 'productAdded' ]; } THE EVENT APPLIER
  86. trait BasketEventApplier { private function productAdded(ProductAdded $event) { $instance =

    $this->createInstance($this); $instance->products[] = new BasketProduct($event); return $instance; } public function apply($event) { $applier = $this->eventMap[get_class($event)]; return $this->$applier($event); } private $eventMap = [ ProductAdded::class => 'productAdded' ]; } THE EVENT APPLIER WE BUILD THE CORRESPONDING STATE FROM EACH EVENT
  87. EVENT LIFECYCLE COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY EVENTS

    ARE PERSISTED, NOT THE AGGREGATE STATE PROJECTOR
  88. ES REPOSITORY INTERFACE namespace App\Subscription\Domain; use App\Common\DDD\Repository; use Ramsey\Uuid\Uuid; interface

    SubscriptionPlanRepository extends Repository { public function get(Uuid $id): SubscriptionPlan; } NO MORE SAVE METHODS
  89. ES REPOSITORY INTERFACE class BasketRepository { public function get(Uuid $id)

    { $events = $this->getEventsByAggregate($id); $instance = new Basket(); foreach ($events as $event){ [$instance, $void] = $instance->apply($events); }; return $instance; } private function getEventsByAggregate(Uuid $id) { $stmt = $this->connection->prepare( "SELECT payload FROM events WHERE aggregate_type = 'BASKET' AND aggregate_id = :id ORDER BY timestamp"); $stmt->execute(['id' => $id->toString()]); $results = $stmt->fetchAll(FetchMode::COLUMN); return array_map(function($result) { unserialize($result['payload']); }, $results); }
  90. ES REPOSITORY INTERFACE class BasketRepository { public function get(Uuid $id)

    { $events = $this->getEventsByAggregate($id); $instance = new Basket(); foreach ($events as $event){ [$instance, $void] = $instance->apply($events); }; return $instance; } private function getEventsByAggregate(Uuid $id) { $stmt = $this->connection->prepare( "SELECT payload FROM events WHERE aggregate_type = 'BASKET' AND aggregate_id = :id ORDER BY timestamp"); $stmt->execute(['id' => $id->toString()]); $results = $stmt->fetchAll(FetchMode::COLUMN); return array_map(function($result) { unserialize($result['payload']); }, $results); }
  91. ES HANDLER class AddProductToBasketCommandHandler implements CommandHandler { public function handle(Command

    $command): CommandResponse { $basket = $this->repository->get($command->basketId); [$instance, $events] = $basket->addProduct($command->product); return CommandResponse::withValue($instance->id(), $events); }
  92. THE BIG PICTURE COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY

    PROJECTOR PROJECTOR QUERY HANDLER BUS VIEWMODEL EVENT STORE EVENTS AS A SOURCE OF TRUTH WE CAN CREATE NEW PROJECTION ON DEMAND
  93. THE RULE CHEF INSTEAD OF COOKBOOK USER We tend to

    get sidetracked by tools when we should really be thinking about what our tools are supposed to achieve. Chefs instead of recipe book user « » @_edejong
  94. THANKS ! @LILOBASE et merci à Stéphane Pailha pour son

    aide dans la préparation de ce talk
  95. BONUS

  96. THE LEGACY Can you connect the new membership
 system to

    our current in house CRM ? BY THE WAY « »
  97. THIS ? IT’S JUST BECAUSE WE NEEDED TO TALK WITH

    OUR LEGACY SYSTEM
  98. ANTI-CORRUPTION LAYER GREENFIELD ACL BROWNFIELD OLD, LEGACY API NEW, CLEAN

    API YOU MAY NEED A DEDICATED DATABASE