diff --git a/core/dto.md b/core/dto.md index ca47d83cc14..92bdfb36ea6 100644 --- a/core/dto.md +++ b/core/dto.md @@ -1,215 +1,352 @@ # Using Data Transfer Objects (DTOs) -

Custom Resources screencast
Watch the Custom Resources screencast

+

Custom Resources screencast -As stated in [the general design considerations](design.md), in most cases [the DTO pattern](https://en.wikipedia.org/wiki/Data_transfer_object) should be implemented using an API Resource class representing the public data model exposed through the API and [a custom State Provider](state-providers.md). In such cases, the class marked with `#[ApiResource]` will act as a DTO. +Watch the Custom Resources screencast

-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. +The DTO pattern isolates your public API contract from your internal data model (Entities). This decoupling allows you to evolve your data structure without breaking the API and provides finer control over validation and serialization. -## Implementing a Write Operation With an Input Different From the Resource +In API Platform, [the general design considerations](design.md) recommended pattern is [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) as a Resource: the class marked with `#[ApiResource]` is the DTO, effectively becoming the "contract" of your API. -Using an input, the request body will be denormalized to the input instead of your resource class. +This reference covers three implementation strategies: -```php - [!WARNING] +> You must apply the `#[Map]` attribute to your DTO class. This signals API Platform to use the Object Mapper for transforming data between the Entity and the DTO. For Write operations to work, you must also apply `#[Map]` to your Entity (see below). + +This technique is great for automating standard CRUD operations by linking the DTO to an Entity. By default, this exposes all standard operations (Get, GetCollection, Post, Put, Patch, Delete). + +#### The API Resource (DTO) ```php - */ -final class UserResetPasswordProcessor implements ProcessorInterface -{ - /** - * @param UserResetPasswordDto $data - * - * @throws NotFoundHttpException - */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User + public static function formatPrice(mixed $price, object $source, ?object $target): int|string { - if ('user@example.com' === $data->email) { - return new User(email: $data->email, id: 1); + // Transform Entity (int) to DTO (string with $) + if ($target instanceof self) { + return number_format($price / 100, 2).'$'; } - throw new NotFoundHttpException(); + // Transform DTO (string with $) to Entity (int) + if ($target instanceof BookEntity) { + return 100 * (int) str_replace('$', '', $price); + } + + throw new \LogicException(\sprintf('Unexpected "%s" source.', $source::class)); } } ``` -In some cases, using an input DTO is a way to avoid serialization groups. - -## Use Symfony Messenger With an Input DTO +#### The Entity -Let's use a message that will be processed by [Symfony Messenger](https://symfony.com/components/Messenger). API Platform has an [integration with messenger](../symfony/messenger.md), to use a DTO as input you need to specify the `input` attribute: +To allow the Object Mapper to transform the DTO back into an Entity (required for POST, PUT, etc.), you must also add the `#[Map]` attribute to your Entity class. ```php [!NOTE] +**Note:** For MongoDB, use `ApiPlatform\Doctrine\Odm\State\Options` instead. -## Implementing a Read Operation With an Output Different From the Resource +### 2. Automated Mapped Inputs -To return another representation of your data in a [State Provider](./state-providers.md) 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. +When an operation requires specific input data (different from the resource representation), you can define an input DTO. + +If `stateOptions` is present and the classes have the correct `#[Map]` attributes, API Platform's built-in Object Mapper Processor handles the transformation chain automatically: + +1. Deserializes request to Input DTO. +2. Maps Input DTO -> Entity. +3. Persists Entity. +4. Maps Entity -> Resource DTO (Output). + +This can be useful for Create/Update operations (POST, PUT, PATCH) where the logic is simply "save this data". + +#### The Resource Configuration ```php BookEntity -> Book Resource + new Post( + input: CreateBook::class + ), + // The processor will transform UpdateBook -> BookEntity -> Book Resource + new Put( + input: UpdateBook::class + ) + ] +)] +#[Map(source: BookEntity::class)] +class Book +{ + // ... properties +} +``` + +#### The Input DTOs + +```php + - */ -final class BookRepresentationProvider implements ProviderInterface +final class UpdateBook { - public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation - { - return new AnotherRepresentation(); - } + #[Assert\NotBlank] + public ?string $description = null; + + #[Assert\Positive] + public ?int $price = null; } ``` -## Implementing a Write Operation With an Output Different From the Resource +#### The Entity (Updated for Inputs) -For returning another representation of your data in a [State Processor](./state-processors.md), you should specify your processor class in -the `processor` attribute and same for your `output`. +Ensure your Entity also knows how to map from these specific inputs: - +```php + - - - - - - - - - - +```php + +#### The Custom Processor -Here the `$data` attribute represents an instance of your resource. +We recommend injecting the `ObjectMapperInterface` into your processor to handle the final transformation (Entity -> Resource) to keep your code clean. ```php + * @implements ProcessorInterface */ -final class BookRepresentationProcessor implements ProcessorInterface +final class DiscountBookProcessor implements ProcessorInterface { - /** - * @param Book $data - */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation + public function __construct( + private BookRepository $repository, + private ObjectMapperInterface $objectMapper, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Book { - return new AnotherRepresentation( - $data->getId(), - $data->getTitle(), - // etc. - ); + // 1. Validate that we received the expected Input DTO + assert($data instanceof DiscountBook); + + // 2. Fetch the entity manually using the ID from the URL + $bookEntity = $this->repository->find($uriVariables['id']); + + if (!$bookEntity) { + throw new \Exception('Book not found'); + } + + // 3. Execute Domain Logic + $bookEntity->discount($data->percentage); + + // 4. Persist changes + $this->repository->save($bookEntity); + + // 5. Return the Resource DTO using the Object Mapper + return $this->objectMapper->map($bookEntity, Book::class); } } ``` + +### Implementation Details: The Object Mapper Magic + +Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider` and `ApiPlatform\State\Processor\ObjectMapperProcessor`. + +These classes act as decorators around the standard Provider/Processor chain. They are activated when: + +* The Object Mapper component is available. +* `stateOptions` are configured with an `entityClass` (or `documentClass` for ODM). +* The Resource (and Entity for writes) classes have the `#[Map]` attribute. + +#### How it works internally + +**Read (GET):** + +The `ObjectMapperProvider` delegates fetching the data to the underlying Doctrine provider (which returns an Entity). It then uses `$objectMapper->map($entity, $resourceClass)` to transform the Entity into your DTO Resource. + +**Write (POST/PUT/PATCH):** + +The `ObjectMapperProcessor` receives the deserialized Input DTO. It uses `$objectMapper->map($inputDto, $entityClass)` to transform the input into an Entity instance. It then delegates to the underlying Doctrine processor (to persist the Entity). Finally, it maps the persisted Entity back to the Output DTO Resource. +