Skip to content

Commit 845d64b

Browse files
committed
dto review
1 parent 31b4718 commit 845d64b

1 file changed

Lines changed: 101 additions & 17 deletions

File tree

core/dto.md

Lines changed: 101 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ final class Book
123123

124124
### Implementation Details: The Object Mapper Magic
125125

126-
Automated mapping relies on two internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider`
127-
and `ApiPlatform\State\Processor\ObjectMapperProcessor`.
126+
Automated mapping relies on three internal classes: `ApiPlatform\State\Provider\ObjectMapperProvider`,
127+
`ApiPlatform\State\Processor\ObjectMapperInputProcessor`, and
128+
`ApiPlatform\State\Processor\ObjectMapperOutputProcessor`.
128129

129130
These classes act as decorators around the standard Provider/Processor chain. They are activated
130131
when:
@@ -133,20 +134,75 @@ when:
133134
- `stateOptions` are configured with an `entityClass` (or `documentClass` for ODM).
134135
- The Resource (and Entity for writes) classes have the `#[Map]` attribute.
135136

136-
#### How it works internally
137-
138-
**Read (GET):**
137+
#### Read flow (GET)
138+
139+
```mermaid
140+
sequenceDiagram
141+
participant Client
142+
participant Provider as Doctrine Provider
143+
participant OMP as ObjectMapperProvider
144+
participant Serializer
145+
146+
Client->>Provider: GET /books/1
147+
Provider->>Provider: Fetch Entity from DB
148+
Provider->>OMP: Entity
149+
OMP->>OMP: map(Entity, output ?? ResourceClass)
150+
OMP->>Serializer: Resource DTO
151+
Serializer->>Client: JSON response
152+
```
139153

140154
The `ObjectMapperProvider` delegates fetching the data to the underlying Doctrine provider (which
141-
returns an Entity). It then uses `$objectMapper->map($entity, $resourceClass)` to transform the
142-
Entity into your DTO Resource.
155+
returns an Entity). It then maps the Entity to the **output class** (if `output:` is configured on
156+
the operation) or the **resource class**, using `$objectMapper->map($entity, $outputOrResourceClass)`.
157+
The `input:` configuration is not used during read operations.
158+
159+
#### Write flow (POST/PUT/PATCH)
160+
161+
```mermaid
162+
sequenceDiagram
163+
participant Client
164+
participant Serializer as Deserializer
165+
participant OMIP as ObjectMapper<br/>InputProcessor
166+
participant VP as ValidateProcessor
167+
participant WP as WriteProcessor
168+
participant OMOP as ObjectMapper<br/>OutputProcessor
169+
participant Ser as Serializer
170+
171+
Client->>Serializer: POST /books (JSON body)
172+
Serializer->>Serializer: Deserialize → input DTO (or Resource)
173+
Serializer->>OMIP: Input DTO
174+
OMIP->>OMIP: map(DTO, Entity)
175+
OMIP->>VP: Entity
176+
VP->>VP: Validate Entity
177+
VP->>WP: Entity
178+
WP->>WP: Persist (Doctrine flush)
179+
WP->>OMOP: Persisted Entity
180+
OMOP->>OMOP: map(Entity, ResourceClass)
181+
OMOP->>Ser: Resource DTO
182+
Ser->>Client: JSON response
183+
```
184+
185+
The serializer deserializes the request body into the **input class** (if `input:` is configured)
186+
or the resource class. The `ObjectMapperInputProcessor` then receives that deserialized object and
187+
maps it to the Entity. For PATCH, it maps onto the existing Entity retrieved by the provider
188+
(stored in `request->attributes['mapped_data']`), so only the properties set by the client are
189+
applied. It then delegates to the underlying Doctrine processor to persist the Entity. After
190+
persistence, `ObjectMapperOutputProcessor` maps the persisted Entity back to the **resource
191+
class**.
192+
193+
#### Without stateOptions (custom or static provider)
194+
195+
`stateOptions` is not required to use the Object Mapper. When it is absent, the three decorator
196+
classes still activate as long as the resource class (and input class for writes) carry the
197+
`#[Map]` attribute. The difference is in what the mapper targets:
143198

