Slide 1

Slide 1 text

Domain-Driven Design and API Platform 3 @matarld @chalas_r

Slide 2

Slide 2 text

chalasr Mathias Arlaud @matarld les-tilleuls.coop mtarld Robin Chalas @chalas_r les-tilleuls.coop

Slide 3

Slide 3 text

DDD is not prescriptive. Mathias Verraes - What is Domain Driven Design @matarld @chalas_r

Slide 4

Slide 4 text

However, it is able to ease your life when you have to deal with complex business expectations .” “Domain-Driven Design is not a fast way to build software. DDD ≠ RAD William Durand - DDD with Symfony 2: Making things clear @matarld @chalas_r

Slide 5

Slide 5 text

However, it is able to ease your life when you have to deal with complex business expectations .” “Domain-Driven Design is not a fast way to build software. DDD ≠ RAD William Durand - DDD with Symfony 2: Making things clear @matarld @chalas_r

Slide 6

Slide 6 text

@matarld @chalas_r Directory structure Directory structure Directory structure

Slide 7

Slide 7 text

@matarld @chalas_r Directory structure Directory structure Directory structure

Slide 8

Slide 8 text

@matarld @chalas_r Hexagonal architecture

Slide 9

Slide 9 text

Domain Models, Value objects, Events, Repositories @matarld @chalas_r Hexagonal architecture

Slide 10

Slide 10 text

Application Use cases, Application services, DTOs, Commands, Queries Domain Models, Value objects, Events, Repositories @matarld @chalas_r Hexagonal architecture

Slide 11

Slide 11 text

Application Use cases, Application services, DTOs, Commands, Queries Domain Models, Value objects, Events, Repositories Infrastructure Controllers, Databases, Caches, Vendors @matarld @chalas_r Hexagonal architecture

Slide 12

Slide 12 text

Domain integrity is preserved Code is more testable Technological decisions can be deferred Domain is agnostic to the outside world @matarld @chalas_r Hexagonal architecture

Slide 13

Slide 13 text

@matarld @chalas_r Reading is dreaming with open eyes... namespace App\BookStore\Domain\Model; final class Book { public readonly BookId $id; public function __construct( public BookName $name, public BookDescription $description, public Author $author, public Price $price, ) { $this->id = new BookId(); } }

Slide 14

Slide 14 text

PATCH /books/{id} GET /books namespace App\BookStore\Domain\Model; final class Book { public readonly BookId $id; public function __construct( public BookName $name, public BookDescription $description, public Author $author, public Price $price, ) { $this->id = new BookId(); } } GET /books/cheapests DELETE /books/{id} POST /books/anonymize POST /books/{id}/discount [...] @matarld @chalas_r Reading is dreaming with open eyes...

Slide 15

Slide 15 text

#[ApiResource(operations: [ new Get(), new Post('/books/{id}/discount'), ])] final class Book { public function __construct( #[Groups(['book:create'])] #[Assert\NotBlank] public ?BookId $id = null; #[Groups(['book:create', 'book:update'])] #[Assert\Positive] public ?Price $price = null, // ... ) { $this->id = $id ?? new BookId(); } } @matarld @chalas_r Simple as sugar cookies?

Slide 16

Slide 16 text

#[ApiResource(operations: [ new Get(), new Post('/books/{id}/discount'), ])] final class Book { public function __construct( #[Groups(['book:create'])] #[Assert\NotBlank] public ?BookId $id = null; #[Groups(['book:create', 'book:update'])] #[Assert\Positive] public ?Price $price = null, // ... ) { $this->id = $id ?? new BookId(); } } 11 warnings @matarld @chalas_r Simple as sugar cookies?

Slide 17

Slide 17 text

#[ApiResource(operations: [ new Get(), new Post('/books/{id}/discount'), ])] final class Book { public function __construct( #[Groups(['book:create'])] #[Assert\NotBlank] public ?BookId $id = null; #[Groups(['book:create', 'book:update'])] #[Assert\Positive] public ?Price $price = null, // ... ) { $this->id = $id ?? new BookId(); } } 11 warnings 7 errors @matarld @chalas_r Simple as sugar cookies?

Slide 18

Slide 18 text

