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

CQRS/ES avec Symfony, c’est (trop) bien !

CQRS/ES avec Symfony, c’est (trop) bien !

Jérémy Romey

April 01, 2024
Tweet

More Decks by Jérémy Romey

Other Decks in Programming

Transcript

  1. 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 !
  2. 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 !
  3. 6

  4. 7 Car id ST RING brand ST RING weight INT

    length INT color ST RING Exemple, la voiture “1234” a une couleur “grise”
  5. 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
  6. 9

  7. 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
  8. 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";
  9. 13 On assigne la couleur de la voiture 1234 à

    pink On peint la voiture en pink OU
  10. 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" }
  11. 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" }';
  12. 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)”
  13. 25 Aggregate Est représenté par une entity principale Garanti la

    cohérence métier Transaction métier Exemple : Notre aggregate Car
  14. 29 Car painted Wash car Whenever a car is painted

    Car Preview Car maintenance log Paint car System
  15. 31 Routing HttpFoundation HttpKernel Validator Security T wi g Uid

    Clock Scheduler HttpClient Messenger Serializer Introducing Event Stor mi ng — Alberto Brandolini
  16. 32 • Sauvegarder des events • Récupérer des events •

    Transmettre des events Ce dont nous avons besoin :
  17. 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" }
  18. 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
  19. trait AggregateTrait { final public function releaseEvents(): array { $releasedEvents

    = $t his->events; $t his->events = []; return $releasedEvents; } } Aggregate 42
  20. 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
  21. trait AggregateTrait { final protected function apply(object $event): void {

    $applyEvent = 'apply'.EventNameTransformer::shortName( $ event); $ t his->$applyEvent( $ event); ++ $t his->version; } } Aggregate 44
  22. 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, ); } } }
  23. 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
  24. interface EventStoreInterface { public function store(Event ... $ events): void;

    public function findByAggregate(string $rootId): iterable; } Event st or e 47
  25. 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
  26. 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
  27. 50 Car painted Wash car Whenever a car is painted

    PolicyBus Command Event Policy
  28. 51 Car painted Car Preview On CarPainted Projection Read model

    Event Projection Car maintenance log ProjectionBus
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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() )); } }
  34. 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
  35. final class Car { public function paint( string $color, ):

    void { $t his->record( new CarPainted( $t his->rootId, $color, ), ); } } Aggregate 68
  36. final readonly class PaintCar { public function __construct( public CarId

    $carId, public string $color, ) {} } Command 7 0
  37. 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
  38. 72 Car painted Car Preview On CarPainted Projection ProjectionBus avec

    Sy m f ony Messenger Read model Event Projection Car maintenance log
  39. final readonly class CarPreview { public function __construct( public CarId

    $carId, public string $color, ) { } } Read model 74
  40. final readonly class CarMaintenanceLog { public function __construct( public CarId

    $carId, public array $paint, public array $engine, public array $t ires, ) { } } Read model 75
  41. 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
  42. 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
  43. 79 Car painted Wash car Whenever a car is painted

    PolicyBus avec Sy m f ony Messenger Command Event Policy
  44. 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
  45. 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
  46. final class CarAction { public function __invoke( string $i d,

    CarReadModelRepositoryInterface $repository, ): CarReadModel { return $repository->get( new CarId( $i d), ); } } User interface 86
  47. 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 :
  48. 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
  49. 89 Une voiture de couleur ne peut pas être peinte

    d’une autre couleur. Changement métier :
  50. 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
  51. final class Car { + private ?string $color = null;

    public function applyCarPainted( CarPainted $carPainted, ): void { + $t his->color = $carPainted->color; } } Aggregate 91
  52. 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.