Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

VS UPDATE ADDRESS MISTAKE CORRECTION RELOCATION CRUD ARCHITECTURE TASK BASED UI STAND FOR USER INTERACTION USER INTENT IS LOST :-(

Slide 4

Slide 4 text

DO YOU WANT TO HAVE AS MUCH ADDED VALUE THAN AN ACCESS DATABASE ?

Slide 5

Slide 5 text

WHAT DO THEY NEED ? WELCOME TO SPORTLAND Subscription plan Membership management Revenues from membership Pay-as-you-gym option

Slide 6

Slide 6 text

HOW DO WE START ? SubscriptionController UserController DashboardController SessionController MCD CONTROLLERS SAME AS HAVING ANEMIC DOCTRINE ENTITIES

Slide 7

Slide 7 text

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 ?

Slide 8

Slide 8 text

THE RULE YAGNI Never write code that is not required to fulfill a use case « »

Slide 9

Slide 9 text

IT WAS JUST IN CASE

Slide 10

Slide 10 text

ENTITY & AGGREGATE ROOT

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

USE CASES COMMAND VS QUERY

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

DI / THE HEXAGONE DOMAIN DOMAIN INTERFACE CONCRETE IMPLEMENTATION OF THE DOMAIN INTERFACE TECHNICAL COLLABORATOR

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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’)

Slide 22

Slide 22 text

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();

Slide 23

Slide 23 text

CQS THE SHARED REPOSITORY

Slide 24

Slide 24 text

CQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

Slide 25

Slide 25 text

CQS - THE REPOSITORY REPOSITORY DOMAIN INTERFACE IMPLEMENTATION CAN BE IMPLEMENTED USING DOCTRINE DOMAIN ENTITIES NOT ORM ENTITIES

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

THE BUS & MIDDLEWARE

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

FINALLY, THE BUS class CommandBusFactory { static function build( iterable $handler Logger $logger ): CommandBus { return new LoggerBusMiddleware( new CommandBusDispatcher($handler), $logger); } }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

THE BUS COMMAND HANDLER A GREAT PLACE TO PUT A TRANSACTIONAL MIDDLEWARE DISPATCHER BUS MIDDLEWARE TRANSACTION ACK/NACK

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

DOMAIN EVENT BUSINESS SIDE-EFFECTS

Slide 50

Slide 50 text

DOMAIN EVENT COMMAND HANDLER ACK/NACK DOMAIN EVENTS REPRESENT A FACT AND NOT AN INTENT LIKE A COMMAND

Slide 51

Slide 51 text

THE RULE LOCAL EVENT ≠ GLOBAL These events are not for a global messaging bus like kafka « »

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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”

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

PROJECTION & SOME CQRS

Slide 61

Slide 61 text

PROJECTION COMMAND HANDLER DISPATCHER BUS EVENTS MIDDLEWARE MIDDLEWARE ACK/NACK EVENT DISPATCHER EVENT HANDLER AN EVENT HANDLER CAN UPDATE ANOTHER DATASTORE

Slide 62

Slide 62 text

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; } }

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

PROJECTION COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY MEMBERSHIP OR IN COMPLETELY DIFFERENT DATASTORE (GIS, SEARCH, …) PROJECTOR

Slide 68

Slide 68 text

PROJECTIONS COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY AND YOU CAN MIX THEM PROJECTOR PROJECTOR PROJECTOR

Slide 69

Slide 69 text

CQRS COMMAND QUERY RESPONSABILITY SEGREGATION

Slide 70

Slide 70 text

CQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

Slide 71

Slide 71 text

CRQS COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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)); }

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

CRQS - THE QUERY COMMAND QUERY HANDLER HANDLER REPOSITORY INTENT VIEW MODEL

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

SCALING FREE SCALING QUERY HANDLER BUS VIEWMODEL QUERY HANDLER BUS VIEWMODEL QUERY HANDLER BUS VIEWMODEL

Slide 81

Slide 81 text

EVENT SOURCING AT LAST…

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

EVENT LIFECYCLE COMMAND HANDLER BUS ACK/NACK EVENT DISPATCHER REPOSITORY EVENTS ARE PERSISTED, NOT THE AGGREGATE STATE PROJECTOR

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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); }

Slide 90

Slide 90 text

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); }

Slide 91

Slide 91 text

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); }

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

THANKS ! @LILOBASE et merci à Stéphane Pailha pour son aide dans la préparation de ce talk

Slide 95

Slide 95 text

BONUS

Slide 96

Slide 96 text

THE LEGACY Can you connect the new membership
 system to our current in house CRM ? BY THE WAY « »

Slide 97

Slide 97 text

THIS ? IT’S JUST BECAUSE WE NEEDED TO TALK WITH OUR LEGACY SYSTEM

Slide 98

Slide 98 text

ANTI-CORRUPTION LAYER GREENFIELD ACL BROWNFIELD OLD, LEGACY API NEW, CLEAN API YOU MAY NEED A DEDICATED DATABASE