Skip to content

Commit c864846

Browse files
committed
feature #51004 [HttpKernel] Support backed enums in #[MapQueryParameter] (andersmateusz)
This PR was merged into the 6.4 branch. Discussion ---------- [HttpKernel] Support backed enums in `#[MapQueryParameter]` | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #50910 | License | MIT | Doc PR | I think documentation about MapQueryParameter is missing I had two options to introduce this feature. Extending `QueryParameterValueResolver` or extending `BackedEnumValueResolver`. Both options have it's prons and cons. Extending `QueryParameterValueResolver` is not consistent with DRY principle, but on the other hand does not mislead users about usage of `ValueResolverInterface` (`#[MapQueryParameter]`takes `resolver` argument but only `QueryParameterValueResolver` resolves value unless creating your own implementation of the interface). I have chosen to extend `QueryParameterValueResolver`. I think in the future separation of concerns should be introduced (resolving query params, resolving attributes etc.). For example by typing resolvers with new interface and using intersection types. Commits ------- 487f5f8 [HttpKernel] Support backed enums in #[MapQueryParameter]
2 parents 3db10d5 + 487f5f8 commit c864846

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.4
55
---
66

7+
* Support backed enums in #[MapQueryParameter]
78
* `BundleInterface` no longer extends `ContainerAwareInterface`
89
* Add optional `$className` parameter to `ControllerEvent::getAttributes()`
910
* Add native return types to `TraceableEventDispatcher` and to `MergeExtensionConfigurationPass`

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1919

2020
/**
21+
* Resolve arguments of type: array, string, int, float, bool, \BackedEnum from query parameters.
22+
*
2123
* @author Ruud Kamphuis <ruud@ticketswap.com>
2224
* @author Nicolas Grekas <p@tchwork.com>
25+
* @author Mateusz Anders <anders_mateusz@outlook.com>
2326
*/
2427
final class QueryParameterValueResolver implements ValueResolverInterface
2528
{
@@ -39,8 +42,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
3942
}
4043

4144
$value = $request->query->all()[$name];
45+
$type = $argument->getType();
4246

43-
if (null === $attribute->filter && 'array' === $argument->getType()) {
47+
if (null === $attribute->filter && 'array' === $type) {
4448
if (!$argument->isVariadic()) {
4549
return [(array) $value];
4650
}
@@ -59,24 +63,45 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
5963
'options' => $attribute->options,
6064
];
6165

62-
if ('array' === $argument->getType() || $argument->isVariadic()) {
66+
if ('array' === $type || $argument->isVariadic()) {
6367
$value = (array) $value;
6468
$options['flags'] |= \FILTER_REQUIRE_ARRAY;
6569
} else {
6670
$options['flags'] |= \FILTER_REQUIRE_SCALAR;
6771
}
6872

69-
$filter = match ($argument->getType()) {
73+
$enumType = null;
74+
$filter = match ($type) {
7075
'array' => \FILTER_DEFAULT,
7176
'string' => \FILTER_DEFAULT,
7277
'int' => \FILTER_VALIDATE_INT,
7378
'float' => \FILTER_VALIDATE_FLOAT,
7479
'bool' => \FILTER_VALIDATE_BOOL,
75-
default => throw new \LogicException(sprintf('#[MapQueryParameter] cannot be used on controller argument "%s$%s" of type "%s"; one of array, string, int, float or bool should be used.', $argument->isVariadic() ? '...' : '', $argument->getName(), $argument->getType() ?? 'mixed'))
80+
default => match ($enumType = is_subclass_of($type, \BackedEnum::class) ? (new \ReflectionEnum($type))->getBackingType()->getName() : null) {
81+
'int' => \FILTER_VALIDATE_INT,
82+
'string' => \FILTER_DEFAULT,
83+
default => throw new \LogicException(sprintf('#[MapQueryParameter] cannot be used on controller argument "%s$%s" of type "%s"; one of array, string, int, float, bool or \BackedEnum should be used.', $argument->isVariadic() ? '...' : '', $argument->getName(), $type ?? 'mixed')),
84+
}
7685
};
7786

7887
$value = filter_var($value, $attribute->filter ?? $filter, $options);
7988

89+
if (null !== $enumType && null !== $value) {
90+
$enumFrom = static function ($value) use ($type) {
91+
if (!\is_string($value) && !\is_int($value)) {
92+
return null;
93+
}
94+
95+
try {
96+
return $type::from($value);
97+
} catch (\ValueError) {
98+
return null;
99+
}
100+
};
101+
102+
$value = \is_array($value) ? array_map($enumFrom, $value) : $enumFrom($value);
103+
}
104+
80105
if (null === $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) {
81106
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name));
82107
}

src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
1919
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
2020
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
21+
use Symfony\Component\HttpKernel\Tests\Fixtures\Suit;
2122

2223
class QueryParameterValueResolverTest extends TestCase
2324
{
@@ -64,6 +65,13 @@ public static function provideTestResolve(): iterable
6465
[['1', '2'], ['2']],
6566
null,
6667
];
68+
yield 'parameter found and array variadic with parameter not array failure' => [
69+
Request::create('/', 'GET', ['ids' => [['1', '2'], 1]]),
70+
new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]),
71+
[],
72+
NotFoundHttpException::class,
73+
'Invalid query parameter "ids".',
74+
];
6775
yield 'parameter found and string' => [
6876
Request::create('/', 'GET', ['firstName' => 'John']),
6977
new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]),
@@ -176,6 +184,71 @@ public static function provideTestResolve(): iterable
176184
'Invalid query parameter "isVerified".',
177185
];
178186

