@matarld
@chalas_r
Mathias Arlaud
mtarld
@matarld
les-tilleuls.coop
Robin Chalas
chalasr
@chalas_r
les-tilleuls.coop
@matarld
@chalas_r
William Durand - DDD with Symfony 2: Making things clear
@matarld
@chalas_r
William Durand - DDD with Symfony 2: Making things clear
@matarld
@chalas_r
@matarld
@chalas_r
API Platform common
└── src
├── DataProvider
│
├── DataPersister
│
├── Entity
│ └── Panda.php
│
└── Repository
└── PandaRepository.php
@matarld
@chalas_r
API Platform hexagonal
└── src
├── Application
│ └── Forest
│ ├── Command
│ ├── Payload
│ ├── Query
│ └── View
├── Domain
│ └── Forest
│ ├── Event
│ ├── Model
│ │ └── Panda.php
│ └── Repository
│ └── PandaRepository.php
└── Infrastructure
└── Forest
└── Repository
@matarld
@chalas_r
The Layers and The Dependency Rule
Domain
Models, Value objects, Events, Repositories
Application
Use cases, Application services, DTOs, Commands, Queries
Infrastructure
Controllers, Databases, Caches, Vendors
@matarld
@chalas_r
Benefits
Domain integrity is preserved
Code is more testable
Technological decisions can be deferred
Domain is agnostic to the outside world
Doctrine
Schema
API Platform
Resource
PHP
Object
@matarld
@chalas_r
// src/Domain/Forest/Model/Panda.php
#[ORM\Entity]
#[ApiResource]
final class Panda
{
public function __construct(
private UuidInterface $uuid,
// ...
#[ORM\Column(type: 'int')]
private int $hungerAmount,
) {
}
// ...
public function isHungry(): bool
{
return $this->hungerAmount > self::HUNGERNESS_THRESHOLD;
}
public function feed(Bamboo $bamboo): void
{
$this->hungerAmount = min(0, $this->hunger - $bamboo->getSize());
}
}
@matarld
@chalas_r
#[ORM\Entity]
#[ApiResource]
final class Panda
{
public function __construct(
private UuidInterface $uuid,
// ...
#[ORM\Column(type: 'int')]
private int $hunger,
) {
}
// ...
}
<!-- src/Infrastructure/Forest/Doctrine/Mapping/Panda.xml -->
<entity name="Acme\Domain\Forest\Model\Panda" table="panda">
<field name="uuid" type="uuid" unique="true" />
<!-- ... -->
<field name="hungry" type="integer" />
</entity>
@matarld
@chalas_r
#[ApiResource]
final class Panda
{
public function __construct(
private UuidInterface $uuid,
// ...
private int $hunger,
) {
}
// ...
}
<!-- src/Infrastructure/Forest/ApiPlatform/Resource/Panda.xml -->
<resource class="Acme\Domain\Forest\Model\Panda">
<itemOperations><!-- ... --></itemOperations>
<collectionOperations><!-- ... --></collectionOperations>
</resource>
> curl /api/pandas/{uuid}
API Platform
ReadListener
@matarld
@chalas_r
ChainItemDataProvider
[resourceClass, operationName, context]
Doctrine
ItemDataProvider
> curl /api/pandas/{uuid}
API Platform
ReadListener
ChainItemDataProvider
[resourceClass, operationName, context]
DependencyInjection
Compiler Pass
Clearing tags
Our DataProvider
@matarld
@chalas_r
@matarld
@chalas_r
Command
Domain
Infra
Query
// src/Application/Forest/Query/FindPandaByUuidQuery.php
final class FindPandaByUuidQuery extends FindByUuidQuery
{
}
FindByUuidQuery
@matarld
@chalas_r
// src/Application/Forest/Query/FindPandaByUuidQueryHandler.php
use Acme\Domain\Forest\Repository\PandaRepository;
final class FindPandaByUuidQueryHandler implements QueryHandler
{
public function __construct(private PandaRepository $repository) {}
final public function __invoke(FindPandaByUuidQuery $query): ?Panda
{
return $this->repository->searchByUuid($query->uuid);
}
}
FindPandaByUuidQuery
Messenger
Query bus
Panda
A new resource operation attribute
@matarld
@chalas_r
<itemOperation name="get">
<attribute name="query">FindPandaByUuidQuery</attribute>
</itemOperation>
// src/Infrastructure/Shared/ApiPlatform/DataProvider/ItemQueryDataProvider.php
final class ItemQueryDataProvider implements DataProviderInterface
{
public function getItem(...): ?object
{
$queryClass = $this->resourceMetadataFactory
->create($resourceClass)
->getItemOperationAttribute($operationName, 'query');
return $this->queryBus->ask(new $queryClass($identifiers['uuid']));
}
public function supports(...): bool { /* ... */}
}
ItemQueryDataProvider
Check that we have a query attribute defined
Ensure that's a
FindByUuidQuery
DependencyInjection
Compiler Pass
@matarld
@chalas_r
// src/Infrastructure/Shared/ApiPlatform/DataProvider/CollectionQueryDataProvider.php
final class CollectionQueryDataProvider implements DataProviderInterface
{
public function getCollection(...): iterable
{
$queryClass = $this->resourceMetadataFactory
->create($resourceClass)
->getCollectionOperationAttribute($operationName, 'query');
return $this->queryBus->ask($queryClass::fromContext($context));
}
}
<collectionOperation name="get">
<attribute name="query">FindPandasQuery</attribute>
</collectionOperation>
CollectionQueryDataProvider
FindPandasQuery
Pandas
FindByCriteriaQuery
@matarld
@chalas_r
Custom DataProvider
FindExtraterrialPandasQuery
Custom DataProvider
ExtraterrialPandaDataProvider
FindPandaByUuidQuery
CRUD DataProvider
ItemQueryDataProvider
> curl -X POST /api/pandas
API Platform
WriteListener
ChainDataPersister
[resourceClass, operationName, context]
DependencyInjection
Compiler Pass
Clearing tags
Our DataPersister
@matarld
@chalas_r
// src/Infrastructure/Shared/ApiPlatform/DataPersister/DataPersister.php
final class DataPersister implements DataPersisterInterface
{
public function persist(...): ?object
{
$commandClass = $this->resourceMetadataFactory->create($resourceClass)
->getOperationAttribute($context, 'command');
return $this->commandBus->dispatch($commandClass::fromModel($data));
}
public function remove(...): void
{
$commandClass = $this->resourceMetadataFactory->create($resourceClass)
->getOperationAttribute($context, 'command');
return $this->commandBus->dispatch($commandClass::fromModel($data));
}
}
DataPersister
@matarld
@chalas_r
PersistCommand
RemoveCommand
Commands
// src/Application/Forest/Command/RemovePandaCommandHandler.php
final class RemovePandaCommandHandler implements CommandHandler
{
public function __construct(private PandaRepository $repository) {}
public function __invoke(RemovePandaCommand $command): void
{
$this->repository->remove($command->id());
}
}
@matarld
@chalas_r
// src/Application/Forest/Command/RemovePandaCommand.php
final class RemovePandaCommand implements RemoveCommand
{
public function __construct(private int $id) {}
public static function fromModel(object $panda): self
{
return new self($panda->id());
}
}
Commands
@matarld
@chalas_r
<itemOperation name="remove">
<attribute name="query">FindPandaByUuidQuery</attribute>
<attribute name="command">RemovePandaCommand</attribute>
</itemOperation>
Delete it
Find a panda by its uuid
ItemQueryDataProvider
FindByUuidQuery
CollectionQueryDataProvider
FindByCriteriaQuery
DataPersister
PersistCommand
DataPersister
RemoveCommand
API Platform
ReadListener
API Platform
WriteListener
@matarld
@chalas_r
Use case
final class Panda
{
public function __construct(private string $name)
{
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
{"name": "Pedro Panda"}
PATCH
Symfony
PropertyAccessor
Public properties
Setters
Constructor arguments
final class Panda
{
public function __construct(private string $name)
{
}
public function name(): string
{
return $this->name;
}
public function rename(string $firstname, string $lastname): void
{
$this->name = sprintf('%s %s', $firstname, $lastname);
}
}
Public properties
Setters
Constructor arguments
@matarld
@chalas_r
Data Transfer Objects
@matarld
@chalas_r
Payload and views
{
"uuid": "...",
"name": "Pedro Panda",
"hungry": true
}
{
"firstname": "Pedro",
"lastname": "Panda"
}
final class Panda
{
public function __construct(
private UuidInterface $uuid,
private string $name,
private int $hunger,
) {
}
// ...
}
Payload
Model
View
Concatenate firstname and lastname
Convert hunger to "hungry" boolean
@matarld
@chalas_r
API Platform configuration
<itemOperation name="get">
<attribute name="input">Acme\Application\Forest\Payload\PandaPayload</attribute>
<attribute name="output">Acme\Application\Forest\View\PandaView</attribute>
</itemOperation>
API Platform
DeserializeListener
API Platform
SerializeListener
@matarld
@chalas_r
Input DTO
// src/Application/Forest/Payload/PandaPayload.php
final class PandaPayload
{
public function __construct(
public readonly ?string $firstname = null,
public readonly ?string $lastname = null,
) {
}
}
// src/Domain/Forest/Model/Panda.php
final class Panda
{
public function __construct(private string $name)
{
}
public function rename(
string $firstname,
string $lastname,
): void {
$this->name = sprintf('%s %s', $firstname, $lastname);
}
}
PandaPayloadDataTransformer
Use rename method
@matarld
@chalas_r
PayloadDataTransformer
// src/Infrastructure/Forest/ApiPlatform/DataTransformer/PandaPayloadDataTransformer.php
final class PandaPayloadDataTransformer implements DataTransformerInterface
{
public function transform($payload, string $to, array $context = []) {
$panda = $context['object_to_populate'] ?? new Panda();
$panda->rename($payload->firstname, $payload->lastname);
return $panda;
}
public function supportsTransformation($data, string $to, array $context = []): bool {
return PandaPayload::class === ($context['input']['class'] ?? null)
&& Panda::class === $to;
}
}
@matarld
@chalas_r
Output DTO
// src/Domain/Forest/Model/Panda.php
final class Panda
{
public function __construct(
private UuidInterface $uuid
private string $name,
private int $hunger,
) {
}
public function isHungry(): bool
{
return $this->hunger > self::HUNGERNESS_THRESHOLD;
}
}
// src/Application/Forest/View/PandaView.php
final class PandaView
{
public function __construct(
public readonly string $uuid,
public readonly string $name,
public readonly bool $hungry,
) {
}
}
PandaViewDataTransformer
Cast uuid, use isHungry method
@matarld
@chalas_r
ViewDataTransformer
// src/Infrastructure/Forest/ApiPlatform/DataTransformer/PandaViewDataTransformer.php
final class PandaViewDataTransformer implements DataTransformer
{
public function transform($panda, string $to, array $context = [])
{
return PandaView((string) $panda->uuid(), $panda->name(), $panda->isHungry());
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return PandaView::class === $to && $data instanceof Panda;
}
}
@matarld
@chalas_r
@matarld
@chalas_r
API Platform is fully extensible.
Don't do this for your personal blog!
Take back control over the domain
Embrace complexity
Write more scalable & testable code
@matarld
@chalas_r