mirror of
https://github.com/symfony/http-kernel.git
synced 2026-03-24 01:12:09 +01:00
feature #52134 [HttpKernel] Add option to map empty data with MapQueryString and MapRequestPayload (Jeroeny)
This PR was merged into the 8.1 branch.
Discussion
----------
[HttpKernel] Add option to map empty data with `MapQueryString` and `MapRequestPayload`
| Q | A
| ------------- | ---
| Branch? | 8.1
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Issues | -
| License | MIT
When `#[MapQueryString]` or `#[MapRequestPayload]` is used on a nullable/default-valued parameter and the query string or request body is empty, the resolver short-circuits and returns `null` without ever calling the serializer. This prevents custom denormalizers from constructing the object.
This PR adds `bool $mapWhenEmpty = false` to both attributes. When `true`, the resolver passes `[]` to `denormalize()` even when no data is present, giving custom denormalizers a chance to populate the DTO.
```php
public function __construct(
#[MapRequestPayload(mapWhenEmpty: true)] SearchFilters $filters,
) {}
```
**Use case:** a DTO where some fields come from the request and others are injected by a custom denormalizer (e.g. from the security context or session):
```php
class SearchFilters {
public function __construct(
public ?string $keyword = null, // from query string
public int $userId = 0, // set by a custom denormalizer
) {}
}
```
Without `mapWhenEmpty`, an empty query string yields `null`, the denormalizer never runs. With `mapWhenEmpty: true`, denormalization proceeds and the custom denormalizer can populate `$userId`.
Commits
-------
5117bb6f178 [HttpKernel] Add argument `$mapWhenEmpty` to `MapQueryString` and `MapRequestPayload` for always attempting denormalization with empty query and request payload
This commit is contained in:
@@ -41,6 +41,7 @@ class MapQueryString extends ValueResolver
|
||||
string $resolver = RequestPayloadValueResolver::class,
|
||||
public readonly int $validationFailedStatusCode = Response::HTTP_NOT_FOUND,
|
||||
public readonly ?string $key = null,
|
||||
public bool $mapWhenEmpty = false,
|
||||
) {
|
||||
parent::__construct($resolver);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class MapRequestPayload extends ValueResolver
|
||||
string $resolver = RequestPayloadValueResolver::class,
|
||||
public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
public readonly ?string $type = null,
|
||||
public bool $mapWhenEmpty = false,
|
||||
) {
|
||||
parent::__construct($resolver);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ CHANGELOG
|
||||
* Deprecate passing a `ControllerArgumentsEvent` to the `ViewEvent` constructor; pass a `ControllerArgumentsMetadata` instead
|
||||
* Support variadic argument with `#[MapRequestPayload]`
|
||||
* Add `#[Serialize]` to serialize values returned by controllers
|
||||
* Add argument `$mapWhenEmpty` to `MapQueryString` and `MapRequestPayload` for always attempting denormalization with empty query and request payload
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -207,7 +207,7 @@ class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscr
|
||||
|
||||
private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object
|
||||
{
|
||||
if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue())) {
|
||||
if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue()) && !$attribute->mapWhenEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -216,8 +216,12 @@ class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscr
|
||||
|
||||
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null
|
||||
{
|
||||
if ('' === ($data = $request->request->all() ?: $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) {
|
||||
return null;
|
||||
if ('' === $data = $request->request->all() ?: $request->getContent()) {
|
||||
if ($attribute->mapWhenEmpty) {
|
||||
$data = [];
|
||||
} elseif ($argument->isNullable() || $argument->hasDefaultValue()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $format = $request->getContentTypeFormat()) {
|
||||
|
||||
@@ -165,6 +165,46 @@ class RequestPayloadValueResolverTest extends TestCase
|
||||
$this->assertSame([null], $event->getArguments());
|
||||
}
|
||||
|
||||
public function testMapQueryStringEmpty()
|
||||
{
|
||||
$payload = new RequestPayload(50);
|
||||
$denormalizer = new RequestPayloadDenormalizer($payload);
|
||||
$serializer = new Serializer([$denormalizer]);
|
||||
$resolver = new RequestPayloadValueResolver($serializer);
|
||||
$argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [
|
||||
MapQueryString::class => new MapQueryString(mapWhenEmpty: true),
|
||||
]);
|
||||
$request = Request::create('/', 'GET');
|
||||
|
||||
$kernel = $this->createStub(HttpKernelInterface::class);
|
||||
$arguments = $resolver->resolve($request, $argument);
|
||||
$event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$resolver->onKernelControllerArguments($event);
|
||||
|
||||
$this->assertSame([$payload], $event->getArguments());
|
||||
}
|
||||
|
||||
public function testMapRequestPayloadEmpty()
|
||||
{
|
||||
$payload = new RequestPayload(50);
|
||||
$denormalizer = new RequestPayloadDenormalizer($payload);
|
||||
$serializer = new Serializer([$denormalizer]);
|
||||
$resolver = new RequestPayloadValueResolver($serializer);
|
||||
$argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [
|
||||
MapRequestPayload::class => new MapRequestPayload(mapWhenEmpty: true),
|
||||
]);
|
||||
$request = Request::create('/', 'POST');
|
||||
|
||||
$kernel = $this->createStub(HttpKernelInterface::class);
|
||||
$arguments = $resolver->resolve($request, $argument);
|
||||
$event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$resolver->onKernelControllerArguments($event);
|
||||
|
||||
$this->assertSame([$payload], $event->getArguments());
|
||||
}
|
||||
|
||||
public function testNullPayloadAndNotDefaultOrNullableArgument()
|
||||
{
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
@@ -1415,3 +1455,25 @@ class FormPayloadWithBool
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class RequestPayloadDenormalizer implements DenormalizerInterface
|
||||
{
|
||||
public function __construct(private RequestPayload $payload)
|
||||
{
|
||||
}
|
||||
|
||||
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
return RequestPayload::class === $type;
|
||||
}
|
||||
|
||||
public function getSupportedTypes(?string $format = null): array
|
||||
{
|
||||
return [RequestPayload::class => true];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user