|
1 | 1 | # Accept `application/x-www-form-urlencoded` Form Data
|
2 | 2 |
|
3 |
| -API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. |
4 |
| -However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional [`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format (HTML form content type). This can easily be done using [the powerful event system](events.md) of the framework. |
5 |
| - |
6 |
| -**⚠ Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF attacks](<https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)>). Be sure to enable proper countermeasures [such as DunglasAngularCsrfBundle](https://github.com/dunglas/DunglasAngularCsrfBundle).** |
| 3 | +API Platform only supports raw documents as request input (encoded in JSON, XML, YAML...). This has many advantages |
| 4 | +including support of types and the ability to send back to the API documents originally retrieved through a `GET` request. |
| 5 | +However, sometimes - for instance, to support legacy clients - it is necessary to accept inputs encoded in the traditional |
| 6 | +[`application/x-www-form-urlencoded`](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1) format |
| 7 | +(HTML form content type). This can easily be done using the powerful [System providers and processors](extending.md#system-providers-and-processors) |
| 8 | +of the framework. |
| 9 | + |
| 10 | +> [!WARNING] |
| 11 | +> Adding support for `application/x-www-form-urlencoded` makes your API vulnerable to [CSRF (Cross-Site Request Forgery)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) attacks. |
| 12 | +> It's crucial to implement proper countermeasures to protect your application. |
| 13 | +> |
| 14 | +> If you're using Symfony, make sure you enable [Stateless CSRF protection](https://symfony.com/blog/new-in-symfony-7-2-stateless-csrf). |
| 15 | +> |
| 16 | +> If you're working with Laravel, refer to the [Laravel CSRF documentation](https://laravel.com/docs/csrf) to ensure |
| 17 | +> adequate protection against such attacks. |
7 | 18 |
|
8 | 19 | In this tutorial, we will decorate the default `DeserializeListener` class to handle form data if applicable, and delegate to the built-in listener for other cases.
|
9 | 20 |
|
10 |
| -## Create your `DeserializeListener` Decorator |
| 21 | +## Create your `FormRequestProcessorDecorator` processor |
11 | 22 |
|
12 | 23 | This decorator is able to denormalize posted form data to the target object. In case of other format, it fallbacks to the original [DeserializeListener](https://github.com/api-platform/core/blob/91dc2a4d6eeb79ea8dec26b41e800827336beb1a/src/Bridge/Symfony/Bundle/Resources/config/api.xml#L85-L91).
|
13 | 24 |
|
14 | 25 | ```php
|
15 | 26 | <?php
|
16 |
| -// api/src/EventListener/DeserializeListener.php |
| 27 | +// api/src/State/FormRequestProcessorDecorator.php using Symfony or app/State/FormRequestProcessorDecorator.php using Laravel |
17 | 28 |
|
18 |
| -namespace App\EventListener; |
| 29 | +namespace App\State; |
19 | 30 |
|
20 |
| -use ApiPlatform\Serializer\SerializerContextBuilderInterface; |
21 |
| -use ApiPlatform\Symfony\EventListener\DeserializeListener as DecoratedListener; |
22 |
| -use ApiPlatform\Util\RequestAttributesExtractor; |
| 31 | +use ApiPlatform\State\ProcessorInterface; |
23 | 32 | use Symfony\Component\HttpFoundation\Request;
|
24 |
| -use Symfony\Component\HttpKernel\Event\RequestEvent; |
| 33 | +use ApiPlatform\Metadata\Operation; |
| 34 | +use ApiPlatform\Serializer\SerializerContextBuilderInterface; |
25 | 35 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
26 | 36 |
|
27 |
| -final class DeserializeListener |
| 37 | +final class FormRequestProcessorDecorator implements ProcessorInterface |
28 | 38 | {
|
29 |
| - private $decorated; |
30 |
| - private $denormalizer; |
31 |
| - private $serializerContextBuilder; |
| 39 | + public function __construct( |
| 40 | + private readonly ProcessorInterface $decorated, |
| 41 | + private readonlyDenormalizerInterface $denormalizer, |
| 42 | + private readonly SerializerContextBuilderInterface $serializerContextBuilder |
| 43 | + ) {} |
32 | 44 |
|
33 |
| - public function __construct(DenormalizerInterface $denormalizer, SerializerContextBuilderInterface $serializerContextBuilder, DecoratedListener $decorated) |
| 45 | + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed |
34 | 46 | {
|
35 |
| - $this->denormalizer = $denormalizer; |
36 |
| - $this->serializerContextBuilder = $serializerContextBuilder; |
37 |
| - $this->decorated = $decorated; |
38 |
| - } |
39 |
| - |
40 |
| - public function onKernelRequest(RequestEvent $event): void { |
41 |
| - $request = $event->getRequest(); |
42 |
| - if ($request->isMethodCacheable(false) || $request->isMethod(Request::METHOD_DELETE)) { |
43 |
| - return; |
| 47 | + // If the content type is form data, we process it separately |
| 48 | + if ('form' === $data->getContentType()) { |
| 49 | + return $this->handleFormRequest($data); |
44 | 50 | }
|
45 | 51 |
|
46 |
| - if ('form' === $request->getContentType()) { |
47 |
| - $this->denormalizeFormRequest($request); |
48 |
| - } else { |
49 |
| - $this->decorated->onKernelRequest($event); |
50 |
| - } |
| 52 | + // Delegate the processing to the original processor for other cases |
| 53 | + return $this->decorated->process($data, $operation, $uriVariables, $context); |
51 | 54 | }
|
52 | 55 |
|
53 |
| - private function denormalizeFormRequest(Request $request): void |
| 56 | + /** |
| 57 | + * Handle form requests by deserializing the data into the correct entity |
| 58 | + */ |
| 59 | + private function handleFormRequest(Request $request) |
54 | 60 | {
|
55 |
| - if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) { |
56 |
| - return; |
| 61 | + $attributes = $request->attributes->get('_api_attributes'); |
| 62 | + if (!$attributes) { |
| 63 | + return null; |
57 | 64 | }
|
58 | 65 |
|
59 | 66 | $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
|
60 |
| - $populated = $request->attributes->get('data'); |
61 |
| - if (null !== $populated) { |
62 |
| - $context['object_to_populate'] = $populated; |
63 |
| - } |
64 | 67 |
|
| 68 | + // Deserialize the form data into an entity |
65 | 69 | $data = $request->request->all();
|
66 |
| - $object = $this->denormalizer->denormalize($data, $attributes['resource_class'], null, $context); |
67 |
| - $request->attributes->set('data', $object); |
| 70 | + |
| 71 | + return $this->denormalizer->denormalize($data, 'App\Entity\SomeEntity', null, $context); |
68 | 72 | }
|
69 | 73 | }
|
70 | 74 | ```
|
71 | 75 |
|
72 |
| -## Creating the Service Definition |
| 76 | +Next, configure the `FormRequestProcessorDecorator` according to whether you're using Symfony or Laravel, as shown below: |
| 77 | + |
| 78 | +### Creating the Service Definition using Symfony |
73 | 79 |
|
74 | 80 | ```yaml
|
75 | 81 | # api/config/services.yaml
|
76 | 82 | services:
|
77 | 83 | # ...
|
78 |
| - 'App\EventListener\DeserializeListener': |
79 |
| - tags: |
80 |
| - - { |
81 |
| - name: 'kernel.event_listener', |
82 |
| - event: 'kernel.request', |
83 |
| - method: 'onKernelRequest', |
84 |
| - priority: 2, |
85 |
| - } |
86 |
| - # Autoconfiguration must be disabled to set a custom priority |
87 |
| - autoconfigure: false |
88 |
| - decorates: 'api_platform.listener.request.deserialize' |
89 |
| - arguments: |
90 |
| - $decorated: '@App\EventListener\DeserializeListener.inner' |
| 84 | + App\State\FormRequestProcessorDecorator: |
| 85 | + decorates: api_platform.state.processor |
| 86 | + arguments: |
| 87 | + $decorated: '@App\State\FormRequestProcessorDecorator.inner' |
| 88 | + $denormalizer: '@serializer' |
| 89 | + $serializerContextBuilder: '@api_platform.serializer.context_builder' |
| 90 | + tags: |
| 91 | + - { name: 'api_platform.state.processor' } |
| 92 | +``` |
| 93 | +
|
| 94 | +### Registering a Decorated Processor using Laravel |
| 95 | +
|
| 96 | +```php |
| 97 | +<?php |
| 98 | +// app/Providers/AppServiceProvider.php |
| 99 | + |
| 100 | +namespace App\Providers; |
| 101 | + |
| 102 | +use Illuminate\Support\ServiceProvider; |
| 103 | +use App\State\FormRequestProcessorDecorator; |
| 104 | +use ApiPlatform\Core\State\ProcessorInterface; |
| 105 | +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; |
| 106 | +use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; |
| 107 | + |
| 108 | +class AppServiceProvider extends ServiceProvider |
| 109 | +{ |
| 110 | + public function register() |
| 111 | + { |
| 112 | + $this->app->bind(ProcessorInterface::class, function ($app) { |
| 113 | + $decoratedProcessor = $app->make(ProcessorInterface::class); |
| 114 | + |
| 115 | + return new FormRequestProcessorDecorator( |
| 116 | + $decoratedProcessor, |
| 117 | + $app->make(DenormalizerInterface::class), |
| 118 | + $app->make(SerializerContextBuilderInterface::class) |
| 119 | + ); |
| 120 | + }); |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +## Using your `FormRequestProcessorDecorator` processor |
| 126 | + |
| 127 | +Finally, you can use the processor in your API Resource like this: |
| 128 | + |
| 129 | +```php |
| 130 | +<?php |
| 131 | +// api/src/ApiResource/SomeEntity.php with Symfony or app/ApiResource/SomeEntity.php with Laravel |
| 132 | + |
| 133 | +namespace App\ApiResource; |
| 134 | + |
| 135 | +use ApiPlatform\Metadata\Post; |
| 136 | +use App\State\FormRequestProcessorDecorator; |
| 137 | + |
| 138 | +#[Post(processor: FormRequestProcessorDecorator::class)] |
| 139 | +class SomeEntity |
| 140 | +{ |
| 141 | + //... |
| 142 | +} |
91 | 143 | ```
|
0 commit comments