Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

DDD & CQRS — PHP Tour 2018

DDD & CQRS — PHP Tour 2018

Arnaud LEMAIRE

May 17, 2018
Tweet

More Decks by Arnaud LEMAIRE

Other Decks in Programming

Transcript

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

    UI STAND FOR USER INTERACTION USER INTENT IS LOST :-(
  2. WHAT DO THEY NEED ? WELCOME TO SPORTLAND Subscription plan

    Membership management Revenues from membership Pay-as-you-gym option
  3. 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 ?
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. DI / THE HEXAGONE DOMAIN DOMAIN INTERFACE CONCRETE IMPLEMENTATION OF

    THE DOMAIN INTERFACE TECHNICAL COLLABORATOR
  11. 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
  12. 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
  13. 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’)
  14. 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();
  15. CQS - THE REPOSITORY REPOSITORY DOMAIN INTERFACE IMPLEMENTATION CAN BE

    IMPLEMENTED USING DOCTRINE DOMAIN ENTITIES NOT ORM ENTITIES
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. FINALLY, THE BUS class CommandBusFactory { static function build( iterable

    $handler Logger $logger ): CommandBus { return new LoggerBusMiddleware( new CommandBusDispatcher($handler), $logger); } }
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. THE BUS COMMAND HANDLER A GREAT PLACE TO PUT A

    TRANSACTIONAL MIDDLEWARE DISPATCHER BUS MIDDLEWARE TRANSACTION ACK/NACK
  36. 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
  37. THE RULE LOCAL EVENT ≠ GLOBAL These events are not

    for a global messaging bus like kafka « »
  38. 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
  39. 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
  40. 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
  41. 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”
  42. 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
  43. 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
  44. 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
  45. 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
  46. PROJECTION COMMAND HANDLER DISPATCHER BUS EVENTS MIDDLEWARE MIDDLEWARE ACK/NACK EVENT

    DISPATCHER EVENT HANDLER AN EVENT HANDLER CAN UPDATE ANOTHER DATASTORE
  47. 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; } }
  48. 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
  49. PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER DATABASE REPOSITORY MEMBERSHIP

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

    IN COMPLETELY DIFFERENT DATASTORE (GIS, SEARCH, …) PROJECTOR
  51. 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
  52. 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)); }
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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); }
  65. 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); }
  66. 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); }
  67. 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
  68. 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
  69. THANKS ! @LILOBASE et merci à Stéphane Pailha pour son

    aide dans la préparation de ce talk
  70. THE LEGACY Can you connect the new membership
 system to

    our current in house CRM ? BY THE WAY « »