144-
**Write (POST/PUT/PATCH):**
199+
- **`ObjectMapperProvider`** maps your provider's return value to the **output class** (if set) or
200+
the **resource class** — determined by `getOutput()['class'] ?? getClass()`.
201+
- **`ObjectMapperInputProcessor`** maps the deserialized input to the **resource class** — it falls
202+
back to `$operation->getClass()` when no entity class is found in `stateOptions`.
145203

146-
The `ObjectMapperProcessor` receives the deserialized Input DTO. It uses
147-
`$objectMapper->map($inputDto, $entityClass)` to transform the input into an Entity instance. It
148-
then delegates to the underlying Doctrine processor (to persist the Entity). Finally, it maps the
149-
persisted Entity back to the Output DTO Resource.
204+
This is useful for non-Doctrine backends (static data, remote APIs, in-memory stores) where you
205+
still want clean DTO separation without writing manual mapping code in a custom processor.
150206

151207
## 2. Automated Mapped Inputs and Outputs
152208

@@ -188,8 +244,10 @@ final class CreateBook
188244

189245
### UpdateBook DTO
190246

247+
For PATCH, properties must be **uninitialized** (no default values). A default value causes every
248+
PATCH request to overwrite that field even when the client did not include it in the request body.
249+
191250
```php
192-
<?php
193251
// src/Api/Dto/UpdateBook.php
194252
namespace App\Api\Dto;
195253

@@ -234,7 +292,18 @@ final class BookCollection
234292

235293
#### Wiring it all together in the Resource
236294

237-
In your Book resource, configure the operations to use these classes via input and output.
295+
In your Book resource, configure the operations to use these classes via `input` and `output`.
296+
297+
> [!NOTE]
298+
> `input:` and `output:` operate at the **serializer** layer: `input:` is the class the request
299+
> body is deserialized into; `output:` is the class the serializer normalizes into the response.
300+
> The ObjectMapper (`map: true`, `#[Map]`) operates at the **state pipeline** layer: the Provider
301+
> maps the Entity to the output class (or resource class), and the InputProcessor maps the input
302+
> DTO to the Entity. These two mechanisms are independent and can be combined safely.
303+
>
304+
> When `stateOptions` is configured, the ObjectMapper maps between your DTO and the Doctrine
305+
> Entity. Without `stateOptions`, the ObjectMapper still works but maps to the resource class
306+
> itself — useful when you bring your own provider.
238307
239308
```php
240309
// src/Api/Resource/Book.php
@@ -243,15 +312,15 @@ In your Book resource, configure the operations to use these classes via input a
243312
stateOptions: new Options(entityClass: BookEntity::class),
244313
operations: [
245314
new Get(),
246-
// Use the specialized Output DTO for collections
315+
// ObjectMapperProvider maps Entity -> BookCollection for this operation
247316
new GetCollection(
248317
output: BookCollection::class
249318
),
250-
// Use the specialized Input DTO for creation
319+
// Serializer deserializes request body into CreateBook; ObjectMapperInputProcessor maps CreateBook -> Entity
251320
new Post(
252321
input: CreateBook::class
253322
),
254-
// Use the specialized Input DTO for updates
323+
// Serializer deserializes request body into UpdateBook; ObjectMapperInputProcessor maps UpdateBook -> existing Entity
255324
new Patch(
256325
input: UpdateBook::class
257326
),
@@ -260,6 +329,21 @@ In your Book resource, configure the operations to use these classes via input a
260329
final class Book { /* ... */ }
261330
```
262331

332+
### Recommendations on input and output
333+
334+
**Declare your operations on the Resource class** — it represents the JSON contract of your API.
335+
Avoid using `output:` on write operations (`Post`, `Put`, `Patch`, `Delete`). The
336+
`ObjectMapperOutputProcessor` already maps the persisted Entity back to the Resource class
337+
automatically. Adding an explicit `output:` on writes creates confusion and can lead to subtle
338+
bugs (see [#7745](https://github.com/api-platform/core/issues/7745)).
339+
340+
The main legitimate use case for `output:` is on `GetCollection` when you need a lighter
341+
representation with fewer fields than the main Resource. Even then, consider whether serialization
342+
groups or a separate Resource class might be clearer.
343+
344+
Use `input:` freely for write operations — it is the right tool for differentiating Create vs
345+
Update validation and accepted fields.
346+
263347
## 3. Custom Business Logic (Custom Processor)
264348

265349
For complex business actions (like applying a discount), standard CRUD mapping isn't enough. You

0 commit comments

Comments
 (0)