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
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
{ 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
$response = $handler->handle( new CreateSubscriptionPlanCommand( 'label', 12, 20, '2018-10-02', ['gym', ‘tennis’] )); //we get the created subscription plan Uuid $response->value();
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
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
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
QUERIES HANDLER DISPATCHER BUS LOGGING ERRORS HANDLER DISPATCHER BUS LOGGING CACHE WE CAN CONFIGURE SPECIFIC MIDDLEWARE ACK/NACK QUERY VIEWMODEL COMMAND
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
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
{ 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
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
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
*/) {} 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”
$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
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
{ $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; } }
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
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
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
BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } WE BUILD THE CORRESPONDING EVENT
BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } WE APPLY IT
BasketEventApplier; public function addProduct(Product $product) { $event = new ProductAdded($product); return [ $this->apply($event), $event ]; } } THEN WE RETURN A TUPLE
$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
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