Skip to content

Commit 51d3d19

Browse files
committed
Update input/output doc + diagrams
1 parent 1113a82 commit 51d3d19

File tree

5 files changed

+196
-59
lines changed

5 files changed

+196
-59
lines changed

core/dto.md

Lines changed: 196 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Using Data Transfer Objects (DTOs)
22

3-
## Specifying an Input or an Output Class
3+
## Specifying an Input or an Output data representation
44

55
For a given resource class, you may want to have a different representation of this class as input (write) or output (read).
66
To do so, a resource can take an input and/or an output class:
@@ -17,100 +17,126 @@ use App\Dto\BookOutput;
1717

1818
/**
1919
* @ApiResource(
20-
* inputClass=BookInput::class,
21-
* outputClass=BookOutput::class
20+
* input=BookInput::class,
21+
* output=BookOutput::class
2222
* )
2323
*/
2424
final class Book
2525
{
2626
}
2727
```
2828

29-
The `input_class` attribute is used during [the deserialization process](serialization.md), when transforming the user provided data to a resource instance.
30-
Similarly, the `output_class` attribute is used during the serialization process, this class represents how the `Book` resource will be represented in the `Response`.
29+
The `input` attribute is used during [the deserialization process](serialization.md), when transforming the user provided data to a resource instance.
30+
Similarly, the `output` attribute is used during [the serialization process](serialization.md), this class represents how the `Book` resource will be represented in the `Response`.
3131

32-
To create a `Book`, we `POST` a data structure corresponding to the `BookInput` class and get back in the response a data structure corresponding to the `BookOuput` class.
32+
The `input` and `output` attributes are taken into account by all the documentation generators (GraphQL and OpenAPI, Hydra).
3333

34-
To persist the input object, a custom [data persister](data-persisters.md) handling `BookInput` instances must be written.
35-
To retrieve an instance of the output class, a custom [data provider](data-providers.md) returning a `BookOutput` instance must be written.
34+
To create a `Book`, we `POST` a data structure corresponding to the `BookInput` class and get back in the response a data structure corresponding to the `BookOuput` class:
3635

37-
The `input_class` and `output_class` attributes are taken into account by all the documentation generators (GraphQL and OpenAPI, Hydra).
36+
![Diagram post input output](images/diagrams/api-platform-post-i-o.png)
3837

39-
## Disabling the Input or the Output
38+
To simplify object transformations we have to implement a Data Transformer that will convert the input into a resource or a resource into an output.
4039

41-
Both the `input_class` and the `output_class` attributes can be set to `false`.
42-
If `input_class` is `false`, the deserialization process will be skipped, and no data persisters will be called.
43-
If `output_class` is `false`, the serialization process will be skipped, and no data providers will be called.
40+
With the following `BookInput`:
4441

45-
## Creating a Service-Oriented endpoint
42+
```php
43+
<?php
44+
// src/Dto/BookInput.php
4645

47-
Sometimes it's convenient to create [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call)-like endpoints.
48-
For example, the application should be able to send an email when someone has lost his password.
46+
namespace App\Dto;
4947

50-
So let's create a basic DTO for this request:
48+
final class BookInput {
49+
public $isbn;
50+
}
51+
```
52+
53+
We can transform the `BookInput` to a `Book` resource instance:
5154

5255
```php
5356
<?php
54-
// api/src/Entity/ResetPasswordRequest.php
57+
// src/DataTransformer/BookInputDataTransformer.php
5558

56-
namespace App\Entity;
59+
namespace App\DataTransformer;
5760

58-
use ApiPlatform\Core\Annotation\ApiResource;
59-
use Symfony\Component\Validator\Constraints as Assert;
61+
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
62+
use App\Dto\BookInput;
6063

61-
/**
62-
* @ApiResource(
63-
* collectionOperations={
64-
* "post"={
65-
* "path"="/users/forgot-password-request"
66-
* },
67-
* },
68-
* itemOperations={},
69-
* outputClass=false
70-
* )
71-
*/
72-
final class ResetPasswordRequest
64+
final class BookInputDataTransformer implements DataTransformerInterface
7365
{
7466
/**
75-
* @var string
76-
*
77-
* @Assert\NotBlank
67+
* {@inheritdoc}
7868
*/
79-
public $username;
69+
public function transform($data, string $to, array $context = [])
70+
{
71+
$book = new Book();
72+
$book->isbn = $data->isbn;
73+
return $book;
74+
}
75+
76+
/**
77+
* {@inheritdoc}
78+
*/
79+
public function supportsTransformation($data, string $to, array $context = []): bool
80+
{
81+
return Book::class === $to && $data instanceof BookInput;
82+
}
8083
}
8184
```
8285

83-
In this case, we disable all operations except `POST`. We also set the `output_class` attribute to `false` to hint API Platform that no data will be returned by this endpoint.
86+
And register it:
87+
88+
```yaml
89+
# api/config/services.yaml
90+
services:
91+
# ...
92+
'App\DataTransformer\BookInputDataTransformer': ~
93+
# Uncomment only if autoconfiguration is disabled
94+
#tags: [ 'api_platform.data_transformer' ]
95+
```
96+
97+
To manage the output, it's exactly the same process. For example with `BookOutput` being:
8498

85-
Then, thanks to [a custom data persister](data-persisters.md), it's possible to trigger some custom logic when the request is received.
99+
```php
100+
<?php
101+
// src/Dto/BookOutput.php
102+
103+
namespace App\Dto;
104+
105+
final class BookOutput {
106+
public $name;
107+
}
108+
```
86109

87-
Create the data persister:
110+
We can transform the `Book` to a `BookOutput` object:
88111

89112
```php
90113
<?php
91-
// api/src/DataPersister/ResetPasswordRequestDataPersister.php
114+
// src/DataTransformer/BookOutputDataTransformer.php
92115

