Share
Embed
CQRS/ES avec Symfony, c’est (trop) bien !
Jérémy Romey
April 01, 2024
Transcript
93 slides
-
1 @j eremyFreeAgent JÉR ÉM Y RO ME Y This
presentation has been designed using images from Flaticon.com CQRS/ES avec Symfony, c’est (trop) bien ! -
What’s next 1. Rappels CQRS/ES 2. Cycle de vie 3.
Merci Sy mf ony 4. Exemple de fonctionnalité CQRS/ES avec Symfony, c’est (trop) bien ! -
What’s next Rappels CQRS/ES Cycle de vie Merci Sy mf
ony Exemple de fonctionnalité -
7 Car id ST RING brand ST RING weight INT
length INT color ST RING Exemple, la voiture “1234” a une couleur “grise” -
8 class Car { string $i d; string $brand; int
$weight; int $l ength; string $color; } Classe PHP basée sur notre structure de données modélisée -
1 0 UPDA TE cars S E T color =
"pink" WHERE car_id = "1234"; PUT /cars/1234 { "color": "pink" } Requête HTTP Objet PHP $car->setColor('pink'); Requête SQL -
11 CREA TE READ UPDA TE DEL ETE PO ST
G E T PUT DEL ETE Modification de l’état (d’une resource) UPDA TE cars S ET color = "pink" WHERE car_id = "1234"; -
13 On assigne la couleur de la voiture 1234 à
pink On peint la voiture en pink OU -
14 UPDA TE cars S ET color = "pink" WHERE
car_id = "1234"; $car->setColor('pink'); $car->paint('pink'); PUT /cars/1234 { "color": "pink" } PO ST /cars/1234/paint { "color": "pink" } -
17 L’ ÉT AT La couleur de la voiture 1234
est mi se à pink LA MODIFICA TI ON La voiture a été peinte en pink UPDA TE cars S E T color = "pink" WHERE car_id = "1234"; INSERT INTO events S ET event_name = "car_painted", event_data = '{ "color": "pink" }'; -
19 Introducing Event Stor mi ng — Alberto Brandolini “The
Picture That Explains Everything” -
What’s next Rappels CQRS/ES Cycle de vie Merci Sy mf
ony Exemple de fonctionnalité -
21 Event Fait Im mutable Trans mi ssible Exemple :
Notre évènement CarPainted -
22 Command Intention (d’un utilisateur) Contient les informations nécessaire pour
faire l’action Exemple : Notre command PaintCar -
23 Read model Permet (à l’utilisateur) de prendre une décision
Objet utilisé en lecture -
24 Policy Permet de réagir à des évènements “A chaque
fois que un Event on déclenche une Command” “A chaque fois qu’une voiture est peinte (CarPainted) on lave la voiture ( W ashCar)” -
25 Aggregate Est représenté par une entity principale Garanti la
cohérence métier Transaction métier Exemple : Notre aggregate Car -
26 Entity Est représentée par un identifiant Possède un cycle
de vie Exemple : Notre entité Car -
28 Introducing Event Stor mi ng — Alberto Brandolini “The
Picture That Explains Everything” -
29 Car painted Wash car Whenever a car is painted
Car Preview Car maintenance log Paint car System -
What’s next Rappels CQRS/ES Cycle de vie Merci Sy mf
ony Exemple de fonctionnalité -
31 Routing HttpFoundation HttpKernel Validator Security T wi g Uid
Clock Scheduler HttpClient Messenger Serializer Introducing Event Stor mi ng — Alberto Brandolini -
32 • Sauvegarder des events • Récupérer des events •
Transmettre des events Ce dont nous avons besoin : -
trait AggregateTrait { final public function __construct( public readonly object
$rootId ) {} } Aggregate 33 -
38 findByAggregate( $ rootId) reconstruct( $ events) Event store Aggregate
Reconstruction de l’aggregate -
39 NA ME ST RING car.car_painted AGGREGA TE_T YPE ST
RING car AGGREGA TE _ID UUID d3b0e026-2940-4093-9580-6da0d1854e71 AGGREGA TE _VERSION INT 13 DAT ETIME T IME 29/03/2024 14:00:00 DATA JSON { "color": "pink" } 67b2262a-8a14-43 f0 -884f-6 0 7 2784fd272 ID UUID CON TE XT JSON { "user": "5678", "request": "ABCD" } -
use Sy mf ony\Component\Clock\DatePoint; trait AggregateTrait { final protected function
record(object $event): void { $t his->apply( $ event); $t his->events[] = [ 't i m e' => new DatePoint(), 'event' => $event, ]; } } Aggregate 41 -
trait AggregateTrait { final public function releaseEvents(): array { $releasedEvents
= $t his->events; $t his->events = []; return $releasedEvents; } } Aggregate 42 -
trait AggregateTrait { final public static function reconstruct( object $rootId,
iterable $events, ): static { $aggregate = new static( $ rootId); foreach ( $ events as $event) { $aggregate->apply( $ event); } return $aggregate; } } Aggregate 43 -
trait AggregateTrait { final protected function apply(object $event): void {
$applyEvent = 'apply'.EventNameTransformer::shortName( $ event); $ t his->$applyEvent( $ event); ++ $t his->version; } } Aggregate 44 -
Aggregate reposit or y 45 use Sy mf ony\Component\Uid\Uuid; trait
AggregateRepositoryTrait { public function transfor m (object $rootId, int $v ersion, array $events): iterable { $context = []; // Add context to event (current user id, current request id, etc.) foreach ( $ events as $event) { yield new Event( Uuid::v4(), EventNameTransformer::name( $ event['event']::class), $event['event'], $event['t i m e'], $rootId, $v ersion, $context, ); } } } -
trait AggregateRepositoryTrait { public function get(object $rootId): AggregateInterface { $aggregateType
= $t his->ge tT ype(); $events = $ t his->eventStore->findByAggregate( $rootId, ); return $aggregateType::reconstruct( $ rootId, $events); } } Aggregate reposit or y 46 -
interface EventStoreInterface { public function store(Event ... $ events): void;
public function findByAggregate(string $rootId): iterable; } Event st or e 47 -
use Sy mf ony\Component\Serializer\SerializerInterface; final class DoctrineEventStore im plements EventStoreInterface
{ public function store(Event ... $ events): void { foreach ( $ events as $event) { $ t his->doctrineConnection->insert( 'events', $ t his->serializer->serialize( $ event), ); } $t his->eventBus->dispatch( $ events); } } Event st or e 48 -
final class DoctrineEventStore i m plements EventStoreInterface { public function
findByAggregate(string $rootId): iterable { $queryBuilder = $t his->getSelectQueryBuilder() ->andWhere('events.aggregate_root_id = :rootId') ->setParameter('rootId', $rootId) ; return $ t his->createEvents( $ queryBuilder); } } Event st or e 49 -
50 Car painted Wash car Whenever a car is painted
PolicyBus Command Event Policy -
51 Car painted Car Preview On CarPainted Projection Read model
Event Projection Car maintenance log ProjectionBus -
final class EventBus i m plements EventBusInterface { public function
__construct( private readonly ProjectionBusInterface $projectionBus, private readonly PolicyBusInterface $policyBus, ) { } public function dispatch(object ... $ events): void { $t his->projectionBus->dispatch( $ events); $t his->policyBus->dispatch( $ events); } } Event bus 53 -
use Sy m f ony\Component\Messenger\MessageBusInterface; final class ProjectionBus im plements
ProjectionBusInterface { public function __construct( private readonly MessageBusInterface $messageBus ) {} public function dispatch(object $event): void { $t his->messageBus->dispatch( $ event); } } Projection bus 54 -
use Sy m f ony\Component\Messenger\MessageBusInterface; final class PolicyBus im plements
PolicyBusInterface { public function __construct( private readonly MessageBusInterface $messageBus ) {} public function dispatch(object $event): void { $t his->messageBus->dispatch( $ event); } } Policy bus 55 -
use Sy mf ony\Component\Messenger\HandleTrait; use Sy mf ony\Component\Messenger\MessageBusInterface; final class
CommandBus im plements CommandBusInterface { use HandleTrait; public function __construct( M essageBusInterface $messageBus) { $ t his->messageBus = $messageBus; } public function dispatch(object $command): void { $ t his->handle( $ command); } } Command bus 57 -
60 Webhook Système extérieur Event (Ex: Payment system provider) (Ex:
le paiement a été refusé) -
Scheduler 62 use Sy m f ony\Component\Scheduler\Attribute\AsSchedule; use Sy m
f ony\Component\Scheduler\RecurringMessage; use Sy m f ony\Component\Scheduler\Schedule; use Sy m f ony\Component\Scheduler\ScheduleProviderInterface; # [AsSchedule(name: 'car')] class EndO f W eekProvider i m plements ScheduleProviderInterface { public function getSchedule(): Schedule { return (new Schedule())->add( RecurringMessage::every( 'first day of week m i dnight', new DispatchEndO fW eek() )); } } -
What’s next Rappels CQRS/ES Cycle de vie Merci Sy mf
ony Exemple de fonctionnalité -
65 • Id • Event • Aggregate • Command Ce
dont nous avons besoin : -
use Sy m f ony\Component\Uid\Ulid; final readonly class CarId {
final public function __construct(public string $v alue) { // do validation about the value } final public static function create(): static { return new static((new Ulid())->toBase32()); } } Id 66 -
final readonly class CarPainted { public function __construct( public CarId
$carId, public string $color, ) {} } Event 67 -
final class Car { public function paint( string $color, ):
void { $t his->record( new CarPainted( $t his->rootId, $color, ), ); } } Aggregate 68 -
final class Car { public function applyCarPainted( CarPainted $carPainted, ):
void { } } Aggregate 69 -
final readonly class PaintCar { public function __construct( public CarId
$carId, public string $color, ) {} } Command 7 0 -
final class PaintCarCommandHandler { public function __construct( private readonly CarRepository
$carRepository, ) { } public function __invoke(PaintCar $paintCar): void { $car = $t his->carRepository->get( $ paintCar->id); $car->paint( $paintCar->color, ); $t his->carRepository->save( $ car); } } Command handler 71 -
72 Car painted Car Preview On CarPainted Projection ProjectionBus avec
Sy m f ony Messenger Read model Event Projection Car maintenance log -
final readonly class CarPreview { public function __construct( public CarId
$carId, public string $color, ) { } } Read model 74 -
final readonly class CarMaintenanceLog { public function __construct( public CarId
$carId, public array $paint, public array $engine, public array $t ires, ) { } } Read model 75 -
final class OnCarPaintedProjection { public function __invoke(CarPainted $carPainted): void {
$carPreview = $t his->carPreviewReadModelRepository->get( $carPainted->id ); $carPreview->color = $carPainted->color; $t his->carPreviewReadModelRepository->save( $ carPrevie w ); } } 76 Projection -
final class OnCarPaintedProjection { public function __invoke(CarPainted $carPainted): void {
$carMaintenanceLog = $t his->carMaintenanceLogReadModelRepository->get( $carPainted->id ); $carMaintenanceLog->paint[] = $carPainted->color; $ t his->carMaintenanceLogReadModelRepository->save( $ carMaintenanceLog); } } Projection 77 -
78 Car Preview Car maintenance log SQL FILE SY S
TEM ELA S TI C SEARCH ET /OU -
79 Car painted Wash car Whenever a car is painted
PolicyBus avec Sy m f ony Messenger Command Event Policy -
final readonly class WashCar { public function __construct( public CarId
$carId, ) {} } Command 81 -
final class WheneverCarPaintedPolicy { public function __construct( private readonly CommandBusInterface
$commandBus, ) { } public function __invoke(CarPainted $carPainted): void { $t his->commandBus->dispatch( new WashCar( $ carPainted->id) ); } } Policy 82 -
use Sy m f ony\Component\HttpFoundation\Response; use Sy m f ony\Component\HttpKernel\Attribute\MapRequestPayload;
final class PaintCarAction { public function __invoke( string $i d, # [ M apRequestPayload] PaintCarInput $paintCarInput, CommandBusInterface $commandBus, ): Response { $commandBus->dispatch(new PaintCar( new CarId( $i d), $gradeAnswerInput->color, )); return new Response(status: Response::HTTP_ACCEP TE D); } } User interface 84 -
final class CarAction { public function __invoke( string $i d,
CarReadModelRepositoryInterface $repository, ): CarReadModel { return $repository->get( new CarId( $i d), ); } } User interface 86 -
87 A chaque fois qu’une voiture est peinte, on lave
la voiture mais si elle est blanche, on ne la lave pas. Changement métier : -
final class WheneverCarPaintedPolicy { public function __construct( private readonly CommandBusInterface
$commandBus, ) { } public function __invoke(CarPainted $carPainted): void { + if ( $ carPainted->color === 'white') { + return; + } $t his->commandBus->dispatch( new WashCar( $ carPainted->id) ); } } Policy 88 -
89 Une voiture de couleur ne peut pas être peinte
d’une autre couleur. Changement métier : -
final class Car { public function paint(string $color): void {
+ if ( $ t his->color !== 'white' && $color !== 'white') { + throw new CannotPaintCarBecauseNo tW hite( + $t his->rootId, + $color, + ); + } $ t his->record(new CarPainted( $t his->rootId, $color)); } } Aggregate 90 -
final class Car { + private ?string $color = null;
public function applyCarPainted( CarPainted $carPainted, ): void { + $t his->color = $carPainted->color; } } Aggregate 91 -
Project created successful This project uses the symfony libraries. If
you see no image in this page, you may need to configure your web server so that it gains access to the sy mf ony_data/web/sf/ directory. This is a temporary page This page is part of the symfony default module. It will disappear as soon as you define a homepage route in your routing.y ml . What’s next Create your data model Customize the layout of the generated templates Learn more from the online documentation Symfony Project Created Congratulations! You have successfully created your symfony project.