$30 off During Our Annual Pro Sale. View Details »

[API Platform Con] Domain Driven Design with API Platform 3

Mathias Arlaud
September 15, 2022

[API Platform Con] Domain Driven Design with API Platform 3

At Les-Tilleuls.coop, we like to use API Platform. Mainly because it addresses a lot of questions about web API design, but also for its ease of use. This simplicity, however, comes with a cost that is generally felt when you need to get out of the box.

Fact is that at Les-Tilleuls.coop we also enjoy working on applications with strong business constraints, and even more to get off the rails. And when appropriate, we do so based on the DDD principles.

Hexagonal message-oriented architecture, business logic decoupled from the infrastructure, … So many precepts that become prerequisites when it comes to putting the business at the center of our applications while ensuring their good maintainability.

Mathias Arlaud

September 15, 2022
Tweet

Other Decks in Programming

Transcript

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

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

  3. DDD is not prescriptive. Mathias Verraes - What is Domain

    Driven Design @matarld @chalas_r
  4. 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
  5. 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
  6. @matarld @chalas_r Directory structure Directory structure Directory structure

  7. @matarld @chalas_r Directory structure Directory structure Directory structure

  8. @matarld @chalas_r Hexagonal architecture

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

  10. Application Use cases, Application services, DTOs, Commands, Queries Domain Models,

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

    Value objects, Events, Repositories Infrastructure Controllers, Databases, Caches, Vendors @matarld @chalas_r Hexagonal architecture
  12. 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
  13. @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(); } }
  14. 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...
  15. #[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?
  16. #[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?
  17. #[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?
  18. 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
  19. @matarld @chalas_r The Application Layer

  20. Model Domain layer API resource Infrastructure layer Command/Query bus Application

    layer @matarld @chalas_r Command/Query pattern
  21. @matarld @chalas_r Find the cheapest books. Use case #1

  22. @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
  23. new GetCollection('/books/cheapest') Operation @matarld @chalas_r GET /books/cheapest API Platform providers

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

    FindCheapestBooksQuery Provider
  25. @matarld @chalas_r API Platform providers GET /books/cheapest FooProvider CheapestBooksProvider BarProvider

    new GetCollection('/books/cheapest') Operation
  26. @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
  27. @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
  28. @matarld @chalas_r Can we do better? Spoiler: yes

  29. @matarld @chalas_r Providers - the new way GET /books/cheapest FooProvider

    CheapestBooksProvider BarProvider new GetCollection('/books/cheapest') Operation
  30. @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
  31. @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
  32. @matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider;

    final class CheapestBooksProvider implements ProviderInterface { /** @return list<BookResource> */ public function provide(...): object|array|null { /** @var iterable<Book> $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
  33. @matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider;

    final class CheapestBooksProvider implements ProviderInterface { /** @return list<BookResource> */ public function provide(...): object|array|null { /** @var iterable<Book> $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<BookResource> */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable<Book> $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
  34. @matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider;

    final class CheapestBooksProvider implements ProviderInterface { /** @return list<BookResource> */ public function provide(...): object|array|null { /** @var iterable<Book> $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<BookResource> */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable<Book> $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<Book> $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<BookResource> */ 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
  35. @matarld @chalas_r Domain Appli Infra Query providers' content namespace App\BookStore\Infrastructure\ApiPlatform\State\Provider;

    final class CheapestBooksProvider implements ProviderInterface { /** @return list<BookResource> */ public function provide(...): object|array|null { /** @var iterable<Book> $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<BookResource> */ 1 2 final class CheapestBooksProvider implements ProviderInterface 3 { 4 5 public function provide(...): object|array|null 6 { 7 /** @var iterable<Book> $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<Book> $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<BookResource> */ 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<Book> $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<BookResource> */ 5 public function provide(...): object|array|null 6 { 7 8 9 10 11 12 13 14 15 16 } 17 } 18
  36. @matarld @chalas_r Applying a discount. Use case #2

  37. @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
  38. @matarld @chalas_r Processors - the new way POST /books/{id}/discount new

    Post( '/books/{id}/discount', input: DiscountBookPayload::class, provider: BookItemProvider::class, ) Operation
  39. @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
  40. @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
  41. @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
  42. @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
  43. @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
  44. @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
  45. @matarld @chalas_r Subscribing to new books. Use case #3

  46. @matarld @chalas_r Bounded contexts Subscription BookStore Payment Hexagonal RAD

  47. @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, ) { } }
  48. @matarld @chalas_r Wrap it up Domain

  49. @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
  50. @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
  51. @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
  52. @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
  53. @matarld @chalas_r And it just works!

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