@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