namespace App\BookStore\Domain\Model; final class Book { public readonly BookId $id; public function __construct( public BookName $name, public BookDescription $description, public Author $author, public Price $price, ) { // ... } } namespace App\BookStore\Infrastructure\ApiPlatform\Resource; #[ApiResource(operations: [new Get(), new Post('...')])] final class BookResource { public function __construct( #[ApiProperty(identifier: true)] #[Groups(['book:create'])] #[Assert\NotBlank] public ?Uuid $id = null; // ... ) { } } @matarld @chalas_r From API Platform to the Domain Model API Resource

Slide 19

Slide 19 text

@matarld @chalas_r The Application Layer

Slide 20

Slide 20 text

Model Domain layer API resource Infrastructure layer Command/Query bus Application layer @matarld @chalas_r Command/Query pattern

Slide 21

Slide 21 text

@matarld @chalas_r Find the cheapest books. Use case #1

Slide 22

Slide 22 text

@matarld @chalas_r Query and QueryHandler namespace App\BookStore\Application\Query; final class FindCheapestBooksQuery { public function __construct( public readonly int $size = 10, ) { } } Query namespace App\BookStore\Application\Query; final class FindCheapestBooksQueryHandler { public function __construct( private BookRepositoryInterface $bookRepository ) { } public function __invoke(FindCheapestBooksQuery $query) { return $this->bookRepository ->withCheapestsFirst() ->withPagination(1, $query->size); } } QueryHandler

Slide 23

Slide 23 text

new GetCollection('/books/cheapest') Operation @matarld @chalas_r GET /books/cheapest API Platform providers FindCheapestBooksQuery

Slide 24

Slide 24 text

new GetCollection('/books/cheapest') Operation @matarld @chalas_r GET /books/cheapest API Platform providers FindCheapestBooksQuery Provider

Slide 25

Slide 25 text

@matarld @chalas_r API Platform providers GET /books/cheapest FooProvider CheapestBooksProvider BarProvider new GetCollection('/books/cheapest') Operation

Slide 26

Slide 26 text

@matarld @chalas_r Let's hold the query in the operation! API Platform providers GET /books/cheapest FooProvider CheapestBooksProvider BarProvider new QueryOperation( '/books/cheapest', FindCheapestBooksQuery::class, ) "Query" operation

Slide 27

Slide 27 text

@matarld @chalas_r Let's hold the query in the operation! API Platform providers GET /books/cheapest FooProvider CheapestBooksProvider BarProvider new QueryOperation( '/books/cheapest', FindCheapestBooksQuery::class, ) "Query" operation

Slide 28

Slide 28 text

@matarld @chalas_r Can we do better? Spoiler: yes

Slide 29

Slide 29 text

@matarld @chalas_r Providers - the new way GET /books/cheapest FooProvider CheapestBooksProvider BarProvider new GetCollection('/books/cheapest') Operation

Slide 30

Slide 30 text

@matarld @chalas_r Let's hold the provider in the operation! Providers - the new way GET /books/cheapest FooProvider CheapestBooksProvider BarProvider new GetCollection( '/books/cheapest', provider: CheapestBooksProvider::class, ) Operation

Slide 31

Slide 31 text

@matarld @chalas_r Let's hold the provider in the operation! Providers - the new way GET /books/cheapest CheapestBooksProvider new GetCollection( '/books/cheapest', provider: CheapestBooksProvider::class, ) Operation

Slide 32

Slide 32 text

@matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class CheapestBooksProvider implements ProviderInterface { /** @return list */ public function provide(...): object|array|null { /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); $resources = []; foreach ($models as $m) { $resources[] = BookResource::fromModel($m); } return $resources; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Slide 33

Slide 33 text

@matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class CheapestBooksProvider implements ProviderInterface { /** @return list */ public function provide(...): object|array|null { /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); $resources = []; foreach ($models as $m) { $resources[] = BookResource::fromModel($m); } return $resources; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** @return list */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable $models */ 8 $models = $this->queryBus->ask(new FindCheapestBooksQuery()); 9 10 $resources = []; 11 foreach ($models as $m) { 12 $resources[] = BookResource::fromModel($m); 13 } 14 15 return $resources; 16 } 17 } 18

Slide 34

Slide 34 text

@matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class CheapestBooksProvider implements ProviderInterface { /** @return list */ public function provide(...): object|array|null { /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); $resources = []; foreach ($models as $m) { $resources[] = BookResource::fromModel($m); } return $resources; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** @return list */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable $models */ 8 $models = $this->queryBus->ask(new FindCheapestBooksQuery()); 9 10 $resources = []; 11 foreach ($models as $m) { 12 $resources[] = BookResource::fromModel($m); 13 } 14 15 return $resources; 16 } 17 } 18 /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 /** @return list */ 5 public function provide(...): object|array|null 6 { 7 8 9 10 $resources = []; 11 foreach ($models as $m) { 12 $resources[] = BookResource::fromModel($m); 13 } 14 15 return $resources; 16 } 17 } 18

Slide 35

Slide 35 text

@matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class CheapestBooksProvider implements ProviderInterface { /** @return list */ public function provide(...): object|array|null { /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); $resources = []; foreach ($models as $m) { $resources[] = BookResource::fromModel($m); } return $resources; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** @return list */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable $models */ 8 $models = $this->queryBus->ask(new FindCheapestBooksQuery()); 9 10 $resources = []; 11 foreach ($models as $m) { 12 $resources[] = BookResource::fromModel($m); 13 } 14 15 return $resources; 16 } 17 } 18 /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 /** @return list */ 5 public function provide(...): object|array|null 6 { 7 8 9 10 $resources = []; 11 foreach ($models as $m) { 12 $resources[] = BookResource::fromModel($m); 13 } 14 15 return $resources; 16 } 17 } 18 /** @var iterable $models */ $models = $this->queryBus->ask(new FindCheapestBooksQuery()); $resources = []; foreach ($models as $m) { $resources[] = BookResource::fromModel($m); } return $resources; namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 /** @return list */ 5 public function provide(...): object|array|null 6 { 7 8 9 10 11 12 13 14 15 16 } 17 } 18

Slide 36

Slide 36 text

@matarld @chalas_r Applying a discount. Use case #2

Slide 37

Slide 37 text

@matarld @chalas_r Command and CommandHandler namespace App\BookStore\Application\Command; final class DiscountBookCommand { public function __construct( public readonly Uuid $id, public readonly int $amount, ) { } } Command namespace App\BookStore\Application\Command; final class DiscountBookCommandHandler { public function __construct( private BookRepositoryInterface $bookRepository, ) { } public function __invoke(DiscountBookCommand $command) { // my super complex logic } } CommandHandler

Slide 38

Slide 38 text

@matarld @chalas_r Processors - the new way POST /books/{id}/discount new Post( '/books/{id}/discount', input: DiscountBookPayload::class, provider: BookItemProvider::class, ) Operation

Slide 39

Slide 39 text

@matarld @chalas_r Processors - the new way POST /books/{id}/discount DiscountBookProcessor new Post( '/books/{id}/discount', input: DiscountBookPayload::class, provider: BookItemProvider::class, processor: DiscountBookProcessor::class, ) Operation

Slide 40

Slide 40 text

@matarld @chalas_r Domain Appli Infra Command processors' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class DiscountBookProcessor implements ProcessorInterface { /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed { $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); $model = $this->queryBus->ask(new FindBookQuery($command->id)); return BookResource::fromModel($model); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Slide 41

Slide 41 text

@matarld @chalas_r Domain Appli Infra Command processors' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class DiscountBookProcessor implements ProcessorInterface { /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed { $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); $model = $this->queryBus->ask(new FindBookQuery($command->id)); return BookResource::fromModel($model); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 5 6 7 8 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20

Slide 42

Slide 42 text

@matarld @chalas_r Domain Appli Infra Command processors' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class DiscountBookProcessor implements ProcessorInterface { /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed { $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); $model = $this->queryBus->ask(new FindBookQuery($command->id)); return BookResource::fromModel($model); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 5 6 7 8 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20 $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 11 12 13 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20

Slide 43

Slide 43 text

@matarld @chalas_r Domain Appli Infra Command processors' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class DiscountBookProcessor implements ProcessorInterface { /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed { $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); $model = $this->queryBus->ask(new FindBookQuery($command->id)); return BookResource::fromModel($model); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 5 6 7 8 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20 $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 11 12 13 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20 $model = $this->queryBus->ask(new FindBookQuery($command->id)); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 16 17 return BookResource::fromModel($model); 18 } 19 } 20

Slide 44

Slide 44 text

@matarld @chalas_r Domain Appli Infra Command processors' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; final class DiscountBookProcessor implements ProcessorInterface { /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed { $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); $model = $this->queryBus->ask(new FindBookQuery($command->id)); return BookResource::fromModel($model); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; /** * @var DiscountBookPayload $data * @return BookResource */ public function process(...): mixed 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 5 6 7 8 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20 $this->commandBus->dispatch(new DiscountBookCommand( id: $context['previous_data']->id, discountPercentage: $data->discountPercentage, )); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 11 12 13 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 return BookResource::fromModel($model); 18 } 19 } 20 $model = $this->queryBus->ask(new FindBookQuery($command->id)); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 16 17 return BookResource::fromModel($model); 18 } 19 } 20 return BookResource::fromModel($model); namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider; 1 2 final class DiscountBookProcessor implements ProcessorInterface 3 { 4 /** 5 * @var DiscountBookPayload $data 6 * @return BookResource 7 */ 8 public function process(...): mixed 9 { 10 $this->commandBus->dispatch(new DiscountBookCommand( 11 id: $context['previous_data']->id, 12 discountPercentage: $data->discountPercentage, 13 )); 14 15 $model = $this->queryBus->ask(new FindBookQuery($command->id)); 16 17 18 } 19 } 20

Slide 45

Slide 45 text

@matarld @chalas_r Subscribing to new books. Use case #3

Slide 46

Slide 46 text

@matarld @chalas_r Bounded contexts Subscription BookStore Payment Hexagonal RAD

Slide 47

Slide 47 text

@matarld @chalas_r RAD API Platform CRUD namespace App\Subscription\Entity; #[ApiResource(operations: [new GetCollection(), new Post()])] #[ORM\Entity] class Subscription { public function __construct( #[ApiProperty(identifier: true)] #[ORM\Id] #[ORM\Column(type: 'uuid', unique: true)] public ?Uuid $id = null, #[Assert\NotBlank(groups: ['create'])] #[Assert\Email(groups: ['create', 'Default'])] #[ORM\Column(name: 'name', nullable: false)] public ?string $email = null, ) { } }

Slide 48

Slide 48 text

@matarld @chalas_r Wrap it up Domain

Slide 49

Slide 49 text

@matarld @chalas_r ├── BookStore │ ├── Application │ │ ├── Command │ │ │ ├── CreateSubscriptionCommand.php │ │ │ ├── CreateSubscriptionCommandHandler.php │ │ │ ├── DiscountBookCommand.php │ │ │ ├── DiscountBookCommandHandler.php │ │ │ └── ... │ │ └── Query │ │ ├── FindBookQuery.php │ │ ├── FindBookQueryHandler.php │ │ ├── FindCheapestBooksQuery.php │ │ ├── FindCheapestBooksQueryHandler.php │ │ └── ... │ ├── Domain │ └── Infrastructure └── Subscription Wrap it up Domain Application

Slide 50

Slide 50 text

@matarld @chalas_r ├── BookStore │ ├── Application │ │ ├── Command │ │ │ ├── CreateSubscriptionCommand.php │ │ │ ├── CreateSubscriptionCommandHandler.php │ │ │ ├── DiscountBookCommand.php │ │ │ ├── DiscountBookCommandHandler.php │ │ │ └── ... │ │ └── Query │ │ ├── FindBookQuery.php │ │ ├── FindBookQueryHandler.php │ │ ├── FindCheapestBooksQuery.php │ │ ├── FindCheapestBooksQueryHandler.php │ │ └── ... │ ├── Domain │ └── Infrastructure └── Subscription ├── BookStore │ ├── Application │ ├── Domain │ └── Infrastructure │ └── ApiPlatform │ ├── Payload │ │ └── DiscountBookPayload.php │ ├── Resource │ │ └── BookResource.php │ └── State │ ├── Processor │ │ ├── AnonymizeBooksProcessor.php │ │ └── DiscountBookProcessor.php │ └── Provider │ ├── BookItemProvider.php │ └── CheapestBooksProvider.php └── Subscription Wrap it up Domain Application Infrastructure

Slide 51

Slide 51 text

@matarld @chalas_r ├── BookStore │ ├── Application │ │ ├── Command │ │ │ ├── CreateSubscriptionCommand.php │ │ │ ├── CreateSubscriptionCommandHandler.php │ │ │ ├── DiscountBookCommand.php │ │ │ ├── DiscountBookCommandHandler.php │ │ │ └── ... │ │ └── Query │ │ ├── FindBookQuery.php │ │ ├── FindBookQueryHandler.php │ │ ├── FindCheapestBooksQuery.php │ │ ├── FindCheapestBooksQueryHandler.php │ │ └── ... │ ├── Domain │ └── Infrastructure └── Subscription ├── BookStore │ ├── Application │ ├── Domain │ └── Infrastructure │ └── ApiPlatform │ ├── Payload │ │ └── DiscountBookPayload.php │ ├── Resource │ │ └── BookResource.php │ └── State │ ├── Processor │ │ ├── AnonymizeBooksProcessor.php │ │ └── DiscountBookProcessor.php │ └── Provider │ ├── BookItemProvider.php │ └── CheapestBooksProvider.php └── Subscription ├── BookStore └── Subscription └── Entity └── Subscription.php Wrap it up Domain Application Infrastructure RAD

Slide 52

Slide 52 text

@matarld @chalas_r Wrap it up #[ApiResource( operations: [ new GetCollection( '/books/cheapest', provider: CheapestBooksProvider::class, ), new Post( '/books/{id}/discount', input: DiscountBookPayload::class, provider: BookItemProvider::class, processor: DiscountBookProcessor::class, ), ], )] final class BookResource Business hexagonal Hexagonal business #[ApiResource( operations: [new Get(), new Post()], )] final class Subscription RAD CRUD

Slide 53

Slide 53 text

@matarld @chalas_r And it just works!

Slide 54

Slide 54 text

Thanks! https://github.com/mtarld/apip-ddd @matarld @chalas_r @coopTilleuls