187+
yield 'parameter found and backing value' => [
188+
Request::create('/', 'GET', ['suit' => 'H']),
189+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
190+
[Suit::Hearts],
191+
null,
192+
];
193+
yield 'parameter found and backing value variadic' => [
194+
Request::create('/', 'GET', ['suits' => ['H', 'D']]),
195+
new ArgumentMetadata('suits', Suit::class, true, false, false, attributes: [new MapQueryParameter()]),
196+
[Suit::Hearts, Suit::Diamonds],
197+
null,
198+
];
199+
yield 'parameter found and backing value not int nor string' => [
200+
Request::create('/', 'GET', ['suit' => 1]),
201+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]),
202+
[],
203+
NotFoundHttpException::class,
204+
'Invalid query parameter "suit".',
205+
];
206+
yield 'parameter found and backing value not int nor string that fallbacks to null on failure' => [
207+
Request::create('/', 'GET', ['suit' => 1]),
208+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL, flags: \FILTER_NULL_ON_FAILURE)]),
209+
[null],
210+
null,
211+
];
212+
yield 'parameter found and value not valid backing value' => [
213+
Request::create('/', 'GET', ['suit' => 'B']),
214+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
215+
[],
216+
NotFoundHttpException::class,
217+
'Invalid query parameter "suit".',
218+
];
219+
yield 'parameter found and value not valid backing value that falls back to null on failure' => [
220+
Request::create('/', 'GET', ['suit' => 'B']),
221+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
222+
[null],
223+
null,
224+
];
225+
yield 'parameter found and backing type variadic and at least one backing value not int nor string' => [
226+
Request::create('/', 'GET', ['suits' => [1, 'D']]),
227+
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]),
228+
[],
229+
NotFoundHttpException::class,
230+
'Invalid query parameter "suits".',
231+
];
232+
yield 'parameter found and backing type variadic and at least one backing value not int nor string that fallbacks to null on failure' => [
233+
Request::create('/', 'GET', ['suits' => [1, 'D']]),
234+
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
235+
[null],
236+
null,
237+
];
238+
yield 'parameter found and backing type variadic and at least one value not valid backing value' => [
239+
Request::create('/', 'GET', ['suits' => ['B', 'D']]),
240+
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
241+
[],
242+
NotFoundHttpException::class,
243+
'Invalid query parameter "suits".',
244+
];
245+
yield 'parameter found and backing type variadic and at least one value not valid backing value that falls back to null on failure' => [
246+
Request::create('/', 'GET', ['suits' => ['B', 'D']]),
247+
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
248+
[null],
249+
null,
250+
];
251+
179252
yield 'parameter not found but nullable' => [
180253
Request::create('/', 'GET'),
181254
new ArgumentMetadata('firstName', 'string', false, false, false, true, [new MapQueryParameter()]),
@@ -203,14 +276,14 @@ public static function provideTestResolve(): iterable
203276
new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]),
204277
[],
205278
\LogicException::class,
206-
'#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float or bool should be used.',
279+
'#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
207280
];
208281
yield 'unsupported type variadic' => [
209282
Request::create('/', 'GET', ['standardClass' => 'test']),
210283
new ArgumentMetadata('standardClass', \stdClass::class, true, false, false, attributes: [new MapQueryParameter()]),
211284
[],
212285
\LogicException::class,
213-
'#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float or bool should be used.',
286+
'#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
214287
];
215288
}
216289

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy