Skip to content

Commit 8399e86

Browse files
Jibbarthdunglas
authored andcommitted
Feature/document api respond parameters (#576)
* Mention api_respond param in events page * Add doc to use DTO for reading without entity * Update dto.md
1 parent f14f2cd commit 8399e86

File tree

2 files changed

+231
-2
lines changed

2 files changed

+231
-2
lines changed

core/dto.md

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Handling Data Transfer Objects (DTOs)
22

3-
## How to use a DTO for Writing
3+
## How to Use a DTO for Writing
44

55
Sometimes it's easier to use a DTO than an Entity when performing simple
66
operation. For example, the application should be able to send an email when
@@ -9,6 +9,7 @@ someone has lost its password.
99
So let's create a basic DTO for this request:
1010

1111
```php
12+
<?php
1213
// api/src/Api/Dto/ForgotPasswordRequest.php
1314

1415
namespace App\Api\Dto;
@@ -122,3 +123,230 @@ services:
122123
# Uncomment the following line only if you don't use autoconfiguration
123124
#tags: [ 'kernel.event_subscriber' ]
124125
```
126+
127+
## How to Use a DTO for Reading
128+
129+
Sometimes, you need to retrieve data not related to an entity.
130+
For example, the application can send the
131+
[list of supported locales](https://github.com/symfony/demo/blob/master/config/services.yaml#L6)
132+
and the default locale.
133+
134+
So let's create a basic DTO for this datas:
135+
136+
```php
137+
<?php
138+
// api/src/Dto/LocalesList.php
139+
140+
namespace App\Dto;
141+
142+
final class LocalesList
143+
{
144+
/**
145+
* @var array
146+
*/
147+
public $locales;
148+
149+
/**
150+
* @var string
151+
*/
152+
public $defaultLocale;
153+
}
154+
```
155+
156+
And create a controller to send them:
157+
158+
```php
159+
<?php
160+
// api/src/Controller/LocaleController.php
161+
162+
namespace App\Controller;
163+
164+
use App\DTO\LocalesList;
165+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
166+
use Symfony\Component\Routing\Annotation\Route;
167+
168+
class LocaleController extends AbstractController
169+
{
170+
/**
171+
* @Route(
172+
* path="/api/locales",
173+
* name="api_get_locales",
174+
* methods={"GET"},
175+
* defaults={
176+
* "_api_respond"=true,
177+
* "_api_normalization_context"={"api_sub_level"=true}
178+
* }
179+
* )
180+
*/
181+
public function __invoke(): LocalesDTO
182+
{
183+
$response = new LocalesList();
184+
$response->locales = explode('|', $this->getParameter('app_locales'));
185+
$response->defaultLocale = $this->getParameter('locale');
186+
187+
return $response;
188+
}
189+
}
190+
```
191+
192+
As you can see, the controller doesn't return a `Response`, but the data object directly.
193+
Behind the scene, the `SerializeListener` catch the response, and thanks to the `_api_respond`
194+
flag, it serializes the object correctly.
195+
196+
To deal with arrays, we have to set the `api_sub_level` context option to `true`.
197+
It prevents API Platform's normalizers to look for a non-existing class marked as an API resource.
198+
199+
### Adding this Custom DTO reading in Swagger Documentation.
200+
201+
By default, ApiPlatform Swagger UI integration will display documentation only
202+
for ApiResource operations.
203+
In this case, our DTO is not declared as ApiResource, so no documentation will
204+
be displayed.
205+
206+
There is two solutions to achieve that:
207+
208+
#### Use Swagger Decorator
209+
210+
By following the doc about [Override the Swagger Documentation](swagger.md##overriding-the-swagger-documentation)
211+
and adding the ability to retrieve a `_api_swagger_context` in route
212+
parameters, you should be able to display your custom endpoint.
213+
214+
```php
215+
<?php
216+
// src/App/Swagger/ControllerSwaggerDecorator
217+
218+
namespace App\Swagger;
219+
220+
use Symfony\Component\Routing\RouterInterface;
221+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
222+
223+
final class ControllerSwaggerDecorator implements NormalizerInterface
224+
{
225+
private $decorated;
226+
227+
private $router;
228+
229+
public function __construct(
230+
NormalizerInterface $decorated,
231+
RouterInterface $router
232+
) {
233+
$this->decorated = $decorated;
234+
$this->router = $router;
235+
}
236+
237+
public function normalize($object, $format = null, array $context = [])
238+
{
239+
$docs = $this->decorated->normalize($object, $format, $context);
240+
$mimeTypes = $object->getMimeTypes();
241+
foreach ($this->router->getRouteCollection()->all() as $routeName => $route) {
242+
$swaggerContext = $route->getDefault('_api_swagger_context');
243+
if (!$swaggerContext) {
244+
// No swagger_context set, continue
245+
continue;
246+
}
247+
248+
$methods = $route->getMethods();
249+
$uri = $route->getPath();
250+
251+
foreach ($methods as $method) {
252+
// Add available mimesTypes
253+
$swaggerContext['produces'] ?? $swaggerContext['produces'] = $mimeTypes;
254+
255+
$docs['paths'][$uri][\strtolower($method)] = $swaggerContext;
256+
}
257+
}
258+
259+
return $docs;
260+
}
261+
262+
public function supportsNormalization($data, $format = null)
263+
{
264+
return $this->decorated->supportsNormalization($data, $format);
265+
}
266+
}
267+
```
268+
269+
Register it as a service:
270+
271+
```yaml
272+
#config/services.yaml
273+
# ...
274+
'App\Swagger\ControllerSwaggerDecorator':
275+
decorates: 'api_platform.swagger.normalizer.documentation'
276+
arguments: [ '@App\Swagger\ControllerSwaggerDecorator.inner']
277+
autoconfigure: false
278+
```
279+
280+
And finally, complete the Route annotation of your controller like this:
281+
282+
```php
283+
<?php
284+
// api/src/Controller/LocaleController.php
285+
286+
use Nelmio\ApiDocBundle\Annotation\Model;
287+
use Swagger\Annotations as SWG;
288+
289+
//...
290+
291+
/**
292+
* @Route(
293+
* path="/api/locales",
294+
* name="api_get_locales",
295+
* methods={"GET"},
296+
* defaults={
297+
* "_api_respond"=true,
298+
* "_api_normalization_context"={"api_sub_level"=true},
299+
* "_api_swagger_context"={
300+
* "tags"={"Locales"},
301+
* "summary"="Retrieve locales availables",
302+
* "parameters"={},
303+
* "responses"={
304+
* "200"={
305+
* "description"="List of available locales and the default locale",
306+
* "schema"={
307+
* "type"="object",
308+
* "properties"={
309+
* "defaultLocale"={"type"="string"},
310+
* }
311+
* }
312+
* }
313+
* }
314+
* }
315+
* }
316+
* )
317+
*/
318+
public function __invoke()
319+
```
320+
321+
#### Use [NelmioApiDoc](nelmio-api-doc.md)
322+
323+
With NelmioApiDoc, you should be able to add annotations to your controllers :
324+
325+
```php
326+
<?php
327+
// api/src/Controller/LocaleController.php
328+
329+
use Nelmio\ApiDocBundle\Annotation\Model;
330+
use Swagger\Annotations as SWG;
331+
332+
//...
333+
334+
/**
335+
* @Route(
336+
* path="/api/locales",
337+
* name="api_get_locales",
338+
* methods={"GET"},
339+
* defaults={
340+
* "_api_respond"=true,
341+
* "_api_normalization_context"={"api_sub_level"=true}
342+
* }
343+
* )
344+
* @SWG\Tag(name="Locales")
345+
* @SWG\Response(
346+
* response=200,
347+
* description="List of available locales and the default locale",
348+
* @SWG\Schema(ref=@Model(type=LocalesList::class)),
349+
* )
350+
*/
351+
public function __invoke()
352+
```

core/events.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ Constant | Event | Priority |
106106
`PRE_RESPOND` | `kernel.view` | 9 |
107107
`POST_RESPOND` | `kernel.response` | 0 |
108108

109-
Some of those built-in listeners can be enabled/disabled by setting request attributes ([for instance in the `defaults`
109+
Some of those built-in listeners can be enabled/disabled by setting request attributes ([for instance in the `defaults`
110110
attribute of an operation](operations.md#recommended-method)):
111111

112112
Listener | Parameter | Values | Default | Description |
113113
----------------------|----------------|----------------|---------|----------------------------------------|
114114
`ReadListener` | `_api_receive` | `true`/`false` | `true` | set to `false` to disable the listener |
115115
`DeserializeListener` | `_api_receive` | `true`/`false` | `true` | set to `false` to disable the listener |
116116
`ValidateListener``_api_receive` | `true`/`false` | `true` | set to `false` to disable the listener |
117+
`SerializeListener``_api_respond` | `true`/`false` | `true` | set to `false` to disable the listener |

0 commit comments

Comments
 (0)