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

[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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  6. @matarld
    @chalas_r
    Directory structure
    Directory structure
    Directory structure

    View full-size slide

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

    View full-size slide

  8. @matarld
    @chalas_r
    Hexagonal architecture

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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();
    }
    }

    View full-size slide

  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...

    View full-size slide

  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?

    View full-size slide

  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?

    View full-size slide

  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?

    View full-size slide

  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

    View full-size slide

  19. @matarld
    @chalas_r
    The Application Layer

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  32. @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

    View full-size slide

  33. @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

    View full-size slide

  34. @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

    View full-size slide

  35. @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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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,
    ) {
    }
    }

    View full-size slide

  48. @matarld
    @chalas_r
    Wrap it up
    Domain

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  53. @matarld
    @chalas_r
    And it just works!

    View full-size slide

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

    View full-size slide