93-
namespace App\DataPersister;
116+
namespace App\DataTransformer;
94117

95-
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
96-
use App\Entity\ResetPasswordRequest;
118+
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
119+
use App\Dto\BookOutput;
120+
use App\Entity\Book;
97121

98-
final class ResetPasswordRequestDataPersister implements DataPersisterInterface
122+
final class BookOutputDataTransformer implements DataTransformerInterface
99123
{
100-
public function supports($data): bool
101-
{
102-
return $data instanceof ResetPasswordRequest;
103-
}
104-
105-
public function persist($data)
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function transform($data, string $to, array $context = [])
106128
{
107-
// Trigger your custom logic here
108-
return $data;
129+
$output = new BookOutput();
130+
$output->name = $data->name;
131+
return $output;
109132
}
110-
111-
public function remove($data)
133+
134+
/**
135+
* {@inheritdoc}
136+
*/
137+
public function supportsTransformation($data, string $to, array $context = []): bool
112138
{
113-
throw new \RuntimeException('"remove" is not supported');
139+
return BookOutput::class === $to && $data instanceof Book;
114140
}
115141
}
116142
```
@@ -121,9 +147,120 @@ And register it:
121147
# api/config/services.yaml
122148
services:
123149
# ...
124-
'App\DataPersister\ResetPasswordRequestDataPersister': ~
150+
'App\DataTransformer\BookOutputDataTransformer': ~
151+
# Uncomment only if autoconfiguration is disabled
152+
#tags: [ 'api_platform.data_transformer' ]
153+
```
154+
155+
## Updating a resource with a custom input
156+
157+
When performing an update (e.g. `PUT` operation), the resource to be updated is read by ApiPlatform before the deserialization phase. To do so, it uses a [data provider](data-providers.md) with the `:id` parameter given in the URL. The *body* of the request is the JSON object sent by the client, it is deserialized and is used to update the previously found resource.
158+
159+
![Diagram put input output](images/diagrams/api-platform-put-i-o.png)
160+
161+
Now, we will update our resource by using a different input representation.
162+
163+
With the following `BookInput`:
164+
165+
```
166+
<?php
167+
// src/Dto/BookInput.php
168+
169+
namespace App\Dto;
170+
171+
final class BookInput {
172+
/**
173+
* @var \App\Entity\Author
174+
*/
175+
public $author;
176+
}
177+
```
178+
179+
We will implement a `BookInputDataTransformer` that transforms the `BookInput` to our `Book` resource instance. In this case, the `Book` (`/books/1`) already exists, so we will just update it.
180+
181+
```
182+
<?php
183+
// src/DataTransformer/BookInputDataTransformer.php
184+
185+
namespace App\DataTransformer;
186+
187+
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
188+
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
189+
use App\Dto\BookInput;
190+
191+
final class BookInputDataTransformer implements DataTransformerInterface
192+
{
193+
/**
194+
* {@inheritdoc}
195+
*/
196+
public function transform($data, string $to, array $context = [])
197+
{
198+
$existingBook = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE];
199+
$existingBook->author = $data->author;
200+
return $existingBook;
201+
}
202+
203+
/**
204+
* {@inheritdoc}
205+
*/
206+
public function supportsTransformation($data, string $to, array $context = []): bool
207+
{
208+
return Book::class === $to && $data instanceof BookInput;
209+
}
210+
}
211+
```
212+
213+
```yaml
214+
# api/config/services.yaml
215+
services:
216+
# ...
217+
'App\DataTransformer\BookInputDataTransformer': ~
125218
# Uncomment only if autoconfiguration is disabled
126-
#tags: [ 'api_platform.data_persister' ]
219+
#tags: [ 'api_platform.data_transformer' ]
220+
```
221+
222+
## Disabling the Input or the Output
223+
224+
Both the `input` and the `output` attributes can be set to `false`.
225+
If `input` is `false`, the deserialization process will be skipped, and no data persisters will be called.
226+
If `output` is `false`, the serialization process will be skipped, and no data providers will be called.
227+
228+
## Input/Output metadata
229+
230+
When specified, `input` and `output` attributes support:
231+
- a string representing the class to use
232+
- a falsy boolean to disable them
233+
- an array to specify more metadata for example `['class' => BookInput::class, 'name' => 'BookInput', 'iri' => '/book_input']`
234+
235+
236+
## Using DTO objects inside resources
237+
238+
Because ApiPlatform can (de)normalize anything in the supported formats (`jsonld`, `jsonapi`, `hal`, etc.), you can use any object you want inside resources. For example, let's say that the `Book` has an `attribute` property that can't be represented by a resource, we can do the following:
239+
240+
```php
241+
<?php
242+
// api/src/Entity/Book.php
243+
244+
namespace App\Entity;
245+
246+
use ApiPlatform\Core\Annotation\ApiResource;
247+
use App\Dto\Attribute;
248+
249+
/**
250+
* @ApiResource(
251+
* input=BookInput::class,
252+
* output=BookOutput::class
253+
* )
254+
*/
255+
final class Book
256+
{
257+
/**
258+
* @var Attribute
259+
**/
260+
public $attribute;
261+
262+
public $isbn;
263+
}
127264
```
128265

129-
Instead of a custom data persister, you'll probably want to leverage [the Symfony Messenger Component integration](messenger.md).
266+
The `Book` `attribute` property will now be an instance of `Attribute` after the (de)normalization phase.
2.66 KB
Binary file not shown.
31.4 KB
Loading
2.98 KB
Binary file not shown.
36.4 KB
Loading

0 commit comments

Comments
 (0)