8000 feature #51004 [HttpKernel] Support backed enums in `#[MapQueryParame… · symfony/symfony@c864846 · GitHub
[go: up one dir, main page]

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

+1
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

+29-4
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" 6D47 ;; 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

+75-2
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 F438
[['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 F987 ']]),
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)
0