From f7896f0766ed5816cf93e4be1bf4ccd8a4412f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 8 Jan 2019 18:30:33 +0100 Subject: [PATCH] Improve the docs for DTOs --- core/dto.md | 433 +++++++++------------------------------------------- 1 file changed, 71 insertions(+), 362 deletions(-) diff --git a/core/dto.md b/core/dto.md index 3c762b42e549..cee4d7ac7f7d 100644 --- a/core/dto.md +++ b/core/dto.md @@ -1,421 +1,130 @@ -# Handling Data Transfer Objects (DTOs) +# Using Data Transfer Objects (DTOs) -## How to Use a DTO for Writing +### Specifying an Input or an Output Class -!> The following isn't recommended anymore, please use [Input/Output](specify-an-input-and-or-output-class) instead - -Sometimes it's easier to use a DTO than an Entity when performing simple -operation. For example, the application should be able to send an email when -someone has lost its password. - -So let's create a basic DTO for this request: +For a given resource class, you may want to have a different representation of this class as input (write) or output (read). +To do so, a resource can take an input and/or an output class: ```php userManager = $userManager; - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::VIEW => ['sendPasswordReset', EventPriorities::POST_VALIDATE], - ]; - } - - public function sendPasswordReset(GetResponseForControllerResultEvent $event) - { - $request = $event->getRequest(); - - if ('api_forgot_password_requests_post_collection' !== $request->attributes->get('_route')) { - return; - } - - $forgotPasswordRequest = $event->getControllerResult(); - - $user = $this->userManager->findOneByEmail($forgotPasswordRequest->email); - - // We do nothing if the user does not exist in the database - if ($user) { - $this->userManager->requestPasswordReset($user); - } - - $event->setResponse(new JsonResponse(null, 204)); - } -} -``` - -Then this class should be registered as a service, then tagged. - -If service autowiring and autoconfiguration are enabled (it's the case by -default), you are done! - -Otherwise, the following configuration is needed: - -```yaml -# api/config/services.yaml -services: - # ... - 'App\Api\EventSubscriber\UserSubscriber': - arguments: - - '@app.manager.user' - # Uncomment the following line only if you don't use autoconfiguration - #tags: [ 'kernel.event_subscriber' ] -``` - -## How to Use a DTO for Reading - -!> The following isn't recommended anymore, please use [Input/Output](specify-an-input-and-or-output-class) instead - -Sometimes, you need to retrieve data not related to an entity. -For example, the application can send the -[list of supported locales](https://github.com/symfony/demo/blob/master/config/services.yaml#L6) -and the default locale. - -So let's create a basic DTO for this datas: - -```php -locales = explode('|', $this->getParameter('app_locales')); - $response->defaultLocale = $this->getParameter('locale'); - - return $response; - } } ``` -As you can see, the controller doesn't return a `Response`, but the data object directly. -Behind the scene, the `SerializeListener` catch the response, and thanks to the `_api_respond` -flag, it serializes the object correctly. +The `input_class` attribute is used during [the deserialization process](serialization.md), when transforming the user provided data to a resource instance. +Similarly, the `output_class` attribute is used during the serialization process, this class represents how the `Book` resource will be represented in the `Response`. -To deal with arrays, we have to set the `api_sub_level` context option to `true`. -It prevents API Platform's normalizers to look for a non-existing class marked as an API resource. +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. -### Adding this Custom DTO reading in Swagger Documentation. +To persist the input object, a custom [data persister](data-persisters.md) handling `BookInput` instances must be written. +To retrieve an instance of the output class, a custom [data provider](data-providers.md) returning a `BookOutput` instance must be written. -By default, ApiPlatform Swagger UI integration will display documentation only -for ApiResource operations. -In this case, our DTO is not declared as ApiResource, so no documentation will -be displayed. - -There is two solutions to achieve that: - -#### Use Swagger Decorator - -By following the doc about [Override the Swagger Documentation](swagger.md#overriding-the-swagger-documentation) -and adding the ability to retrieve a `_api_swagger_context` in route -parameters, you should be able to display your custom endpoint. - -```php -decorated = $decorated; - $this->router = $router; - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decorated->normalize($object, $format, $context); - $mimeTypes = $object->getMimeTypes(); - foreach ($this->router->getRouteCollection()->all() as $routeName => $route) { - $swaggerContext = $route->getDefault('_api_swagger_context'); - if (!$swaggerContext) { - // No swagger_context set, continue - continue; - } +The `input_class` and `output_class` attributes are taken into account by all the documentation generators (GraphQL and OpenAPI, Hydra). - $methods = $route->getMethods(); - $uri = $route->getPath(); +## Disabling the Input or the Output - foreach ($methods as $method) { - // Add available mimesTypes - $swaggerContext['produces'] ?? $swaggerContext['produces'] = $mimeTypes; +Both the `input_class` and the `output_class` attributes can be the to `false`. +If `input_class` is `false`, the deserialization process will be skipped, and the no data persisters will be called. +If `output_class` is `false`, the serialization process will be skipped, and no data providers will be called. - $docs['paths'][$uri][\strtolower($method)] = $swaggerContext; - } - } +## Creating a Service-Oriented endpoint - return $docs; - } - - public function supportsNormalization($data, $format = null) - { - return $this->decorated->supportsNormalization($data, $format); - } -} -``` - -Register it as a service: - -```yaml -#config/services.yaml -# ... - 'App\Swagger\ControllerSwaggerDecorator': - decorates: 'api_platform.swagger.normalizer.documentation' - arguments: [ '@App\Swagger\ControllerSwaggerDecorator.inner'] - autoconfigure: false -``` - -And finally, complete the Route annotation of your controller like this: - -```php -name = $data->name; - $output->id = 1; - - return $output; + // Trigger your custom logic here + return $data; } - + public function remove($data) { - // TODO: implement removal - return null; + throw new \RuntimeException('"remove" is not supported'); } } ``` + +And register it: + +```yaml +# api/config/services.yaml +services: + # ... + 'App\DataPersister\ForgotPasswordRequestDataPersister': ~ + # Uncomment only if autoconfiguration is disabled + #tags: [ 'api_platform.data_persister' ] +``` + +Instead of a custom data provider, you'll probably want to leverage the Symfony Messenger Component integration.