main Using Data Transfer Objects (DTOs)

Custom Resources screencast
Watch the Custom Resources screencast

As stated in the general design considerations, in most cases the DTO pattern should be implemented using an API Resource class representing the public data model exposed through the API and a custom State Provider. In such cases, the class marked with #[ApiResource] will act as a DTO.

However, it’s sometimes useful to use a specific class to represent the input or output data structure related to an operation. These techniques are useful to document your API properly (using Hydra or OpenAPI) and will often be used on POST operations.

# Implementing a Write Operation With an Input Different From the Resource

Using an input, the request body will be denormalized to the input instead of your resource class.

<?php
// api/src/Dto/UserResetPasswordDto.php with Symfony or app/Dto/UserResetPasswordDto.php with Laravel
namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;

final class UserResetPasswordDto
{
    #[Assert\Email]
    public $email;
}
<?php
// api/src/Model/User.php with Symfony or app/Model/User.php with Laravel
namespace App\Model;

use ApiPlatform\Metadata\Post;
use App\Dto\UserResetPasswordDto;
use App\State\UserResetPasswordProcessor;

#[Post(input: UserResetPasswordDto::class, processor: UserResetPasswordProcessor::class)]
final class User {}

And the processor:

<?php
// api/src/State/UserResetPasswordProcessor.php with Symfony or app/State/UserResetPasswordProcessor.php with Laravel
namespace App\State;

use App\Dto\UserResetPasswordDto;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * @implements ProcessorInterface<UserResetPasswordDto, User>
 */
final class UserResetPasswordProcessor implements ProcessorInterface
{
    /**
     * @param UserResetPasswordDto $data
     *
     * @throws NotFoundHttpException
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
    {
        if ('[email protected]' === $data->email) {
            return new User(email: $data->email, id: 1);
        }

        throw new NotFoundHttpException();
    }
}

In some cases, using an input DTO is a way to avoid serialization groups.

# Use Symfony Messenger With an Input DTO

Let’s use a message that will be processed by Symfony Messenger. API Platform has an integration with messenger, to use a DTO as input you need to specify the input attribute:

<?php
// api/src/Model/SendMessage.php with Symfony or app/Model/SendMessage.php with Laravel
namespace App\Model;

use ApiPlatform\Metadata\Post;
use ApiPlatform\Symfony\Messenger\Processor as MessengerProcessor;
use App\Dto\Message;

#[Post(input: Message::class, processor: MessengerProcessor::class)]
class SendMessage {}

This will dispatch the App\Dto\Message via Symfony Messenger.

# Implementing a Read Operation With an Output Different From the Resource

To return another representation of your data in a State Provider we advise to specify the output attribute of the resource. Note that this technique works without any changes to the resource but your API documentation would be wrong.

<?php
// api/src/ApiResource/Book.php with Symfony or app/ApiResource/Book.php with Laravel
namespace App\ApiResource;

use ApiPlatform\Metadata\Get;
use App\Dto\AnotherRepresentation;
use App\State\BookRepresentationProvider;

#[Get(output: AnotherRepresentation::class, provider: BookRepresentationProvider::class)]
class Book {}
<?php
// api/src/State/BookRepresentationProvider.php with Symfony or app/State/BookRepresentationProvider.php with Laravel
namespace App\State;

use App\Dto\AnotherRepresentation;
use App\Model\Book;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;

/**
 * @implements ProviderInterface<AnotherRepresentation>
 */
final class BookRepresentationProvider implements ProviderInterface
{
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation
    {
        return new AnotherRepresentation();
    }
}

# Implementing a Write Operation With an Output Different From the Resource

For returning another representation of your data in a State Processor, you should specify your processor class in the processor attribute and same for your output.

<?php
// api/src/ApiResource/Book.php with Symfony or app/ApiResource/Book.php with Laravel
namespace App\ApiResource;

use ApiPlatform\Metadata\Post;
use App\Dto\AnotherRepresentation;
use App\State\BookRepresentationProcessor;

#[Post(output: AnotherRepresentation::class, processor: BookRepresentationProcessor::class)]
class Book {}
# api/config/api_platform/resources.yaml
# The YAML syntax is only supported for Symfony
resources:
  App\ApiResource\Book:
    operations:
      ApiPlatform\Metadata\Post:
        output: App\Dto\AnotherRepresentation
        processor: App\State\BookRepresentationProcessor
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->
<!-- The XML syntax is only supported for Symfony -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Post"
                       processor="App\State\BookRepresentationProcessor"
                       output="App\Dto\AnotherRepresentation" />
        </operations>
    </resource>
</resources>

Here the $data attribute represents an instance of your resource.

<?php
// api/src/State/BookRepresentationProcessor.php with Symfony or app/State/BookRepresentationProcessor.php with Laravel

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Dto\AnotherRepresentation;
use App\Model\Book;

/**
 * @implements ProcessorInterface<Book, AnotherRepresentation>
 */
final class BookRepresentationProcessor implements ProcessorInterface
{
     /**
     * @param Book $data
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation
    {
        return new AnotherRepresentation(
            $data->getId(),
            $data->getTitle(),
            // etc.
        );
    }
}

You can also help us improve the documentation of this page.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop