Domain-Driven Design and
API Platform 3
@matarld
@chalas_r
Mathias Arlaud
@matarld
mtarld
les-tilleuls.coop
Robin Chalas
@chalas_r
les-tilleuls.coop
chalasr
DDD is not prescriptive.
@matarld
@chalas_r
DDD ≠ RAD
“Domain-Driven Design is not a fast way to build software.
However, it is able to ease your life when you have to deal with complex business expectations.”
@matarld
@chalas_r
└── src
├── Provider
│
├── Processor
│
├── Entity
│ └── Book.php
│
└── Repository
└── BookRepository.php
@matarld
@chalas_r
Directory structure
Directory structure
Directory structure
@matarld
@chalas_r
└── src
└── BookStore
├── Application
│ ├── Command
│ │ ├── DiscountBookCommand.php
│ │ └── DiscountBookCommandHandler.php
│ └── Query
├── Domain
│ ├── Model
│ │ └── Book.php
│ └── Repository
│ └── BookRepository.php
└── Infrastructure
├── ApiPlatform
└── Doctrine
Directory structure
Directory structure
Directory structure
Domain
Models, Value objects, Events, Repositories
Application
Application services, DTOs, Commands, Queries
Infrastructure
Controllers, Databases, Caches, Vendors
@matarld
@chalas_r
Hexagonal architecture
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
GET /books
GET /books/cheapests
PATCH /books/{id}
DELETE /books/{id}
POST /books/anonymize
POST /books/{id}/discount
[...]
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();
}
}
@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();
}
}
11 warnings
7 errors
#[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?
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;
// ...
) {
}
}
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,
) {
// ...
}
}
Model
@matarld
@chalas_r
From API Platform to the Domain
API Resource
@matarld
@chalas_r
The Application Layer
Command/Query bus
Application layer
@matarld
@chalas_r
Command/Query pattern
Model
Domain layer
API resource
Infrastructure layer
@matarld
@chalas_r
Find the
cheapest books.
Use case #1
@matarld
@chalas_r
Query and QueryHandler
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
namespace App\BookStore\Application\Query;
final class FindCheapestBooksQuery
{
public function __construct(
public readonly int $size = 10,
) {
}
}
Query
GET /books/cheapest
@matarld
@chalas_r
API Platform providers
FindCheapestBooksQuery
Provider
new GetCollection('/books/cheapest')
Operation
@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
new GetCollection('/books/cheapest')
Operation
@matarld
@chalas_r
Can we
do better?
Spoiler: yes
@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
new GetCollection('/books/cheapest')
Operation
@matarld
@chalas_r
Domain
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;
}
}
Appli
Infra
@matarld
@chalas_r
Applying
a discount.
Use case #2
@matarld
@chalas_r
Command and CommandHandler
namespace App\BookStore\Application\Command;
final class DiscountBookCommandHandler
{
public function __construct(
private BookRepositoryInterface $bookRepository,
) {
}
public function __invoke(DiscountBookCommand $command)
{
// my super complex logic
}
}
CommandHandler
namespace App\BookStore\Application\Command;
final class DiscountBookCommand
{
public function __construct(
public readonly Uuid $id,
public readonly int $amount,
) {
}
}
Command
@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
new Post(
'/books/{id}/discount',
input: DiscountBookPayload::class,
provider: BookItemProvider::class,
)
Operation
@matarld
@chalas_r
Domain
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);
}
}
Appli
Infra
@matarld
@chalas_r
Subscribing to
new books.
Use case #3
@matarld
@chalas_r
Bounded contexts
Subscription
BookStore
Payment
Hexagonal
RAD
@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,
) {
}
}
@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
│ │ ├── Model
│ │ │ ├── Book.php
│ │ │ └── Subscription.php
│ │ └── Repository
│ │ ├── BookRepositoryInterface.php
│ │ └── SubscriptionRepositoryInterface.php
│ └── Infrastructure
└── Subscription
├── BookStore
└── Subscription
└── Entity
└── Subscription.php
├── 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
RAD
@matarld
@chalas_r
Wrap it up
Business hexagonal
#[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
Hexagonal business
RAD CRUD
#[ApiResource(
operations: [new Get(), new Post()],
)]
final class Subscription
@matarld
@chalas_r
And it just works!
Thanks!
[APIP Con] DDD & API-P 3
By Robin Chalas
[APIP Con] DDD & API-P 3
- 247