Mathias Arlaud
Co-Founder & COO @Bakslash - Co-Founder & CTO @Synegram
@matarld
@chalas_r
@matarld
mtarld
les-tilleuls.coop
@chalas_r
les-tilleuls.coop
chalasr
@matarld
@chalas_r
@matarld
@chalas_r
└── src
├── Provider
│
├── Processor
│
├── Entity
│ └── Book.php
│
└── Repository
└── BookRepository.php
@matarld
@chalas_r
@matarld
@chalas_r
└── src
└── BookStore
├── Application
│ ├── Command
│ │ ├── DiscountBookCommand.php
│ │ └── DiscountBookCommandHandler.php
│ └── Query
├── Domain
│ ├── Model
│ │ └── Book.php
│ └── Repository
│ └── BookRepository.php
└── Infrastructure
├── ApiPlatform
└── Doctrine
Models, Value objects, Events, Repositories
Application services, DTOs, Commands, Queries
Controllers, Databases, Caches, Vendors
@matarld
@chalas_r
Domain integrity is preserved
Code is more testable
Technological decisions can be deferred
Domain is agnostic to the outside world
@matarld
@chalas_r
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
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
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,
) {
// ...
}
}
@matarld
@chalas_r
@matarld
@chalas_r
Application layer
@matarld
@chalas_r
Domain layer
Infrastructure layer
@matarld
@chalas_r
Use case #1
@matarld
@chalas_r
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);
}
}
namespace App\BookStore\Application\Query;
final class FindCheapestBooksQuery
{
public function __construct(
public readonly int $size = 10,
) {
}
}
GET /books/cheapest
@matarld
@chalas_r
FindCheapestBooksQuery
Provider
new GetCollection('/books/cheapest')
@matarld
@chalas_r
Let's hold the query in the operation!
GET /books/cheapest
FooProvider
CheapestBooksProvider
BarProvider
new QueryOperation(
'/books/cheapest',
FindCheapestBooksQuery::class,
)
new GetCollection('/books/cheapest')
@matarld
@chalas_r
Spoiler: yes
@matarld
@chalas_r
Let's hold the provider in the operation!
GET /books/cheapest
FooProvider
CheapestBooksProvider
BarProvider
new GetCollection(
'/books/cheapest',
provider: CheapestBooksProvider::class,
)
new GetCollection('/books/cheapest')
@matarld
@chalas_r
Domain
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
Use case #2
@matarld
@chalas_r
namespace App\BookStore\Application\Command;
final class DiscountBookCommandHandler
{
public function __construct(
private BookRepositoryInterface $bookRepository,
) {
}
public function __invoke(DiscountBookCommand $command)
{
// my super complex logic
}
}
namespace App\BookStore\Application\Command;
final class DiscountBookCommand
{
public function __construct(
public readonly Uuid $id,
public readonly int $amount,
) {
}
}
@matarld
@chalas_r
POST /books/{id}/discount
DiscountBookProcessor
new Post(
'/books/{id}/discount',
input: DiscountBookPayload::class,
provider: BookItemProvider::class,
processor: DiscountBookProcessor::class,
)
new Post(
'/books/{id}/discount',
input: DiscountBookPayload::class,
provider: BookItemProvider::class,
)
@matarld
@chalas_r
Domain
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
Use case #3
@matarld
@chalas_r
Subscription
BookStore
Payment
Hexagonal
RAD
@matarld
@chalas_r
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
@matarld
@chalas_r
#[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
#[ApiResource(
operations: [new Get(), new Post()],
)]
final class Subscription
@matarld
@chalas_r
By Mathias Arlaud