Skip to content

Commit 2a8a1af

Browse files
docs(form-data): support for laravel and update (#2098)
1 parent 4f05ac1 commit 2a8a1af

File tree

1 file changed

+105
-53
lines changed

1 file changed

+105
-53
lines changed

core/form-data.md

Lines changed: 105 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,143 @@
11
# Accept `application/x-www-form-urlencoded` Form Data
22

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.
718
819
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.
920

10-
## Create your `DeserializeListener` Decorator
21+
## Create your `FormRequestProcessorDecorator` processor
1122

1223
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).
1324

1425
```php
1526
<?php
16-
// api/src/EventListener/DeserializeListener.php
27+
// api/src/State/FormRequestProcessorDecorator.php using Symfony or app/State/FormRequestProcessorDecorator.php using Laravel
1728

18-
namespace App\EventListener;
29+
namespace App\State;
1930

20-
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
21-
use ApiPlatform\Symfony\EventListener\DeserializeListener as DecoratedListener;
22-
use ApiPlatform\Util\RequestAttributesExtractor;
31+
use ApiPlatform\State\ProcessorInterface;
2332
use Symfony\Component\HttpFoundation\Request;
24-
use Symfony\Component\HttpKernel\Event\RequestEvent;
33+
use ApiPlatform\Metadata\Operation;
34+
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
2535
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2636

27-
final class DeserializeListener
37+
final class FormRequestProcessorDecorator implements ProcessorInterface
2838
{
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+
) {}
3244

33-
public function __construct(DenormalizerInterface $denormalizer, SerializerContextBuilderInterface $serializerContextBuilder, DecoratedListener $decorated)
45+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
3446
{
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);
4450
}
4551

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);
5154
}
5255

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)
5460
{
55-
if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) {
56-
return;
61+
$attributes = $request->attributes->get('_api_attributes');
62+
if (!$attributes) {
63+
return null;
5764
}
5865

5966
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
60-
$populated = $request->attributes->get('data');
61-
if (null !== $populated) {
62-
$context['object_to_populate'] = $populated;
63-
}
6467

68+
// Deserialize the form data into an entity
6569
$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);
6872
}
6973
}
7074
```
7175

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
7379

7480
```yaml
7581
# api/config/services.yaml
7682
services:
7783
# ...
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+
}
91143
```

0 commit comments

Comments
 (0)