Skip to content

[HttpKernel] Support backed enums in #[MapQueryParameter] #51004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
6.4
---

* Support backed enums in #[MapQueryParameter]
* `BundleInterface` no longer extends `ContainerAwareInterface`
* Add optional `$className` parameter to `ControllerEvent::getAttributes()`
* Add native return types to `TraceableEventDispatcher` and to `MergeExtensionConfigurationPass`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

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

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

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

if ('array' === $argument->getType() || $argument->isVariadic()) {
if ('array' === $type || $argument->isVariadic()) {
$value = (array) $value;
$options['flags'] |= \FILTER_REQUIRE_ARRAY;
} else {
$options['flags'] |= \FILTER_REQUIRE_SCALAR;
}

$filter = match ($argument->getType()) {
$enumType = null;
$filter = match ($type) {
'array' => \FILTER_DEFAULT,
'string' => \FILTER_DEFAULT,
'int' => \FILTER_VALIDATE_INT,
'float' => \FILTER_VALIDATE_FLOAT,
'bool' => \FILTER_VALIDATE_BOOL,
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'))
default => match ($enumType = is_subclass_of($type, \BackedEnum::class) ? (new \ReflectionEnum($type))->getBackingType()->getName() : null) {
'int' => \FILTER_VALIDATE_INT,
'string' => \FILTER_DEFAULT,
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')),
}
};

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

if (null !== $enumType && null !== $value) {
$enumFrom = static function ($value) use ($type) {
if (!\is_string($value) && !\is_int($value)) {
return null;
}

try {
return $type::from($value);
} catch (\ValueError) {
return null;
}
};

$value = \is_array($value) ? array_map($enumFrom, $value) : $enumFrom($value);
}

if (null === $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) {
throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Tests\Fixtures\Suit;

class QueryParameterValueResolverTest extends TestCase
{
Expand Down Expand Up @@ -64,6 +65,13 @@ public static function provideTestResolve(): iterable
[['1', '2'], ['2']],
null,
];
yield 'parameter found and array variadic with parameter not array failure' => [
Request::create('/', 'GET', ['ids' => [['1', '2'], 1]]),
new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]),
[],
NotFoundHttpException::class,
'Invalid query parameter "ids".',
];
yield 'parameter found and string' => [
Request::create('/', 'GET', ['firstName' => 'John']),
new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]),
Expand Down Expand Up @@ -176,6 +184,71 @@ public static function provideTestResolve(): iterable
'Invalid query parameter "isVerified".',
];

yield 'parameter found and backing value' => [
Request::create('/', 'GET', ['suit' => 'H']),
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
[Suit::Hearts],
null,
];
yield 'parameter found and backing value variadic' => [
Request::create('/', 'GET', ['suits' => ['H', 'D']]),
new ArgumentMetadata('suits', Suit::class, true, false, false, attributes: [new MapQueryParameter()]),
[Suit::Hearts, Suit::Diamonds],
null,
];
yield 'parameter found and backing value not int nor string' => [
Request::create('/', 'GET', ['suit' => 1]),
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]),
[],
NotFoundHttpException::class,
'Invalid query parameter "suit".',
];
yield 'parameter found and backing value not int nor string that fallbacks to null on failure' => [
Request::create('/', 'GET', ['suit' => 1]),
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL, flags: \FILTER_NULL_ON_FAILURE)]),
[null],
null,
];
yield 'parameter found and value not valid backing value' => [
Request::create('/', 'GET', ['suit' => 'B']),
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
[],
NotFoundHttpException::class,
'Invalid query parameter "suit".',
];
yield 'parameter found and value not valid backing value that falls back to null on failure' => [
Request::create('/', 'GET', ['suit' => 'B']),
new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
[null],
null,
];
yield 'parameter found and backing type variadic and at least one backing value not int nor string' => [
Request::create('/', 'GET', ['suits' => [1, 'D']]),
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]),
[],
NotFoundHttpException::class,
'Invalid query parameter "suits".',
];
yield 'parameter found and backing type variadic and at least one backing value not int nor string that fallbacks to null on failure' => [
Request::create('/', 'GET', ['suits' => [1, 'D']]),
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
[null],
null,
];
yield 'parameter found and backing type variadic and at least one value not valid backing value' => [
Request::create('/', 'GET', ['suits' => ['B', 'D']]),
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter()]),
[],
NotFoundHttpException::class,
'Invalid query parameter "suits".',
];
yield 'parameter found and backing type variadic and at least one value not valid backing value that falls back to null on failure' => [
Request::create('/', 'GET', ['suits' => ['B', 'D']]),
new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]),
[null],
null,
];

yield 'parameter not found but nullable' => [
Request::create('/', 'GET'),
new ArgumentMetadata('firstName', 'string', false, false, false, true, [new MapQueryParameter()]),
Expand Down Expand Up @@ -203,14 +276,14 @@ public static function provideTestResolve(): iterable
new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]),
[],
\LogicException::class,
'#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float or bool should be used.',
'#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
];
yield 'unsupported type variadic' => [
Request::create('/', 'GET', ['standardClass' => 'test']),
new ArgumentMetadata('standardClass', \stdClass::class, true, false, false, attributes: [new MapQueryParameter()]),
[],
\LogicException::class,
'#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float or bool should be used.',
'#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.',
];
}

Expand Down
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