diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index b3d888889f541..7b53fdc9783d4 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -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` diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php index f2e4bee812d79..b186a39c594c8 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -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 * @author Nicolas Grekas + * @author Mateusz Anders */ final class QueryParameterValueResolver implements ValueResolverInterface { @@ -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]; } @@ -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)); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php index 539aaac78ee65..4348f932fd5c6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php @@ -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 { @@ -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()]), @@ -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()]), @@ -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.', ]; } 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