Skip to content

Commit 3a10b7f

Browse files
committed
[HttpKernel] Support backed enums in #[MapQueryParameter]
1 parent 80f1096 commit 3a10b7f

File tree

4 files changed

+64
-3
lines changed

4 files changed

+64
-3
lines changed

UPGRADE-6.4.md

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ HttpFoundation
4343
HttpKernel
4444
----------
4545

46+
* Support backed enums in #[MapQueryParameter]
4647
* `BundleInterface` no longer extends `ContainerAwareInterface`
4748
* Add native return types to `TraceableEventDispatcher` and to `MergeExtensionConfigurationPass`
4849

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

+32-1
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
{
@@ -72,7 +75,17 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
7275
'int' => \FILTER_VALIDATE_INT,
7376
'float' => \FILTER_VALIDATE_FLOAT,
7477
'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'))
78+
default => \call_user_func(static function () use ($argument, $value, $name): int {
79+
if (\is_subclass_of($argument->getType(), \BackedEnum::class)) {
80+
$backingType = (new \ReflectionEnum($argument->getType()))->getBackingType();
81+
if ('int' === $backingType->__toString()) {
82+
return \FILTER_VALIDATE_INT;
83+
} elseif ('string' === $backingType->__toString()) {
84+
return \FILTER_DEFAULT;
85+
}
86+
}
87+
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(), $argument->getType() ?? 'mixed'));
88+
})
7689
};
7790

7891
$value = filter_var($value, $attribute->filter ?? $filter, $options);
@@ -81,7 +94,23 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
8194
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name));
8295
}
8396

97+
$enumFromFunc = static function (&$value) use ($argument, $name): void {
98+
if (!\is_string($value) && !\is_int($value)) {
99+
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s": expecting an int or string, got %s.', $name, \get_debug_type($value)));
100+
}
101+
/** @var class-string<\BackedEnum> $enumType */
102+
$enumType = $argument->getType();
103+
try {
104+
$value = $enumType::from($value);
105+
} catch (\ValueError $e) {
106+
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s": %s', $name, $e->getMessage()), $e);
107+
}
108+
};
109+
84110
if (!\is_array($value)) {
111+
if (\is_subclass_of($argument->getType(), \BackedEnum::class)) {
112+
$enumFromFunc($value);
113+
}
85114
return [$value];
86115
}
87116

@@ -95,6 +124,8 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
95124
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name));
96125
}
97126

127+
\is_subclass_of($argument->getType(), \BackedEnum::class) && \array_walk($filtered, $enumFromFunc);
128+
98129
return $argument->isVariadic() ? $filtered : [$filtered];
99130
}
100131
}

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

+30-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
{
@@ -176,6 +177,33 @@ public static function provideTestResolve(): iterable
176177
'Invalid query parameter "isVerified".',
177178
];
178179

180+
yield 'parameter found and backing value' => [
181+
Request::create('/', 'GET', ['suit' => 'H']),
182+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
183+
[Suit::Hearts],
184+
null,
185+
];
186+
yield 'parameter found and backing value variadic' => [
187+
Request::create('/', 'GET', ['suit' => ['H', 'D']]),
188+
new ArgumentMetadata('suit', Suit::class, true, false, false, attributes: [new MapQueryParameter()]),
189+
[Suit::Hearts, Suit::Diamonds],
190+
null,
191+
];
192+
yield 'parameter found and backing type not int nor string' => [
193+
Request::create('/', 'GET', ['suit' => 1]),
194+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]),
195+
[],
196+
NotFoundHttpException::class,
197+
'Invalid query parameter "suit": expecting an int or string, got bool.',
198+
];
199+
yield 'parameter found and backing type not valid backing value for enum' => [
200+
Request::create('/', 'GET', ['suit' => 10.99]),
201+
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
202+
[],
203+
NotFoundHttpException::class,
204+
'Invalid query parameter "suit":',
205+
];
206+
179207
yield 'parameter not found but nullable' => [
180208
Request::create('/', 'GET'),
181209
new ArgumentMetadata('firstName', 'string', false, false, false, true, [new MapQueryParameter()]),
@@ -203,14 +231,14 @@ public static function provideTestResolve(): iterable
203231
new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]),
204232
[],
205233
\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.',
234+
'#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
207235
];
208236
yield 'unsupported type variadic' => [
209237
Request::create('/', 'GET', ['standardClass' => 'test']),
210238
new ArgumentMetadata('standardClass', \stdClass::class, true, false, false, attributes: [new MapQueryParameter()]),
211239
[],
212240
\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.',
241+
'#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
214242
];
215243
}
216244

0 commit comments

Comments
 (0)