From c4a709b2b36d1ada8e6a0479faf5e489ac016ab3 Mon Sep 17 00:00:00 2001 From: klkvsk Date: Tue, 29 Oct 2024 22:54:15 +0300 Subject: [PATCH 1/6] add controller argument attributes to hint ValueResolvers which request parameter bag to use --- .../HttpKernel/Attribute/FromBody.php | 33 ++ .../HttpKernel/Attribute/FromHeader.php | 33 ++ .../HttpKernel/Attribute/FromQuery.php | 33 ++ .../HttpKernel/Attribute/FromRoute.php | 33 ++ .../HttpKernel/Attribute/ValueBagResolver.php | 34 ++ .../BackedEnumValueResolver.php | 9 +- .../DateTimeValueResolver.php | 9 +- .../RequestAttributeValueResolver.php | 7 +- .../ArgumentResolver/UidValueResolver.php | 7 +- .../VariadicValueResolver.php | 9 +- .../Controller/ValueBagResolverTrait.php | 101 +++++ .../Controller/ValueBagResolverTraitTest.php | 413 ++++++++++++++++++ 12 files changed, 713 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Attribute/FromBody.php create mode 100644 src/Symfony/Component/HttpKernel/Attribute/FromHeader.php create mode 100644 src/Symfony/Component/HttpKernel/Attribute/FromQuery.php create mode 100644 src/Symfony/Component/HttpKernel/Attribute/FromRoute.php create mode 100644 src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromBody.php b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php new file mode 100644 index 0000000000000..8fd46f02f307b --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Controller argument tag forcing ValueResolvers to use the value from request's body. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class FromBody extends ValueBagResolver +{ + /** + * @param string|null $name The name of body parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + */ + public function __construct( + string|null $name = null, + int|array|null $filter = null, + int|array|null $options = null, + ) + { + parent::__construct('request', $name, $filter, $options); + } +} diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php new file mode 100644 index 0000000000000..9e25af3de7f8c --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Controller argument tag forcing ValueResolvers to use the value from request's header. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class FromHeader extends ValueBagResolver +{ + /** + * @param string|null $name The name of header. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + */ + public function __construct( + string|null $name = null, + int|array|null $filter = null, + int|array|null $options = null, + ) + { + parent::__construct('headers', $name, $filter, $options); + } +} diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php new file mode 100644 index 0000000000000..70845fc508a34 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Controller argument tag forcing ValueResolvers to use the value from request's query. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class FromQuery extends ValueBagResolver +{ + /** + * @param string|null $name The name of query parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + */ + public function __construct( + string|null $name = null, + int|array|null $filter = null, + int|array|null $options = null, + ) + { + parent::__construct('query', $name, $filter, $options); + } +} diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php new file mode 100644 index 0000000000000..df68c4a6197a9 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Controller argument tag forcing ValueResolvers to use the value from request's route attributes. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class FromRoute extends ValueBagResolver +{ + /** + * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + */ + public function __construct( + string|null $name = null, + int|array|null $filter = null, + int|array|null $options = null, + ) + { + parent::__construct('attributes', $name, $filter, $options); + } +} diff --git a/src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php b/src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php new file mode 100644 index 0000000000000..e9f05df8c9c1e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Defines which value bag from request be used for a given parameter. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class ValueBagResolver +{ + /** + * @param "attributes"|"request"|"query"|"body"|"headers" $bag The bag to consume the value from + * @param string|null $name The name of the parameter in bag. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + */ + public function __construct( + public string $bag, + public string|null $name = null, + public int|array|null $filter = null, + public int|array|null $options = null, + ) + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php index 9193cee060f69..54ed8b2728434 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -24,6 +25,8 @@ */ final class BackedEnumValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function resolve(Request $request, ArgumentMetadata $argument): iterable { if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { @@ -35,14 +38,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return []; } + $valueBag = $this->resolveValueBag($request, $argument); + // do not support if no value can be resolved at all // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. - if (!$request->attributes->has($argument->getName())) { + if (!$valueBag->has($argument->getName())) { return []; } - $value = $request->attributes->get($argument->getName()); + $value = $valueBag->get($argument->getName()); if (null === $value) { return [null]; diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php index 10ea8826f9d25..66d2716d9f45c 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -14,6 +14,7 @@ use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -26,6 +27,8 @@ */ final class DateTimeValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function __construct( private readonly ?ClockInterface $clock = null, ) { @@ -33,11 +36,13 @@ public function __construct( public function resolve(Request $request, ArgumentMetadata $argument): array { - if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$request->attributes->has($argument->getName())) { + $valueBag = $this->resolveValueBag($request, $argument); + + if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$valueBag->has($argument->getName())) { return []; } - $value = $request->attributes->get($argument->getName()); + $value = $valueBag->get($argument->getName()); $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); if (!$value) { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 2a8d48ee30174..7f78f9f47d37d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -22,8 +23,12 @@ */ final class RequestAttributeValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function resolve(Request $request, ArgumentMetadata $argument): array { - return !$argument->isVariadic() && $request->attributes->has($argument->getName()) ? [$request->attributes->get($argument->getName())] : []; + $valueBag = $this->resolveValueBag($request, $argument); + + return !$argument->isVariadic() && $valueBag->has($argument->getName()) ? [ $valueBag->get($argument->getName()) ] : []; } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php index 4a232eb8aeccf..583b3657fba42 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -19,10 +20,14 @@ final class UidValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function resolve(Request $request, ArgumentMetadata $argument): array { + $valueBag = $this->resolveValueBag($request, $argument); + if ($argument->isVariadic() - || !\is_string($value = $request->attributes->get($argument->getName())) + || !\is_string($value = $valueBag->get($argument->getName())) || null === ($uidClass = $argument->getType()) || !is_subclass_of($uidClass, AbstractUid::class, true) ) { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php index d046129f4f455..aecde19ada9e1 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -22,13 +23,17 @@ */ final class VariadicValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function resolve(Request $request, ArgumentMetadata $argument): array { - if (!$argument->isVariadic() || !$request->attributes->has($argument->getName())) { + $valueBag = $this->resolveValueBag($request, $argument); + + if (!$argument->isVariadic() || !$valueBag->has($argument->getName())) { return []; } - $values = $request->attributes->get($argument->getName()); + $values = $valueBag->get($argument->getName()); if (!\is_array($values)) { throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); diff --git a/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php b/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php new file mode 100644 index 0000000000000..2e2c3cb792303 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\ValueBagResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +trait ValueBagResolverTrait +{ + private function resolveValueBag(Request $request, ArgumentMetadata $argument): ParameterBag + { + /** @var ValueBagResolver[] $valueResolverBagAttributes */ + $valueResolverBagAttributes = $argument->getAttributesOfType(ValueBagResolver::class, $argument::IS_INSTANCEOF); + if (!$valueResolverBagAttributes) { + // Fall back to attributes by default to keep BC. + return $request->attributes; + } + + foreach ($valueResolverBagAttributes as $attribute) { + [ $requestBag, $isReturned ] = match ($attribute->bag) { + 'attributes' => [ new ParameterBag($request->attributes->all()), true ], + 'request' => [ new ParameterBag($request->getPayload()->all()), true ], + 'query' => [ new ParameterBag($request->query->all()), true ], + 'headers' => [ $request->headers, false ], + 'files' => [ $request->files, false ], + default => throw new \InvalidArgumentException(sprintf('Unknown bag "%s" for value consumer.', $attribute->bag)), + }; + + $requestParameterName = $attribute->name ?? $argument->getName(); + if ($requestParameterName === '*') { + $value = $requestBag->all(); + } else if ($requestBag->has($requestParameterName)) { + $value = $requestBag->get($requestParameterName); + } else { + continue; + } + + $filter = $attribute->filter ?? match ($argument->getType()) { + 'int' => \FILTER_VALIDATE_INT, + 'float' => \FILTER_VALIDATE_FLOAT, + 'bool' => \FILTER_VALIDATE_BOOL, + default => \FILTER_DEFAULT, + }; + + $options = [ 'flags' => 0 ]; + if (is_int($attribute->options)) { + $options['flags'] = $attribute->options; + } else if (is_array($attribute->options)) { + $options['options'] = $attribute->options['options'] ?? $attribute->options; + $options['flags'] = $attribute->options['flags'] ?? 0; + } + if ($argument->getType() === 'bool') { + $options['flags'] |= FILTER_NULL_ON_FAILURE; + } + if ($argument->getType() === 'array' || $argument->isVariadic()) { + $options['flags'] |= FILTER_REQUIRE_ARRAY | FILTER_FORCE_ARRAY; + } + + $isNullOnFailure = $options['flags'] & FILTER_NULL_ON_FAILURE; + + if ($value !== null) { + if (is_array($filter)) { + if (is_array($value)) { + $filtered = filter_var_array($value, $filter, $options['add_empty'] ?? true); + } else { + $filtered = $isNullOnFailure ? null : false; + } + } else { + $filtered = filter_var($value, $filter, $options); + } + + if (($filtered === false && !$isNullOnFailure) || ($filtered === null && $isNullOnFailure)) { + continue; + } + + $value = $filtered; + } + + // For attributes, query, request bags, return other values too. + // This way, some sophisticated ValueResolvers like EntityValueResolver can receive them. + $valueBag = $isReturned ? $requestBag : new ParameterBag(); + $valueBag->set($argument->getName(), $value); + + return $valueBag; + } + + return new ParameterBag(); + } + +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php new file mode 100644 index 0000000000000..5a92ad3a779a1 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php @@ -0,0 +1,413 @@ + [ 123, 123 ], + '"123"' => [ 123, '123' ], + '"10e2"' => [ null, '10e2' ], + '(empty string)' => [ null, '' ], + ]; + } + + public static function boolValues() + { + return [ + '"true"' => [ true, 'true' ], + '"1"' => [ true, '1' ], + '"on"' => [ true, 'on' ], + '"false"' => [ false, 'false' ], + '"off"' => [ false, 'off' ], + '"0"' => [ false, '0' ], + '(empty string)' => [ false, '' ], + ]; + } + + public function testStringDefault() + { + $args = $this->argumentResolver->getArguments( + new Request(attributes: [ 'foo' => 'bar' ]), + fn(string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + } + + public function testIgnoreWhenNoAttributes() + { + $this->expectExceptionMessage('requires the "$foo"'); + $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => 'bar' ]), + fn(string $foo) => $foo, + ); + + $this->expectExceptionMessage('requires the "$foo"'); + $this->argumentResolver->getArguments( + new Request(request: [ 'foo' => 'bar' ]), + fn(string $foo) => $foo, + ); + } + + public function testStringFromRoute() + { + $args = $this->argumentResolver->getArguments( + new Request(attributes: [ 'foo' => 'bar' ]), + fn(#[FromRoute] string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + } + + public function testStringFromQuery() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => 'bar' ]), + fn(#[FromQuery] string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + + } + + public function testStringFromBody() + { + $args = $this->argumentResolver->getArguments( + new Request(request: [ 'foo' => 'bar' ]), + fn(#[FromBody] string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + } + + public function testStringFromJsonBody() + { + $args = $this->argumentResolver->getArguments( + new Request(server: [ 'HTTP_CONTENT_TYPE' => 'application/json' ], content: json_encode([ 'foo' => 'bar' ])), + fn(#[FromBody] string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + } + + public function testStringFromHeader() + { + $args = $this->argumentResolver->getArguments( + new Request(server: [ 'HTTP_FOO' => 'bar' ]), + fn(#[FromHeader] string $foo) => $foo + ); + + $this->assertEquals([ 'bar' ], $args); + } + + public function testStringFromQueryRenamed() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'theFoo' => 'bar' ]), + fn(#[FromQuery('theFoo')] string $foo) => $foo + ); + + $this->assertSame([ 'bar' ], $args); + } + + public function testWholeBagAsParameter() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'sort' => 'name', 'order' => 'desc' ]), + fn(#[FromQuery('*')] array $sorting) => $sorting + ); + + $this->assertSame([ [ 'sort' => 'name', 'order' => 'desc' ] ], $args); + } + + public function testStringFromHeaderRenamed() + { + $args = $this->argumentResolver->getArguments( + new Request(server: [ 'HTTP_X_FORWARDED_FOR' => 'bar' ]), + fn(#[FromHeader('x-forwarded-for')] string $foo) => $foo, + ); + $this->assertEquals([ 'bar' ], $args); + } + + public function testInt() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => '1' ]), + fn(#[FromQuery] int $page) => $page, + ); + $this->assertEquals([ 1 ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => '' ]), + fn(#[FromQuery] ?int $page) => $page, + ); + $this->assertEquals([ null ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => 'not-an-integer' ]), + fn(#[FromQuery] ?int $page) => $page, + ); + $this->assertEquals([ null ], $args); + } + + public function testMultipleGoodAfterFailed() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => 'not-an-integer' ], request: [ 'page' => 2 ]), + fn(#[FromQuery] #[FromBody] ?int $page) => $page, + ); + $this->assertEquals([ 2 ], $args); + } + + public function testMultipleFailedAfterGood() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => 'not-an-integer' ], request: [ 'page' => 2 ]), + fn(#[FromBody] #[FromQuery] ?int $page) => $page, + ); + $this->assertEquals([ 2 ], $args); + } + + /** @dataProvider intValues */ + public function testIntFromQuery($expected, $actual) + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => $actual ]), + fn(#[FromQuery] ?int $int) => $int, + ); + $this->assertEquals([ $expected ], $args); + } + + public function testFilterFlags() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => '0xff' ]), + fn(#[FromQuery] ?int $int) => $int, + ); + $this->assertEquals([ null ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => '0xff' ]), + fn(#[FromQuery(options: FILTER_FLAG_ALLOW_HEX)] ?int $int) => $int, + ); + $this->assertEquals([ 255 ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => '0xff' ]), + fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX ])] ?int $int) => $int, + ); + $this->assertEquals([ 255 ], $args); + } + + public function testFilterOptions() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => 42 ]), + fn(#[FromQuery] ?int $int) => $int, + ); + $this->assertEquals([ 42 ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => 42 ]), + fn(#[FromQuery(options: [ 'max_range' => 10 ])] ?int $int) => $int, + ); + $this->assertEquals([ null ], $args); + } + + public function testFilterFlagAndOptions() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => 42 ]), + fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 10 ])] ?int $int) => $int, + ); + + $this->assertEquals([ null ], $args); + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'int' => 42 ]), + fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 100 ])] ?int $int) => $int, + ); + $this->assertEquals([ 42 ], $args); + } + + /** @dataProvider boolValues */ + public function testBoolFromQuery($expected, $actual) + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'bool' => $actual ]), + fn(#[FromQuery] bool $bool) => $bool, + ); + $this->assertEquals([ $expected ], $args); + } + + public function testBoolFromQueryMissing() + { + $args = $this->argumentResolver->getArguments( + new Request(query: []), + fn(#[FromQuery] ?bool $bool) => $bool, + ); + $this->assertEquals([ null ], $args); + + $this->expectExceptionMessage('requires the "$bool"'); + $args = $this->argumentResolver->getArguments( + new Request(query: []), + fn(#[FromQuery] bool $bool) => $bool, + ); + $this->assertEquals([ null ], $args); + } + + public function testUidFromQuery() + { + $uid = new UuidV1(); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'uid' => $uid->toString() ]), + fn(#[FromQuery] AbstractUid $uid) => $uid, + ); + $this->assertEquals([ $uid ], $args); + } + + public function testDateFromQuery() + { + $date = new \DateTimeImmutable('2021-01-01 01:02:03'); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'date' => $date->format(\DateTimeInterface::ATOM) ]), + fn(#[FromQuery] \DateTimeInterface $date) => $date, + ); + $this->assertEquals([ $date ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'date' => $date->getTimestamp() ]), + fn(#[FromQuery] \DateTimeInterface $date) => $date, + ); + $this->assertEquals([ $date ], $args); + } + + public function testDateWithMapDateTimeFromQuery() + { + $date = new \DateTimeImmutable('2021-12-01 01:02:03'); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'date' => '2021/01/12 01:02:03' ]), + fn(#[FromQuery] #[MapDateTime(format: 'Y/d/m H:i:s')] \DateTimeInterface $date) => $date, + ); + $this->assertEquals([ $date ], $args); + } + + public function testEnumFromQuery() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'suit' => 'H' ]), + fn(#[FromQuery] Suit $suit) => $suit, + ); + $this->assertEquals([ Suit::Hearts ], $args); + } + + public function testArrayFromQuery() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ '1', '2' ] ]), + fn(#[FromQuery('foo')] array $foo) => $foo, + ); + $this->assertSame([ [ '1', '2' ] ], $args); + } + + public function testVariadic() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ '1', '2' ] ]), + fn(#[FromQuery('foo')] int ...$foo) => $foo, + ); + $this->assertSame([ 1, 2 ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ '1', '0' ] ]), + fn(#[FromQuery('foo')] bool ...$foo) => $foo, + ); + $this->assertSame([ true, false ], $args); + } + + public function testVariadicEnum() + { + // TODO + } + + public function testVariadicUid() + { + // TODO + } + + public function testVariadicDateTime() + { + // TODO + } + + public function testExtraParametersAreKept() + { + $resolver = new ArgumentResolver(new ArgumentMetadataFactory(), [ new test_AllBagParametersValueResolver() ]); + + $args = $resolver->getArguments( + new Request(attributes: [ 'country' => 'XX', 'area' => '42' ]), + fn ($area) => $area + ); + $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + + $args = $resolver->getArguments( + new Request(attributes: [ 'country' => 'XX', 'area' => '42' ]), + fn (#[FromRoute] $area) => $area + ); + $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + + $args = $resolver->getArguments( + new Request(query: [ 'country' => 'XX', 'area' => '42' ]), + fn (#[FromQuery] $area) => $area + ); + $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + + $args = $resolver->getArguments( + new Request(request: [ 'country' => 'XX', 'area' => '42' ]), + fn (#[FromBody] $area) => $area + ); + $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + } + + protected function setUp(): void + { + $this->argumentResolver = new ArgumentResolver( + new ArgumentMetadataFactory(), + [ + new ArgumentResolver\BackedEnumValueResolver(), + new ArgumentResolver\DateTimeValueResolver(), + new ArgumentResolver\UidValueResolver(), + ...ArgumentResolver::getDefaultArgumentValueResolvers() + ] + ); + } + +} + +class test_AllBagParametersValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $valueBag = $this->resolveValueBag($request, $argument); + return [ $valueBag->all() ]; + } +} From 65cc9abf46fb7da7eff497345022f59ae263f5db Mon Sep 17 00:00:00 2001 From: klkvsk Date: Wed, 30 Oct 2024 12:31:21 +0300 Subject: [PATCH 2/6] add ValueBagResolverTrait support to EntityValueResolver --- .../ArgumentResolver/EntityValueResolver.php | 46 +++++++++++-------- .../EntityValueResolverTest.php | 41 ++++++++++++++++- .../Controller/ValueBagResolverTrait.php | 2 +- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index 7ddf3e72186d6..c6a48ed1171d6 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -18,8 +18,10 @@ use Doctrine\Persistence\ObjectManager; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -31,6 +33,8 @@ */ final class EntityValueResolver implements ValueResolverInterface { + use ValueBagResolverTrait; + public function __construct( private ManagerRegistry $registry, private ?ExpressionLanguage $expressionLanguage = null, @@ -40,7 +44,9 @@ public function __construct( public function resolve(Request $request, ArgumentMetadata $argument): array { - if (\is_object($request->attributes->get($argument->getName()))) { + $valueBag = $this->resolveValueBag($request, $argument); + + if (\is_object($valueBag->get($argument->getName()))) { return []; } @@ -56,13 +62,13 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $message = ''; if (null !== $options->expr) { - if (null === $object = $this->findViaExpression($manager, $request, $options)) { + if (null === $object = $this->findViaExpression($manager, $request, $options, $valueBag)) { $message = \sprintf(' The expression "%s" returned null.', $options->expr); } // find by identifier? - } elseif (false === $object = $this->find($manager, $request, $options, $argument)) { + } elseif (false === $object = $this->find($manager, $request, $options, $argument, $valueBag)) { // find by criteria - if (!$criteria = $this->getCriteria($request, $options, $manager, $argument)) { + if (!$criteria = $this->getCriteria($request, $options, $manager, $argument, $valueBag)) { return []; } try { @@ -94,13 +100,13 @@ private function getManager(?string $name, string $class): ?ObjectManager return $manager->getMetadataFactory()->isTransient($class) ? null : $manager; } - private function find(ObjectManager $manager, Request $request, MapEntity $options, ArgumentMetadata $argument): false|object|null + private function find(ObjectManager $manager, Request $request, MapEntity $options, ArgumentMetadata $argument, ParameterBag $valueBag): false|object|null { if ($options->mapping || $options->exclude) { return false; } - $id = $this->getIdentifier($request, $options, $argument); + $id = $this->getIdentifier($request, $options, $argument, $valueBag); if (false === $id || null === $id) { return $id; } @@ -122,7 +128,7 @@ private function find(ObjectManager $manager, Request $request, MapEntity $optio } } - private function getIdentifier(Request $request, MapEntity $options, ArgumentMetadata $argument): mixed + private function getIdentifier(Request $request, MapEntity $options, ArgumentMetadata $argument, ParameterBag $valueBag): mixed { if (\is_array($options->id)) { $id = []; @@ -132,24 +138,24 @@ private function getIdentifier(Request $request, MapEntity $options, ArgumentMet $field = \sprintf($field, $argument->getName()); } - $id[$field] = $request->attributes->get($field); + $id[$field] = $valueBag->get($field); } return $id; } if ($options->id) { - return $request->attributes->get($options->id) ?? ($options->stripNull ? false : null); + return $valueBag->get($options->id) ?? ($options->stripNull ? false : null); } $name = $argument->getName(); - if ($request->attributes->has($name)) { - if (\is_array($id = $request->attributes->get($name))) { + if ($valueBag->has($name)) { + if (\is_array($id = $valueBag->get($name))) { return false; } - foreach ($request->attributes->get('_route_mapping') ?? [] as $parameter => $attribute) { + foreach ($valueBag->get('_route_mapping') ?? [] as $parameter => $attribute) { if ($name === $attribute) { $options->mapping = [$name => $parameter]; @@ -160,16 +166,16 @@ private function getIdentifier(Request $request, MapEntity $options, ArgumentMet return $id ?? ($options->stripNull ? false : null); } - if ($request->attributes->has('id')) { - return $request->attributes->get('id') ?? ($options->stripNull ? false : null); + if ($valueBag->has('id')) { + return $valueBag->get('id') ?? ($options->stripNull ? false : null); } return false; } - private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager, ArgumentMetadata $argument): array + private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager, ArgumentMetadata $argument, ParameterBag $valueBag): array { - if (!($mapping = $options->mapping) && \is_array($criteria = $request->attributes->get($argument->getName()))) { + if (!($mapping = $options->mapping) && \is_array($criteria = $valueBag->get($argument->getName()))) { foreach ($options->exclude as $exclude) { unset($criteria[$exclude]); } @@ -181,7 +187,7 @@ private function getCriteria(Request $request, MapEntity $options, ObjectManager return $criteria; } elseif (null === $mapping) { trigger_deprecation('symfony/doctrine-bridge', '7.1', 'Relying on auto-mapping for Doctrine entities is deprecated for argument $%s of "%s": declare the identifier using either the #[MapEntity] attribute or mapped route parameters.', $argument->getName(), method_exists($argument, 'getControllerName') ? $argument->getControllerName() : 'n/a'); - $mapping = $request->attributes->keys(); + $mapping = $valueBag->keys(); } if ($mapping && array_is_list($mapping)) { @@ -204,7 +210,7 @@ private function getCriteria(Request $request, MapEntity $options, ObjectManager continue; } - $criteria[$field] = $request->attributes->get($attribute); + $criteria[$field] = $valueBag->get($attribute); } if ($options->stripNull) { @@ -214,14 +220,14 @@ private function getCriteria(Request $request, MapEntity $options, ObjectManager return $criteria; } - private function findViaExpression(ObjectManager $manager, Request $request, MapEntity $options): object|iterable|null + private function findViaExpression(ObjectManager $manager, Request $request, MapEntity $options, ParameterBag $valueBag): object|iterable|null { if (!$this->expressionLanguage) { throw new \LogicException(\sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__)); } $repository = $manager->getRepository($options->class); - $variables = array_merge($request->attributes->all(), [ + $variables = array_merge($valueBag->all(), [ 'repository' => $repository, 'request' => $request, ]); diff --git a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php index 898631bac842c..2ca3a7cec2338 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php @@ -23,6 +23,7 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\ExpressionLanguage\SyntaxError; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\FromQuery; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -297,6 +298,39 @@ public function testResolveWithRouteMapping() $this->assertSame([$article], $resolver->resolve($request, $argument2)); } + + public function testResolveFromValueBag() + { + $manager = $this->createMock(ObjectManager::class); + $registry = $this->createRegistry($manager); + $resolver = new EntityValueResolver($registry); + + $request = new Request(); + $request->query->set('conference_place', 'vienna'); + $request->query->set('conference_year', '2024'); + + $argument1 = $this->createArgument('Conference', new MapEntity('Conference', mapping: ['conference_place' => 'place', 'conference_year' => 'year']), 'conference', false, [ new FromQuery('*') ]); + + $manager->expects($this->never()) + ->method('getClassMetadata'); + + $conference = new \stdClass(); + + $repository = $this->createMock(ObjectRepository::class); + $repository->expects($this->any()) + ->method('findOneBy') + ->willReturnCallback(static fn ($v) => match ($v) { + ['place' => 'vienna', 'year' => '2024' ] => $conference, + default => dd($v) + }); + + $manager->expects($this->any()) + ->method('getRepository') + ->willReturn($repository); + + $this->assertSame([$conference], $resolver->resolve($request, $argument1)); + } + public function testExceptionWithExpressionIfNoLanguageAvailable() { $manager = $this->createMock(ObjectManager::class); @@ -474,9 +508,12 @@ public function testAlreadyResolved() $this->assertSame([], $resolver->resolve($request, $argument)); } - private function createArgument(?string $class = null, ?MapEntity $entity = null, string $name = 'arg', bool $isNullable = false): ArgumentMetadata + private function createArgument(?string $class = null, ?MapEntity $entity = null, string $name = 'arg', bool $isNullable = false, array $attributes = []): ArgumentMetadata { - return new ArgumentMetadata($name, $class ?? \stdClass::class, false, false, null, $isNullable, $entity ? [$entity] : []); + if ($entity) { + $attributes[] = $entity; + } + return new ArgumentMetadata($name, $class ?? \stdClass::class, false, false, null, $isNullable, $attributes); } private function createRegistry(?ObjectManager $manager = null): ManagerRegistry&MockObject diff --git a/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php b/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php index 2e2c3cb792303..91753ccfc894e 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php +++ b/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php @@ -63,7 +63,7 @@ private function resolveValueBag(Request $request, ArgumentMetadata $argument): if ($argument->getType() === 'bool') { $options['flags'] |= FILTER_NULL_ON_FAILURE; } - if ($argument->getType() === 'array' || $argument->isVariadic()) { + if ($argument->getType() === 'array' || $argument->isVariadic() || $requestParameterName === '*') { $options['flags'] |= FILTER_REQUIRE_ARRAY | FILTER_FORCE_ARRAY; } From f2825e783ef41713989d7f4eb1562a1029e81d01 Mon Sep 17 00:00:00 2001 From: klkvsk Date: Wed, 30 Oct 2024 12:43:20 +0300 Subject: [PATCH 3/6] add tests for MapEntity with expression when using ValueBagResolver --- .../EntityValueResolverTest.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php index 2ca3a7cec2338..b4aa42ab56153 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php @@ -419,6 +419,58 @@ public function testExpressionMapsToArgument() $this->assertSame([$object], $resolver->resolve($request, $argument)); } + public static function provideExpressionMapsToArgumentFromQueryExpressions(): array + { + return [ + 'with args in global scope' => [ 'repository.findByPlaceAndYear(place, year)' ], + 'with args in argument value scope' => [ 'repository.findByPlaceAndYear(conference.place, conference.year)' ] + ]; + } + + /** @dataProvider provideExpressionMapsToArgumentFromQueryExpressions */ + public function testExpressionMapsToArgumentFromQuery(string $expr) + { + $manager = $this->createMock(ObjectManager::class); + $registry = $this->createRegistry($manager); + $language = $this->createMock(ExpressionLanguage::class); + $resolver = new EntityValueResolver($registry, $language); + + $request = new Request(); + $request->query->set('place', 'vienna'); + $request->query->set('year', '2024'); + + $argument = $this->createArgument( + 'stdClass', + new MapEntity(expr: $expr), + 'conference', + false, + [ new FromQuery('*') ] + ); + + $repository = $this->createMock(ObjectRepository::class); + // find should not be attempted on this repository as a fallback + $repository->expects($this->never()) + ->method('find'); + + $manager->expects($this->once()) + ->method('getRepository') + ->with(\stdClass::class) + ->willReturn($repository); + + $language->expects($this->once()) + ->method('evaluate') + ->with($expr, [ + 'repository' => $repository, + 'request' => $request, + 'place' => 'vienna', + 'year' => '2024', + 'conference' => [ 'place' => 'vienna', 'year' => '2024' ] + ]) + ->willReturn($object = new \stdClass()); + + $this->assertSame([$object], $resolver->resolve($request, $argument)); + } + public function testExpressionMapsToIterableArgument() { $manager = $this->createMock(ObjectManager::class); From 3ea54031fea4272dcb49965e8c17240850794b89 Mon Sep 17 00:00:00 2001 From: klkvsk Date: Mon, 11 Nov 2024 19:23:22 +0300 Subject: [PATCH 4/6] refactor request parameter resolvers using RequestParameterValueResolverTraitTest --- .../ArgumentResolver/EntityValueResolver.php | 20 +- src/Symfony/Bridge/Doctrine/composer.json | 4 +- .../HttpKernel/Attribute/FromBody.php | 12 +- .../HttpKernel/Attribute/FromFile.php | 31 +++ .../HttpKernel/Attribute/FromHeader.php | 14 +- .../HttpKernel/Attribute/FromQuery.php | 12 +- ...gResolver.php => FromRequestParameter.php} | 14 +- .../HttpKernel/Attribute/FromRoute.php | 14 +- .../Attribute/MapQueryParameter.php | 2 + .../Controller/ArgumentResolver.php | 2 - .../BackedEnumValueResolver.php | 26 +-- .../DateTimeValueResolver.php | 17 +- .../QueryParameterValueResolver.php | 2 + .../RequestAttributeValueResolver.php | 14 +- .../ArgumentResolver/UidValueResolver.php | 29 ++- .../VariadicValueResolver.php | 12 +- .../RequestParameterValueResolverTrait.php | 189 ++++++++++++++++++ .../Controller/ValueBagResolverTrait.php | 101 ---------- .../BackedEnumValueResolverTest.php | 18 +- .../ArgumentResolver/UidValueResolverTest.php | 6 +- .../Tests/Controller/ArgumentResolverTest.php | 3 +- ...equestParameterValueResolverTraitTest.php} | 152 +++++++++++--- 22 files changed, 475 insertions(+), 219 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Attribute/FromFile.php rename src/Symfony/Component/HttpKernel/Attribute/{ValueBagResolver.php => FromRequestParameter.php} (62%) create mode 100644 src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php delete mode 100644 src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php rename src/Symfony/Component/HttpKernel/Tests/Controller/{ValueBagResolverTraitTest.php => RequestParameterValueResolverTraitTest.php} (74%) diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index c6a48ed1171d6..83a3a0d887162 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -21,7 +21,7 @@ use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -33,7 +33,7 @@ */ final class EntityValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; + use RequestParameterValueResolverTrait; public function __construct( private ManagerRegistry $registry, @@ -42,10 +42,14 @@ public function __construct( ) { } - public function resolve(Request $request, ArgumentMetadata $argument): array + protected function supports(ArgumentMetadata $argument): bool { - $valueBag = $this->resolveValueBag($request, $argument); + return $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF) + || ($argument->getType() && $this->getManager($this->defaults->objectManager, $argument->getType())); + } + protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array + { if (\is_object($valueBag->get($argument->getName()))) { return []; } @@ -68,7 +72,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array // find by identifier? } elseif (false === $object = $this->find($manager, $request, $options, $argument, $valueBag)) { // find by criteria - if (!$criteria = $this->getCriteria($request, $options, $manager, $argument, $valueBag)) { + if (!$criteria = $this->getCriteria($options, $manager, $argument, $valueBag)) { return []; } try { @@ -106,7 +110,7 @@ private function find(ObjectManager $manager, Request $request, MapEntity $optio return false; } - $id = $this->getIdentifier($request, $options, $argument, $valueBag); + $id = $this->getIdentifier($options, $argument, $valueBag); if (false === $id || null === $id) { return $id; } @@ -128,7 +132,7 @@ private function find(ObjectManager $manager, Request $request, MapEntity $optio } } - private function getIdentifier(Request $request, MapEntity $options, ArgumentMetadata $argument, ParameterBag $valueBag): mixed + private function getIdentifier(MapEntity $options, ArgumentMetadata $argument, ParameterBag $valueBag): mixed { if (\is_array($options->id)) { $id = []; @@ -173,7 +177,7 @@ private function getIdentifier(Request $request, MapEntity $options, ArgumentMet return false; } - private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager, ArgumentMetadata $argument, ParameterBag $valueBag): array + private function getCriteria(MapEntity $options, ObjectManager $manager, ArgumentMetadata $argument, ParameterBag $valueBag): array { if (!($mapping = $options->mapping) && \is_array($criteria = $valueBag->get($argument->getName()))) { foreach ($options->exclude as $exclude) { diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 1ca2abc4b13c4..b893791dc0408 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -31,7 +31,7 @@ "symfony/doctrine-messenger": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", "symfony/form": "^6.4.6|^7.0.6", - "symfony/http-kernel": "^6.4|^7.0", + "symfony/http-kernel": "^7.2", "symfony/lock": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", @@ -58,7 +58,7 @@ "symfony/dependency-injection": "<6.4", "symfony/form": "<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.4", - "symfony/http-kernel": "<6.4", + "symfony/http-kernel": "<7.2", "symfony/lock": "<6.4", "symfony/messenger": "<6.4", "symfony/property-info": "<6.4", diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromBody.php b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php index 8fd46f02f307b..4ad917bdae18a 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromBody.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php @@ -13,21 +13,25 @@ /** * Controller argument tag forcing ValueResolvers to use the value from request's body. + * + * @author Mike Kulakovsky */ -#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class FromBody extends ValueBagResolver +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromBody extends FromRequestParameter { /** * @param string|null $name The name of body parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). */ public function __construct( string|null $name = null, int|array|null $filter = null, - int|array|null $options = null, + int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, ) { - parent::__construct('request', $name, $filter, $options); + parent::__construct('request', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromFile.php b/src/Symfony/Component/HttpKernel/Attribute/FromFile.php new file mode 100644 index 0000000000000..c3b17f9df7dc4 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/FromFile.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Controller argument tag forcing ValueResolvers to use the value from request's files. + * + * @author Mike Kulakovsky + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromFile extends FromRequestParameter +{ + /** + * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + */ + public function __construct( + string|null $name = null, + ) + { + parent::__construct('files', $name); + } +} diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php index 9e25af3de7f8c..b7727bb2db562 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php @@ -13,21 +13,25 @@ /** * Controller argument tag forcing ValueResolvers to use the value from request's header. + * + * @author Mike Kulakovsky */ -#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class FromHeader extends ValueBagResolver +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromHeader extends FromRequestParameter { /** * @param string|null $name The name of header. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). */ public function __construct( - string|null $name = null, + string|null $name = null, int|array|null $filter = null, - int|array|null $options = null, + int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, ) { - parent::__construct('headers', $name, $filter, $options); + parent::__construct('headers', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php index 70845fc508a34..e240f0f8a6ab1 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php @@ -13,21 +13,25 @@ /** * Controller argument tag forcing ValueResolvers to use the value from request's query. + * + * @author Mike Kulakovsky */ -#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class FromQuery extends ValueBagResolver +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromQuery extends FromRequestParameter { /** * @param string|null $name The name of query parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). */ public function __construct( string|null $name = null, int|array|null $filter = null, - int|array|null $options = null, + int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, ) { - parent::__construct('query', $name, $filter, $options); + parent::__construct('query', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php b/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php similarity index 62% rename from src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php rename to src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php index e9f05df8c9c1e..8aa929523f669 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/ValueBagResolver.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php @@ -12,22 +12,26 @@ namespace Symfony\Component\HttpKernel\Attribute; /** - * Defines which value bag from request be used for a given parameter. + * Controller argument tag forcing ValueResolvers to use the value from request's specified parameter bag. + * + * @author Mike Kulakovsky */ -#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class ValueBagResolver +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromRequestParameter { /** - * @param "attributes"|"request"|"query"|"body"|"headers" $bag The bag to consume the value from + * @param "attributes"|"request"|"query"|"headers"|"files" $bag The bag to consume the value from * @param string|null $name The name of the parameter in bag. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). */ public function __construct( public string $bag, public string|null $name = null, public int|array|null $filter = null, - public int|array|null $options = null, + public int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, + public bool $throwOnFilterFailure = true, ) { } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php index df68c4a6197a9..585e1a7a642af 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php @@ -13,21 +13,25 @@ /** * Controller argument tag forcing ValueResolvers to use the value from request's route attributes. + * + * @author Mike Kulakovsky */ -#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class FromRoute extends ValueBagResolver +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class FromRoute extends FromRequestParameter { /** * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). */ public function __construct( - string|null $name = null, + string|null $name = null, int|array|null $filter = null, - int|array|null $options = null, + int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, ) { - parent::__construct('attributes', $name, $filter, $options); + parent::__construct('attributes', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php b/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php index 5b147b8143700..68098694494d5 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php @@ -20,6 +20,8 @@ * * @author Ruud Kamphuis * @author Ionut Enache + * + * @deprecated Use #[FromQuery] instead. */ #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapQueryParameter extends ValueResolver diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index b09a92f02dab3..617cd63ed3e44 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -18,7 +18,6 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; @@ -135,7 +134,6 @@ public static function getDefaultArgumentValueResolvers(): iterable new RequestValueResolver(), new SessionValueResolver(), new DefaultValueResolver(), - new VariadicValueResolver(), ]; } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php index 54ed8b2728434..5ac3542c3217d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -11,35 +11,31 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * Attempt to resolve backed enum cases from request attributes, for a route path parameter, - * leading to a 404 Not Found if the attribute value isn't a valid backing value for the enum type. + * Attempt to resolve backed enum cases from request parameters, + * leading to a 404 Not Found if the parameter value isn't a valid backing value for the enum type. * * @author Maxime Steinhausser + * @author Mike Kulakovsky */ final class BackedEnumValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; + use RequestParameterValueResolverTrait; - public function resolve(Request $request, ArgumentMetadata $argument): iterable + protected function supports(ArgumentMetadata $argument): bool { - if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { - return []; - } - - if ($argument->isVariadic()) { - // only target route path parameters, which cannot be variadic. - return []; - } - - $valueBag = $this->resolveValueBag($request, $argument); + return is_subclass_of($argument->getType(), \BackedEnum::class); + } + protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array + { // do not support if no value can be resolved at all // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php index 66d2716d9f45c..d5fe0f8c49ebd 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -12,33 +12,38 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Clock\ClockInterface; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * Convert DateTime instances from request attribute variable. + * Convert DateTime instances from request parameter value. * * @author Benjamin Eberlei * @author Tim Goudriaan + * @author Mike Kulakovsky */ final class DateTimeValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; + use RequestParameterValueResolverTrait; public function __construct( private readonly ?ClockInterface $clock = null, ) { } - public function resolve(Request $request, ArgumentMetadata $argument): array + protected function supports(ArgumentMetadata $argument): bool { - $valueBag = $this->resolveValueBag($request, $argument); + return is_a($argument->getType(), \DateTimeInterface::class, true); + } - if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$valueBag->has($argument->getName())) { + protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array + { + if (!$valueBag->has($argument->getName())) { return []; } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php index 8ceaba6da47dc..cfcc32c4d6ff7 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -24,6 +24,8 @@ * @author Nicolas Grekas * @author Mateusz Anders * @author Ionut Enache + * + * @deprecated Covered by RequestAttributeValueResolver and BackedEnumValueResolver with use of #[FromQuery]. */ final class QueryParameterValueResolver implements ValueResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 7f78f9f47d37d..8cf0a7f528e6c 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -11,24 +11,24 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** - * Yields a non-variadic argument's value from the request attributes. + * Provides a value for controller argument of types: string, int, float, bool, array. * * @author Iltar van der Berg + * @author Mike Kulakovsky */ final class RequestAttributeValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; + use RequestParameterValueResolverTrait; - public function resolve(Request $request, ArgumentMetadata $argument): array + protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array { - $valueBag = $this->resolveValueBag($request, $argument); - - return !$argument->isVariadic() && $valueBag->has($argument->getName()) ? [ $valueBag->get($argument->getName()) ] : []; + return $valueBag->has($argument->getName()) ? [ $valueBag->get($argument->getName()) ] : []; } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php index 583b3657fba42..3bca1b823d414 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php @@ -11,29 +11,40 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Uid\AbstractUid; +/** + * Convert to Uid instance from request parameter value. + * + * @author Thomas Calvet + * @author Nicolas Grekas + * @author Mike Kulakovsky + */ final class UidValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; + use RequestParameterValueResolverTrait; - public function resolve(Request $request, ArgumentMetadata $argument): array + protected function supports(ArgumentMetadata $argument): bool { - $valueBag = $this->resolveValueBag($request, $argument); + $uidClass = $argument->getType(); + return $uidClass && is_subclass_of($uidClass, AbstractUid::class, true); + } - if ($argument->isVariadic() - || !\is_string($value = $valueBag->get($argument->getName())) - || null === ($uidClass = $argument->getType()) - || !is_subclass_of($uidClass, AbstractUid::class, true) - ) { + protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array + { + if (!$valueBag->has($argument->getName()) || !\is_string($value = $valueBag->get($argument->getName()))) { return []; } + /** @var class-string $uidClass */ + $uidClass = $argument->getType(); + try { return [$uidClass::fromString($value)]; } catch (\InvalidArgumentException $e) { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php index aecde19ada9e1..58b108964570a 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -20,20 +19,19 @@ * Yields a variadic argument's values from the request attributes. * * @author Iltar van der Berg + * + * @deprecated Variadic parameters are now covered by RequestParameterValueResolverTrait + * @see \Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait */ final class VariadicValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; - public function resolve(Request $request, ArgumentMetadata $argument): array { - $valueBag = $this->resolveValueBag($request, $argument); - - if (!$argument->isVariadic() || !$valueBag->has($argument->getName())) { + if (!$argument->isVariadic() || !$request->attributes->has($argument->getName())) { return []; } - $values = $valueBag->get($argument->getName()); + $values = $request->attributes->get($argument->getName()); if (!\is_array($values)) { throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); diff --git a/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php b/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php new file mode 100644 index 0000000000000..c348f5dfb76a0 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\FromRoute; +use Symfony\Component\HttpKernel\Attribute\FromRequestParameter; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +/** + * A common trait for ValueResolvers that resolve value from request parameters. + * Uses FromRequestParameter attribute to determine which request parameter to use. + * Supports filtering and validation via `filter_var`. + * + * Target ValueResolver must implement `resolveValue` method and use `$valueBag->get($argument->getName())` to get the value. + * Target ValueResolver should NOT have a separate logic for variadic arguments. This trait handles it. + * + * @see \Symfony\Component\HttpKernel\Attribute\FromRoute + * @see \Symfony\Component\HttpKernel\Attribute\FromQuery + * @see \Symfony\Component\HttpKernel\Attribute\FromBody + * @see \Symfony\Component\HttpKernel\Attribute\FromHeader + * @see \Symfony\Component\HttpKernel\Attribute\FromFile + * + * @author Mike Kulakovsky + */ +trait RequestParameterValueResolverTrait +{ + public function resolve(Request $request, ArgumentMetadata $argument): array + { + return $this->resolveFromRequestParameters($request, $argument); + } + + protected function supports(ArgumentMetadata $argument): bool + { + return true; + } + + final protected function resolveFromRequestParameters(Request $request, ArgumentMetadata $argument): array + { + if (!$this->supports($argument)) { + return []; + } + + /** @var FromRequestParameter[] $attributes */ + $attributes = $argument->getAttributesOfType(FromRequestParameter::class, $argument::IS_INSTANCEOF); + + $attribute = match (count($attributes)) { + 0 => new FromRoute(), // Fall back to route attributes by default to keep BC. + 1 => $attributes[0], + default => throw new \LogicException('Multiple FromRequestParameter attributes are not allowed on a single argument.'), + }; + + [ $originalBag, $isOriginalBagCopied ] = match ($attribute->bag) { + 'attributes' => [ new ParameterBag($request->attributes->all()), true ], + 'request' => [ new ParameterBag($request->getPayload()->all()), true ], + 'query' => [ new ParameterBag($request->query->all()), true ], + 'headers' => [ $request->headers, false ], + 'files' => [ $request->files, false ], + default => throw new \InvalidArgumentException(sprintf('Unknown bag "%s" for value consumer.', $attribute->bag)), + }; + + $requestParameterName = $attribute->name ?? $argument->getName(); + + if ($requestParameterName === '*') { + // Complete bag contents, useful to get full query or payload as an array argument + $values = [ $originalBag->all() ]; + } else if ($originalBag->has($requestParameterName)) { + $value = $originalBag->get($requestParameterName); + // Expand variadic only if a list of values provided in request data. Otherwise, treated as a single element. + if ($argument->isVariadic() && is_array($value) && array_is_list($value)) { + $values = [ ...$value ]; + } else { + $values = [ $value ]; + } + } else { + // No values bound to this argument, but we still need to call resolve + $values = []; + } + + $filter = $attribute->filter ?? match ($argument->getType()) { + 'int' => \FILTER_VALIDATE_INT, + 'float' => \FILTER_VALIDATE_FLOAT, + 'bool' => \FILTER_VALIDATE_BOOL, + 'string', 'array' => \FILTER_DEFAULT, + default => null, + }; + + if ($filter !== null && !empty($values)) { + $options = [ 'flags' => 0 ]; + if (is_int($attribute->options)) { + $options['flags'] = $attribute->options; + } else if (is_array($attribute->options)) { + $options['options'] = $attribute->options['options'] ?? $attribute->options; + $options['flags'] = $attribute->options['flags'] ?? 0; + } + if ($filter === \FILTER_VALIDATE_BOOL) { + $options['flags'] |= FILTER_NULL_ON_FAILURE; + } + if ($argument->getType() === 'array') { + $options['flags'] |= FILTER_FORCE_ARRAY | FILTER_REQUIRE_ARRAY; + } + + $isNullOnEmptyString = $options['flags'] & FILTER_FLAG_EMPTY_STRING_NULL; + $isNullOnFailure = $options['flags'] & FILTER_NULL_ON_FAILURE; + + $failedValues = []; + + foreach ($values as $i => $value) { + if (is_object($value)) { + continue; + } + + if ($value === null) { + unset($values[$i]); + continue; + } + + // Manually apply FILTER_FLAG_EMPTY_STRING_NULL to ignore empty string value for numeric arguments + if ($value === '' && $isNullOnEmptyString && ($filter === \FILTER_VALIDATE_INT || $filter === \FILTER_VALIDATE_FLOAT)) { + unset($values[$i]); + continue; + } + + if (is_array($filter)) { + if (is_array($value)) { + $filtered = filter_var_array($value, $filter, $options['add_empty'] ?? true); + } else { + $filtered = $isNullOnFailure ? null : false; + } + } else { + $filtered = filter_var($value, $filter, $options); + } + + if (($filtered === false && !$isNullOnFailure) || ($filtered === null && $isNullOnFailure)) { + $failedValues[] = $value; + unset($values[$i]); + } else { + $values[$i] = $filtered; + } + } + + if ($failedValues && $attribute->throwOnFilterFailure) { + throw new BadRequestHttpException(sprintf('Parameter "%s" is invalid.', $requestParameterName)); + } + } + + $baseParametersBag = $isOriginalBagCopied ? $originalBag : new ParameterBag(); + $parametersBags = []; + if ($values) { + // For variadic arguments, multiple bags are created, each with a single value + foreach ($values as $value) { + $bag = clone $baseParametersBag; + $bag->set($argument->getName(), $value); + $parametersBags[] = $bag; + } + } else { + $bag = clone $baseParametersBag; + // Ensure we do not pass unfiltered value + $bag->remove($argument->getName()); + $parametersBags = [ $bag ]; + } + + $resolvedValues = []; + foreach ($parametersBags as $bag) { + $resolved = $this->resolveValue($request, $argument, $bag); + if (count($resolved) === 1) { + $resolvedValues[] = $resolved[0]; + } else if (count($resolved) > 1) { + throw new \UnexpectedValueException('Method resolveValue() should return an array with a single item containing resolved value or an empty array if nothing was resolved.'); + } + } + + return $resolvedValues; + } + + abstract protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array; + +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php b/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php deleted file mode 100644 index 91753ccfc894e..0000000000000 --- a/src/Symfony/Component/HttpKernel/Controller/ValueBagResolverTrait.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\Controller; - -use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Attribute\ValueBagResolver; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; - -trait ValueBagResolverTrait -{ - private function resolveValueBag(Request $request, ArgumentMetadata $argument): ParameterBag - { - /** @var ValueBagResolver[] $valueResolverBagAttributes */ - $valueResolverBagAttributes = $argument->getAttributesOfType(ValueBagResolver::class, $argument::IS_INSTANCEOF); - if (!$valueResolverBagAttributes) { - // Fall back to attributes by default to keep BC. - return $request->attributes; - } - - foreach ($valueResolverBagAttributes as $attribute) { - [ $requestBag, $isReturned ] = match ($attribute->bag) { - 'attributes' => [ new ParameterBag($request->attributes->all()), true ], - 'request' => [ new ParameterBag($request->getPayload()->all()), true ], - 'query' => [ new ParameterBag($request->query->all()), true ], - 'headers' => [ $request->headers, false ], - 'files' => [ $request->files, false ], - default => throw new \InvalidArgumentException(sprintf('Unknown bag "%s" for value consumer.', $attribute->bag)), - }; - - $requestParameterName = $attribute->name ?? $argument->getName(); - if ($requestParameterName === '*') { - $value = $requestBag->all(); - } else if ($requestBag->has($requestParameterName)) { - $value = $requestBag->get($requestParameterName); - } else { - continue; - } - - $filter = $attribute->filter ?? match ($argument->getType()) { - 'int' => \FILTER_VALIDATE_INT, - 'float' => \FILTER_VALIDATE_FLOAT, - 'bool' => \FILTER_VALIDATE_BOOL, - default => \FILTER_DEFAULT, - }; - - $options = [ 'flags' => 0 ]; - if (is_int($attribute->options)) { - $options['flags'] = $attribute->options; - } else if (is_array($attribute->options)) { - $options['options'] = $attribute->options['options'] ?? $attribute->options; - $options['flags'] = $attribute->options['flags'] ?? 0; - } - if ($argument->getType() === 'bool') { - $options['flags'] |= FILTER_NULL_ON_FAILURE; - } - if ($argument->getType() === 'array' || $argument->isVariadic() || $requestParameterName === '*') { - $options['flags'] |= FILTER_REQUIRE_ARRAY | FILTER_FORCE_ARRAY; - } - - $isNullOnFailure = $options['flags'] & FILTER_NULL_ON_FAILURE; - - if ($value !== null) { - if (is_array($filter)) { - if (is_array($value)) { - $filtered = filter_var_array($value, $filter, $options['add_empty'] ?? true); - } else { - $filtered = $isNullOnFailure ? null : false; - } - } else { - $filtered = filter_var($value, $filter, $options); - } - - if (($filtered === false && !$isNullOnFailure) || ($filtered === null && $isNullOnFailure)) { - continue; - } - - $value = $filtered; - } - - // For attributes, query, request bags, return other values too. - // This way, some sophisticated ValueResolvers like EntityValueResolver can receive them. - $valueBag = $isReturned ? $requestBag : new ParameterBag(); - $valueBag->set($argument->getName(), $value); - - return $valueBag; - } - - return new ParameterBag(); - } - -} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php index 6b0f4027ffd8b..a9b8ca080f547 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php @@ -24,11 +24,11 @@ class BackedEnumValueResolverTest extends TestCase /** * @dataProvider provideTestSupportsData */ - public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport) + public function testSupports(Request $request, ArgumentMetadata $metadata, bool|int $expected) { $resolver = new BackedEnumValueResolver(); - $this->assertCount((int) $expectedSupport, $resolver->resolve($request, $metadata)); + $this->assertCount((int) $expected, $resolver->resolve($request, $metadata)); } public static function provideTestSupportsData(): iterable @@ -57,14 +57,24 @@ public static function provideTestSupportsData(): iterable false, ]; - yield 'unsupported variadic' => [ + yield 'supported variadic' => [ self::createRequest(['suit' => ['H', 'S']]), self::createArgumentMetadata( 'suit', Suit::class, variadic: true, ), - false, + 2, + ]; + + yield 'supported variadic as single' => [ + self::createRequest(['suit' => 'H']), + self::createArgumentMetadata( + 'suit', + Suit::class, + variadic: true, + ), + 1, ]; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UidValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UidValueResolverTest.php index 1da4d976a2083..1f0be47a19788 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UidValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UidValueResolverTest.php @@ -35,9 +35,9 @@ public function testSupports(bool $expected, Request $request, ArgumentMetadata public static function provideSupports() { return [ - 'Variadic argument' => [false, new Request([], [], ['foo' => (string) $uuidV4 = new UuidV4()]), new ArgumentMetadata('foo', UuidV4::class, true, false, null)], + 'Variadic argument as single' => [true, new Request([], [], ['foo' => (string) $uuidV4 = new UuidV4()]), new ArgumentMetadata('foo', UuidV4::class, true, false, null)], + 'Variadic argument' => [true, new Request([], [], ['foo' => [(string) $uuidV4 = new UuidV4()]]), new ArgumentMetadata('foo', UuidV4::class, true, false, null)], 'No attribute for argument' => [false, new Request([], [], []), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], - 'Attribute is not a string' => [false, new Request([], [], ['foo' => ['bar']]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], 'Argument has no type' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', null, false, false, null)], 'Argument type is not a class' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', 'string', false, false, null)], 'Argument type is not a subclass of AbstractUid' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UlidFactory::class, false, false, null)], @@ -76,7 +76,7 @@ public static function provideResolveOK() /** * @dataProvider provideResolveKO */ - public function testResolveKO(string $requestUid, string $argumentType) + public function testResolveKO(mixed $requestUid, string $argumentType) { $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('The uid for the "id" parameter is invalid.'); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php index ed06130b92ec9..a0acc20f31320 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -169,13 +169,12 @@ public function testGetVariadicArguments() public function testGetVariadicArgumentsWithoutArrayInRequest() { - $this->expectException(\InvalidArgumentException::class); $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'foo'); $controller = [new VariadicController(), 'action']; - self::getResolver()->getArguments($request, $controller); + $this->assertEquals(['foo', 'foo'], self::getResolver()->getArguments($request, $controller)); } public function testIfExceptionIsThrownWhenMissingAnArgument() diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php similarity index 74% rename from src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php rename to src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php index 5a92ad3a779a1..d689f47b86ef0 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ValueBagResolverTraitTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php @@ -3,37 +3,50 @@ namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\FromBody; +use Symfony\Component\HttpKernel\Attribute\FromFile; use Symfony\Component\HttpKernel\Attribute\FromHeader; use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Attribute\FromQuery; use Symfony\Component\HttpKernel\Attribute\FromRoute; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; -use Symfony\Component\HttpKernel\Controller\ValueBagResolverTrait; +use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\UuidV1; +use Symfony\Component\Validator\Constraints\DateTime; -class ValueBagResolverTraitTest extends TestCase +class RequestParameterValueResolverTraitTest extends TestCase { protected ArgumentResolverInterface $argumentResolver; - public static function intValues() + public static function intValues(): array { return [ '123' => [ 123, 123 ], '"123"' => [ 123, '123' ], - '"10e2"' => [ null, '10e2' ], '(empty string)' => [ null, '' ], ]; } - public static function boolValues() + public static function floatValues(): array + { + return [ + '1.23' => [ 1.23, 1.23 ], + '"1.23"' => [ 1.23, '1.23' ], + '(empty string)' => [ null, '' ], + ]; + } + + public static function boolValues(): array { return [ '"true"' => [ true, 'true' ], @@ -146,6 +159,28 @@ public function testStringFromHeaderRenamed() $this->assertEquals([ 'bar' ], $args); } + public function testFromFile() + { + $file = __DIR__.'/../Fixtures/Controller/ArgumentResolver/UploadedFile/file-small.txt'; + $request = new Request(files: [ + 'attach' => [ + 'name' => 'file-small.txt', + 'type' => 'text/plain', + 'tmp_name' => $file, + 'size' => filesize($file), + 'error' => UPLOAD_ERR_OK, + ] + ]); + + $args = $this->argumentResolver->getArguments( + $request, + fn(#[FromFile] UploadedFile $attach) => $attach, + ); + $this->assertCount(1, $args); + $this->assertInstanceOf(UploadedFile::class, $args[0]); + $this->assertSame('file-small.txt', $args[0]->getClientOriginalName()); + } + public function testInt() { $args = $this->argumentResolver->getArguments( @@ -159,30 +194,55 @@ public function testInt() fn(#[FromQuery] ?int $page) => $page, ); $this->assertEquals([ null ], $args); + } - $args = $this->argumentResolver->getArguments( + public function testValidationFailed() + { + $this->expectException(BadRequestHttpException::class); + $this->argumentResolver->getArguments( new Request(query: [ 'page' => 'not-an-integer' ]), + fn(#[FromQuery()] ?int $page) => $page, + ); + } + + public function testEmptyStringAsNull() + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'page' => '' ]), fn(#[FromQuery] ?int $page) => $page, ); $this->assertEquals([ null ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'weight' => '' ]), + fn(#[FromQuery] ?float $weight) => $weight, + ); + $this->assertEquals([ null ], $args); + + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'title' => '' ]), + fn(#[FromQuery] ?int $title) => $title, + ); + $this->assertEquals([ null ], $args); } - public function testMultipleGoodAfterFailed() + public function testEmptyStringAsNullDisabled() { - $args = $this->argumentResolver->getArguments( - new Request(query: [ 'page' => 'not-an-integer' ], request: [ 'page' => 2 ]), - fn(#[FromQuery] #[FromBody] ?int $page) => $page, + $this->expectException(BadRequestHttpException::class); + $this->argumentResolver->getArguments( + new Request(query: [ 'page' => '' ]), + fn(#[FromQuery(options: null)] ?int $page) => $page, ); - $this->assertEquals([ 2 ], $args); } - public function testMultipleFailedAfterGood() + public function testMultipleAttributesDisallowed() { - $args = $this->argumentResolver->getArguments( - new Request(query: [ 'page' => 'not-an-integer' ], request: [ 'page' => 2 ]), - fn(#[FromBody] #[FromQuery] ?int $page) => $page, + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Multiple'); + $this->argumentResolver->getArguments( + new Request(query: [ 'page' => 1 ], request: [ 'page' => 2 ]), + fn(#[FromQuery] #[FromBody] ?int $page) => $page, ); - $this->assertEquals([ 2 ], $args); } /** @dataProvider intValues */ @@ -195,11 +255,21 @@ public function testIntFromQuery($expected, $actual) $this->assertEquals([ $expected ], $args); } + /** @dataProvider floatValues */ + public function testFloatFromQuery($expected, $actual) + { + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'float' => $actual ]), + fn(#[FromQuery] ?float $float) => $float, + ); + $this->assertEquals([ $expected ], $args); + } + public function testFilterFlags() { $args = $this->argumentResolver->getArguments( new Request(query: [ 'int' => '0xff' ]), - fn(#[FromQuery] ?int $int) => $int, + fn(#[FromQuery(throwOnFilterFailure: false)] ?int $int) => $int, ); $this->assertEquals([ null ], $args); @@ -224,26 +294,27 @@ public function testFilterOptions() ); $this->assertEquals([ 42 ], $args); - $args = $this->argumentResolver->getArguments( + $this->expectException(BadRequestHttpException::class); + $this->argumentResolver->getArguments( new Request(query: [ 'int' => 42 ]), fn(#[FromQuery(options: [ 'max_range' => 10 ])] ?int $int) => $int, ); - $this->assertEquals([ null ], $args); } public function testFilterFlagAndOptions() { $args = $this->argumentResolver->getArguments( new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 10 ])] ?int $int) => $int, + fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 100 ])] ?int $int) => $int, ); + $this->assertEquals([ 42 ], $args); - $this->assertEquals([ null ], $args); - $args = $this->argumentResolver->getArguments( + $this->expectException(BadRequestHttpException::class); + $this->argumentResolver->getArguments( new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 100 ])] ?int $int) => $int, + fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 10 ])] ?int $int) => $int, ); - $this->assertEquals([ 42 ], $args); + } /** @dataProvider boolValues */ @@ -346,17 +417,37 @@ public function testVariadic() public function testVariadicEnum() { - // TODO + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ 'H', 'S' ] ]), + fn(#[FromQuery('foo')] Suit ...$foo) => $foo, + ); + $this->assertSame([ Suit::Hearts, Suit::Spades ], $args); } public function testVariadicUid() { - // TODO + $uids = [ + new UuidV1(), + new UuidV1(), + ]; + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ $uids[0]->toString(), $uids[1]->toString() ] ]), + fn(#[FromQuery('foo')] AbstractUid ...$foo) => $foo, + ); + $this->assertEquals($uids, $args); } public function testVariadicDateTime() { - // TODO + $dates = [ + new \DateTimeImmutable('2021-01-01 01:01:01'), + new \DateTimeImmutable('2021-02-02 02:02:02'), + ]; + $args = $this->argumentResolver->getArguments( + new Request(query: [ 'foo' => [ $dates[0]->getTimestamp(), $dates[1]->format(\DATE_ATOM) ] ]), + fn(#[FromQuery('foo')] \DateTimeInterface ...$foo) => $foo, + ); + $this->assertEquals($dates, $args); } public function testExtraParametersAreKept() @@ -404,10 +495,11 @@ protected function setUp(): void } class test_AllBagParametersValueResolver implements ValueResolverInterface { - use ValueBagResolverTrait; - public function resolve(Request $request, ArgumentMetadata $argument): iterable + use RequestParameterValueResolverTrait; + + public function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array { - $valueBag = $this->resolveValueBag($request, $argument); return [ $valueBag->all() ]; } + } From c30df5c446ba3b2de6d4d74ef02a7fc1d39db8c1 Mon Sep 17 00:00:00 2001 From: klkvsk Date: Mon, 11 Nov 2024 20:36:17 +0300 Subject: [PATCH 5/6] apply fabbot patch, fix psalm notices and ci tests --- .../ArgumentResolver/EntityValueResolver.php | 2 +- .../EntityValueResolverTest.php | 16 +- .../HttpKernel/Attribute/FromBody.php | 17 +- .../HttpKernel/Attribute/FromFile.php | 5 +- .../HttpKernel/Attribute/FromHeader.php | 17 +- .../HttpKernel/Attribute/FromQuery.php | 17 +- .../Attribute/FromRequestParameter.php | 21 +- .../HttpKernel/Attribute/FromRoute.php | 17 +- .../Attribute/MapQueryParameter.php | 8 +- .../QueryParameterValueResolver.php | 2 +- .../RequestAttributeValueResolver.php | 2 +- .../ArgumentResolver/UidValueResolver.php | 1 + .../RequestParameterValueResolverTrait.php | 84 +++-- ...RequestParameterValueResolverTraitTest.php | 303 +++++++++--------- 14 files changed, 255 insertions(+), 257 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index 83a3a0d887162..7b22f01b39352 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -20,8 +20,8 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; diff --git a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php index b4aa42ab56153..98bef568ed3c1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php @@ -298,7 +298,6 @@ public function testResolveWithRouteMapping() $this->assertSame([$article], $resolver->resolve($request, $argument2)); } - public function testResolveFromValueBag() { $manager = $this->createMock(ObjectManager::class); @@ -309,7 +308,7 @@ public function testResolveFromValueBag() $request->query->set('conference_place', 'vienna'); $request->query->set('conference_year', '2024'); - $argument1 = $this->createArgument('Conference', new MapEntity('Conference', mapping: ['conference_place' => 'place', 'conference_year' => 'year']), 'conference', false, [ new FromQuery('*') ]); + $argument1 = $this->createArgument('Conference', new MapEntity('Conference', mapping: ['conference_place' => 'place', 'conference_year' => 'year']), 'conference', false, [new FromQuery('*')]); $manager->expects($this->never()) ->method('getClassMetadata'); @@ -320,8 +319,8 @@ public function testResolveFromValueBag() $repository->expects($this->any()) ->method('findOneBy') ->willReturnCallback(static fn ($v) => match ($v) { - ['place' => 'vienna', 'year' => '2024' ] => $conference, - default => dd($v) + ['place' => 'vienna', 'year' => '2024'] => $conference, + default => dd($v), }); $manager->expects($this->any()) @@ -422,8 +421,8 @@ public function testExpressionMapsToArgument() public static function provideExpressionMapsToArgumentFromQueryExpressions(): array { return [ - 'with args in global scope' => [ 'repository.findByPlaceAndYear(place, year)' ], - 'with args in argument value scope' => [ 'repository.findByPlaceAndYear(conference.place, conference.year)' ] + 'with args in global scope' => ['repository.findByPlaceAndYear(place, year)'], + 'with args in argument value scope' => ['repository.findByPlaceAndYear(conference.place, conference.year)'], ]; } @@ -444,7 +443,7 @@ public function testExpressionMapsToArgumentFromQuery(string $expr) new MapEntity(expr: $expr), 'conference', false, - [ new FromQuery('*') ] + [new FromQuery('*')] ); $repository = $this->createMock(ObjectRepository::class); @@ -464,7 +463,7 @@ public function testExpressionMapsToArgumentFromQuery(string $expr) 'request' => $request, 'place' => 'vienna', 'year' => '2024', - 'conference' => [ 'place' => 'vienna', 'year' => '2024' ] + 'conference' => ['place' => 'vienna', 'year' => '2024'], ]) ->willReturn($object = new \stdClass()); @@ -565,6 +564,7 @@ private function createArgument(?string $class = null, ?MapEntity $entity = null if ($entity) { $attributes[] = $entity; } + return new ArgumentMetadata($name, $class ?? \stdClass::class, false, false, null, $isNullable, $attributes); } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromBody.php b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php index 4ad917bdae18a..67344604e8b0f 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromBody.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromBody.php @@ -20,18 +20,17 @@ class FromBody extends FromRequestParameter { /** - * @param string|null $name The name of body parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. - * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. - * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() - * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). + * @param string|null $name The name of body parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter the filter for `filter_var()` if int, or for `filter_var_array()` if array + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any) */ public function __construct( - string|null $name = null, + ?string $name = null, int|array|null $filter = null, - int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, - bool $throwOnFilterFailure = true, - ) - { + int|array|null $options = \FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, + ) { parent::__construct('request', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromFile.php b/src/Symfony/Component/HttpKernel/Attribute/FromFile.php index c3b17f9df7dc4..4033351b4a606 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromFile.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromFile.php @@ -23,9 +23,8 @@ class FromFile extends FromRequestParameter * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. */ public function __construct( - string|null $name = null, - ) - { + ?string $name = null, + ) { parent::__construct('files', $name); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php index b7727bb2db562..2619452f6be3f 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromHeader.php @@ -20,18 +20,17 @@ class FromHeader extends FromRequestParameter { /** - * @param string|null $name The name of header. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. - * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. - * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() - * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). + * @param string|null $name The name of header. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter the filter for `filter_var()` if int, or for `filter_var_array()` if array + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any) */ public function __construct( - string|null $name = null, + ?string $name = null, int|array|null $filter = null, - int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, - bool $throwOnFilterFailure = true, - ) - { + int|array|null $options = \FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, + ) { parent::__construct('headers', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php index e240f0f8a6ab1..3e1fa365254c3 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromQuery.php @@ -20,18 +20,17 @@ class FromQuery extends FromRequestParameter { /** - * @param string|null $name The name of query parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. - * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. - * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() - * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). + * @param string|null $name The name of query parameter. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter the filter for `filter_var()` if int, or for `filter_var_array()` if array + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any) */ public function __construct( - string|null $name = null, + ?string $name = null, int|array|null $filter = null, - int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, - bool $throwOnFilterFailure = true, - ) - { + int|array|null $options = \FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, + ) { parent::__construct('query', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php b/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php index 8aa929523f669..901d1adca1b4e 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromRequestParameter.php @@ -20,19 +20,18 @@ class FromRequestParameter { /** - * @param "attributes"|"request"|"query"|"headers"|"files" $bag The bag to consume the value from - * @param string|null $name The name of the parameter in bag. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. - * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. - * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() - * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). + * @param "attributes"|"request"|"query"|"headers"|"files" $bag The bag to consume the value from + * @param string|null $name The name of the parameter in bag. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter the filter for `filter_var()` if int, or for `filter_var_array()` if array + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any) */ public function __construct( - public string $bag, - public string|null $name = null, + public string $bag, + public ?string $name = null, public int|array|null $filter = null, - public int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, - public bool $throwOnFilterFailure = true, - ) - { + public int|array|null $options = \FILTER_FLAG_EMPTY_STRING_NULL, + public bool $throwOnFilterFailure = true, + ) { } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php index 585e1a7a642af..64eb195b6626f 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php +++ b/src/Symfony/Component/HttpKernel/Attribute/FromRoute.php @@ -20,18 +20,17 @@ class FromRoute extends FromRequestParameter { /** - * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. - * @param int|array|null $filter The filter for `filter_var()` if int, or for `filter_var_array()` if array. - * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() - * @param bool $throwOnFilterFailure Whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any). + * @param string|null $name The name of route attribute. Use "*" to collect all parameters. By default, the name of the argument in the controller will be used. + * @param int|array|null $filter the filter for `filter_var()` if int, or for `filter_var_array()` if array + * @param int|array|null $options The filter flag mask if int, or options array. If $filter is array, $options accepts only an array with "add_empty" key to be used as 3rd argument for filter_var_array() + * @param bool $throwOnFilterFailure whether to throw '400 Bad Request' on filtering failure or not, falling back to default (if any) */ public function __construct( - string|null $name = null, + ?string $name = null, int|array|null $filter = null, - int|array|null $options = FILTER_FLAG_EMPTY_STRING_NULL, - bool $throwOnFilterFailure = true, - ) - { + int|array|null $options = \FILTER_FLAG_EMPTY_STRING_NULL, + bool $throwOnFilterFailure = true, + ) { parent::__construct('attributes', $name, $filter, $options, $throwOnFilterFailure); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php b/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php index 68098694494d5..855aaeb28fe7f 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapQueryParameter.php @@ -21,7 +21,7 @@ * @author Ruud Kamphuis * @author Ionut Enache * - * @deprecated Use #[FromQuery] instead. + * @deprecated use #[FromQuery] instead */ #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapQueryParameter extends ValueResolver @@ -29,11 +29,11 @@ final class MapQueryParameter extends ValueResolver /** * @see https://php.net/filter.filters.validate for filter, flags and options * - * @param string|null $name The name of the query parameter; if null, the name of the argument in the controller will be used + * @param string|null $name The name of the query parameter; if null, the name of the argument in the controller will be used * @param (FILTER_VALIDATE_*)|(FILTER_SANITIZE_*)|null $filter The filter to pass to "filter_var()" * @param int-mask-of<(FILTER_FLAG_*)|FILTER_NULL_ON_FAILURE> $flags The flags to pass to "filter_var()" - * @param array $options The options to pass to "filter_var()" - * @param class-string|string $resolver The name of the resolver to use + * @param array $options The options to pass to "filter_var()" + * @param class-string|string $resolver The name of the resolver to use */ public function __construct( public ?string $name = null, diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php index cfcc32c4d6ff7..68f705f80c9c9 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -25,7 +25,7 @@ * @author Mateusz Anders * @author Ionut Enache * - * @deprecated Covered by RequestAttributeValueResolver and BackedEnumValueResolver with use of #[FromQuery]. + * @deprecated covered by RequestAttributeValueResolver and BackedEnumValueResolver with use of #[FromQuery] */ final class QueryParameterValueResolver implements ValueResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 8cf0a7f528e6c..4fc3c25123c81 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -29,6 +29,6 @@ final class RequestAttributeValueResolver implements ValueResolverInterface protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array { - return $valueBag->has($argument->getName()) ? [ $valueBag->get($argument->getName()) ] : []; + return $valueBag->has($argument->getName()) ? [$valueBag->get($argument->getName())] : []; } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php index 3bca1b823d414..eb6b537f779d1 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php @@ -33,6 +33,7 @@ final class UidValueResolver implements ValueResolverInterface protected function supports(ArgumentMetadata $argument): bool { $uidClass = $argument->getType(); + return $uidClass && is_subclass_of($uidClass, AbstractUid::class, true); } diff --git a/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php b/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php index c348f5dfb76a0..0f1c061ac62b5 100644 --- a/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php +++ b/src/Symfony/Component/HttpKernel/Controller/RequestParameterValueResolverTrait.php @@ -13,8 +13,8 @@ use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Attribute\FromRoute; use Symfony\Component\HttpKernel\Attribute\FromRequestParameter; +use Symfony\Component\HttpKernel\Attribute\FromRoute; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -26,7 +26,7 @@ * Target ValueResolver must implement `resolveValue` method and use `$valueBag->get($argument->getName())` to get the value. * Target ValueResolver should NOT have a separate logic for variadic arguments. This trait handles it. * - * @see \Symfony\Component\HttpKernel\Attribute\FromRoute + * @see FromRoute * @see \Symfony\Component\HttpKernel\Attribute\FromQuery * @see \Symfony\Component\HttpKernel\Attribute\FromBody * @see \Symfony\Component\HttpKernel\Attribute\FromHeader @@ -55,33 +55,32 @@ final protected function resolveFromRequestParameters(Request $request, Argument /** @var FromRequestParameter[] $attributes */ $attributes = $argument->getAttributesOfType(FromRequestParameter::class, $argument::IS_INSTANCEOF); - $attribute = match (count($attributes)) { - 0 => new FromRoute(), // Fall back to route attributes by default to keep BC. - 1 => $attributes[0], + $attribute = match (\count($attributes)) { + 0 => new FromRoute(), // Fall back to route attributes by default to keep BC. + 1 => $attributes[0], default => throw new \LogicException('Multiple FromRequestParameter attributes are not allowed on a single argument.'), }; - [ $originalBag, $isOriginalBagCopied ] = match ($attribute->bag) { - 'attributes' => [ new ParameterBag($request->attributes->all()), true ], - 'request' => [ new ParameterBag($request->getPayload()->all()), true ], - 'query' => [ new ParameterBag($request->query->all()), true ], - 'headers' => [ $request->headers, false ], - 'files' => [ $request->files, false ], - default => throw new \InvalidArgumentException(sprintf('Unknown bag "%s" for value consumer.', $attribute->bag)), + [$originalBag, $isOriginalBagCopied] = match ($attribute->bag) { + 'attributes' => [new ParameterBag($request->attributes->all()), true], + 'request' => [new ParameterBag($request->getPayload()->all()), true], + 'query' => [new ParameterBag($request->query->all()), true], + 'headers' => [$request->headers, false], + 'files' => [$request->files, false], }; $requestParameterName = $attribute->name ?? $argument->getName(); - if ($requestParameterName === '*') { + if ('*' === $requestParameterName) { // Complete bag contents, useful to get full query or payload as an array argument - $values = [ $originalBag->all() ]; - } else if ($originalBag->has($requestParameterName)) { + $values = [$originalBag->all()]; + } elseif ($originalBag->has($requestParameterName)) { $value = $originalBag->get($requestParameterName); // Expand variadic only if a list of values provided in request data. Otherwise, treated as a single element. - if ($argument->isVariadic() && is_array($value) && array_is_list($value)) { - $values = [ ...$value ]; + if ($argument->isVariadic() && \is_array($value) && array_is_list($value)) { + $values = [...$value]; } else { - $values = [ $value ]; + $values = [$value]; } } else { // No values bound to this argument, but we still need to call resolve @@ -89,51 +88,51 @@ final protected function resolveFromRequestParameters(Request $request, Argument } $filter = $attribute->filter ?? match ($argument->getType()) { - 'int' => \FILTER_VALIDATE_INT, - 'float' => \FILTER_VALIDATE_FLOAT, - 'bool' => \FILTER_VALIDATE_BOOL, + 'int' => \FILTER_VALIDATE_INT, + 'float' => \FILTER_VALIDATE_FLOAT, + 'bool' => \FILTER_VALIDATE_BOOL, 'string', 'array' => \FILTER_DEFAULT, - default => null, + default => null, }; - if ($filter !== null && !empty($values)) { - $options = [ 'flags' => 0 ]; - if (is_int($attribute->options)) { + if (null !== $filter && !empty($values)) { + $options = ['flags' => 0]; + if (\is_int($attribute->options)) { $options['flags'] = $attribute->options; - } else if (is_array($attribute->options)) { + } elseif (\is_array($attribute->options)) { $options['options'] = $attribute->options['options'] ?? $attribute->options; $options['flags'] = $attribute->options['flags'] ?? 0; } - if ($filter === \FILTER_VALIDATE_BOOL) { - $options['flags'] |= FILTER_NULL_ON_FAILURE; + if (\FILTER_VALIDATE_BOOL === $filter) { + $options['flags'] |= \FILTER_NULL_ON_FAILURE; } - if ($argument->getType() === 'array') { - $options['flags'] |= FILTER_FORCE_ARRAY | FILTER_REQUIRE_ARRAY; + if ('array' === $argument->getType()) { + $options['flags'] |= \FILTER_FORCE_ARRAY | \FILTER_REQUIRE_ARRAY; } - $isNullOnEmptyString = $options['flags'] & FILTER_FLAG_EMPTY_STRING_NULL; - $isNullOnFailure = $options['flags'] & FILTER_NULL_ON_FAILURE; + $isNullOnEmptyString = $options['flags'] & \FILTER_FLAG_EMPTY_STRING_NULL; + $isNullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; $failedValues = []; foreach ($values as $i => $value) { - if (is_object($value)) { + if (\is_object($value)) { continue; } - if ($value === null) { + if (null === $value) { unset($values[$i]); continue; } // Manually apply FILTER_FLAG_EMPTY_STRING_NULL to ignore empty string value for numeric arguments - if ($value === '' && $isNullOnEmptyString && ($filter === \FILTER_VALIDATE_INT || $filter === \FILTER_VALIDATE_FLOAT)) { + if ('' === $value && $isNullOnEmptyString && (\FILTER_VALIDATE_INT === $filter || \FILTER_VALIDATE_FLOAT === $filter)) { unset($values[$i]); continue; } - if (is_array($filter)) { - if (is_array($value)) { + if (\is_array($filter)) { + if (\is_array($value)) { $filtered = filter_var_array($value, $filter, $options['add_empty'] ?? true); } else { $filtered = $isNullOnFailure ? null : false; @@ -142,7 +141,7 @@ final protected function resolveFromRequestParameters(Request $request, Argument $filtered = filter_var($value, $filter, $options); } - if (($filtered === false && !$isNullOnFailure) || ($filtered === null && $isNullOnFailure)) { + if ((false === $filtered && !$isNullOnFailure) || (null === $filtered && $isNullOnFailure)) { $failedValues[] = $value; unset($values[$i]); } else { @@ -151,7 +150,7 @@ final protected function resolveFromRequestParameters(Request $request, Argument } if ($failedValues && $attribute->throwOnFilterFailure) { - throw new BadRequestHttpException(sprintf('Parameter "%s" is invalid.', $requestParameterName)); + throw new BadRequestHttpException(\sprintf('Parameter "%s" is invalid.', $requestParameterName)); } } @@ -168,15 +167,15 @@ final protected function resolveFromRequestParameters(Request $request, Argument $bag = clone $baseParametersBag; // Ensure we do not pass unfiltered value $bag->remove($argument->getName()); - $parametersBags = [ $bag ]; + $parametersBags = [$bag]; } $resolvedValues = []; foreach ($parametersBags as $bag) { $resolved = $this->resolveValue($request, $argument, $bag); - if (count($resolved) === 1) { + if (1 === \count($resolved)) { $resolvedValues[] = $resolved[0]; - } else if (count($resolved) > 1) { + } elseif (\count($resolved) > 1) { throw new \UnexpectedValueException('Method resolveValue() should return an array with a single item containing resolved value or an empty array if nothing was resolved.'); } } @@ -185,5 +184,4 @@ final protected function resolveFromRequestParameters(Request $request, Argument } abstract protected function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array; - } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php index d689f47b86ef0..b6e2d301bc47e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/RequestParameterValueResolverTraitTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\TestCase; @@ -9,9 +18,9 @@ use Symfony\Component\HttpKernel\Attribute\FromBody; use Symfony\Component\HttpKernel\Attribute\FromFile; use Symfony\Component\HttpKernel\Attribute\FromHeader; -use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Attribute\FromQuery; use Symfony\Component\HttpKernel\Attribute\FromRoute; +use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\RequestParameterValueResolverTrait; @@ -22,7 +31,6 @@ use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\UuidV1; -use Symfony\Component\Validator\Constraints\DateTime; class RequestParameterValueResolverTraitTest extends TestCase { @@ -31,132 +39,131 @@ class RequestParameterValueResolverTraitTest extends TestCase public static function intValues(): array { return [ - '123' => [ 123, 123 ], - '"123"' => [ 123, '123' ], - '(empty string)' => [ null, '' ], + '123' => [123, 123], + '"123"' => [123, '123'], + '(empty string)' => [null, ''], ]; } public static function floatValues(): array { return [ - '1.23' => [ 1.23, 1.23 ], - '"1.23"' => [ 1.23, '1.23' ], - '(empty string)' => [ null, '' ], + '1.23' => [1.23, 1.23], + '"1.23"' => [1.23, '1.23'], + '(empty string)' => [null, ''], ]; } public static function boolValues(): array { return [ - '"true"' => [ true, 'true' ], - '"1"' => [ true, '1' ], - '"on"' => [ true, 'on' ], - '"false"' => [ false, 'false' ], - '"off"' => [ false, 'off' ], - '"0"' => [ false, '0' ], - '(empty string)' => [ false, '' ], + '"true"' => [true, 'true'], + '"1"' => [true, '1'], + '"on"' => [true, 'on'], + '"false"' => [false, 'false'], + '"off"' => [false, 'off'], + '"0"' => [false, '0'], + '(empty string)' => [false, ''], ]; } public function testStringDefault() { $args = $this->argumentResolver->getArguments( - new Request(attributes: [ 'foo' => 'bar' ]), - fn(string $foo) => $foo, + new Request(attributes: ['foo' => 'bar']), + fn (string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testIgnoreWhenNoAttributes() { $this->expectExceptionMessage('requires the "$foo"'); $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => 'bar' ]), - fn(string $foo) => $foo, + new Request(query: ['foo' => 'bar']), + fn (string $foo) => $foo, ); $this->expectExceptionMessage('requires the "$foo"'); $this->argumentResolver->getArguments( - new Request(request: [ 'foo' => 'bar' ]), - fn(string $foo) => $foo, + new Request(request: ['foo' => 'bar']), + fn (string $foo) => $foo, ); } public function testStringFromRoute() { $args = $this->argumentResolver->getArguments( - new Request(attributes: [ 'foo' => 'bar' ]), - fn(#[FromRoute] string $foo) => $foo, + new Request(attributes: ['foo' => 'bar']), + fn (#[FromRoute] string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testStringFromQuery() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => 'bar' ]), - fn(#[FromQuery] string $foo) => $foo, + new Request(query: ['foo' => 'bar']), + fn (#[FromQuery] string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); - + $this->assertEquals(['bar'], $args); } public function testStringFromBody() { $args = $this->argumentResolver->getArguments( - new Request(request: [ 'foo' => 'bar' ]), - fn(#[FromBody] string $foo) => $foo, + new Request(request: ['foo' => 'bar']), + fn (#[FromBody] string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testStringFromJsonBody() { $args = $this->argumentResolver->getArguments( - new Request(server: [ 'HTTP_CONTENT_TYPE' => 'application/json' ], content: json_encode([ 'foo' => 'bar' ])), - fn(#[FromBody] string $foo) => $foo, + new Request(server: ['HTTP_CONTENT_TYPE' => 'application/json'], content: json_encode(['foo' => 'bar'])), + fn (#[FromBody] string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testStringFromHeader() { $args = $this->argumentResolver->getArguments( - new Request(server: [ 'HTTP_FOO' => 'bar' ]), - fn(#[FromHeader] string $foo) => $foo + new Request(server: ['HTTP_FOO' => 'bar']), + fn (#[FromHeader] string $foo) => $foo ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testStringFromQueryRenamed() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'theFoo' => 'bar' ]), - fn(#[FromQuery('theFoo')] string $foo) => $foo + new Request(query: ['theFoo' => 'bar']), + fn (#[FromQuery('theFoo')] string $foo) => $foo ); - $this->assertSame([ 'bar' ], $args); + $this->assertSame(['bar'], $args); } public function testWholeBagAsParameter() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'sort' => 'name', 'order' => 'desc' ]), - fn(#[FromQuery('*')] array $sorting) => $sorting + new Request(query: ['sort' => 'name', 'order' => 'desc']), + fn (#[FromQuery('*')] array $sorting) => $sorting ); - $this->assertSame([ [ 'sort' => 'name', 'order' => 'desc' ] ], $args); + $this->assertSame([['sort' => 'name', 'order' => 'desc']], $args); } public function testStringFromHeaderRenamed() { $args = $this->argumentResolver->getArguments( - new Request(server: [ 'HTTP_X_FORWARDED_FOR' => 'bar' ]), - fn(#[FromHeader('x-forwarded-for')] string $foo) => $foo, + new Request(server: ['HTTP_X_FORWARDED_FOR' => 'bar']), + fn (#[FromHeader('x-forwarded-for')] string $foo) => $foo, ); - $this->assertEquals([ 'bar' ], $args); + $this->assertEquals(['bar'], $args); } public function testFromFile() @@ -168,13 +175,13 @@ public function testFromFile() 'type' => 'text/plain', 'tmp_name' => $file, 'size' => filesize($file), - 'error' => UPLOAD_ERR_OK, - ] + 'error' => \UPLOAD_ERR_OK, + ], ]); $args = $this->argumentResolver->getArguments( $request, - fn(#[FromFile] UploadedFile $attach) => $attach, + fn (#[FromFile] UploadedFile $attach) => $attach, ); $this->assertCount(1, $args); $this->assertInstanceOf(UploadedFile::class, $args[0]); @@ -184,54 +191,54 @@ public function testFromFile() public function testInt() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'page' => '1' ]), - fn(#[FromQuery] int $page) => $page, + new Request(query: ['page' => '1']), + fn (#[FromQuery] int $page) => $page, ); - $this->assertEquals([ 1 ], $args); + $this->assertEquals([1], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'page' => '' ]), - fn(#[FromQuery] ?int $page) => $page, + new Request(query: ['page' => '']), + fn (#[FromQuery] ?int $page) => $page, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); } public function testValidationFailed() { $this->expectException(BadRequestHttpException::class); $this->argumentResolver->getArguments( - new Request(query: [ 'page' => 'not-an-integer' ]), - fn(#[FromQuery()] ?int $page) => $page, + new Request(query: ['page' => 'not-an-integer']), + fn (#[FromQuery()] ?int $page) => $page, ); } public function testEmptyStringAsNull() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'page' => '' ]), - fn(#[FromQuery] ?int $page) => $page, + new Request(query: ['page' => '']), + fn (#[FromQuery] ?int $page) => $page, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'weight' => '' ]), - fn(#[FromQuery] ?float $weight) => $weight, + new Request(query: ['weight' => '']), + fn (#[FromQuery] ?float $weight) => $weight, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'title' => '' ]), - fn(#[FromQuery] ?int $title) => $title, + new Request(query: ['title' => '']), + fn (#[FromQuery] ?int $title) => $title, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); } public function testEmptyStringAsNullDisabled() { $this->expectException(BadRequestHttpException::class); $this->argumentResolver->getArguments( - new Request(query: [ 'page' => '' ]), - fn(#[FromQuery(options: null)] ?int $page) => $page, + new Request(query: ['page' => '']), + fn (#[FromQuery(options: null)] ?int $page) => $page, ); } @@ -240,8 +247,8 @@ public function testMultipleAttributesDisallowed() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Multiple'); $this->argumentResolver->getArguments( - new Request(query: [ 'page' => 1 ], request: [ 'page' => 2 ]), - fn(#[FromQuery] #[FromBody] ?int $page) => $page, + new Request(query: ['page' => 1], request: ['page' => 2]), + fn (#[FromQuery] #[FromBody] ?int $page) => $page, ); } @@ -249,98 +256,97 @@ public function testMultipleAttributesDisallowed() public function testIntFromQuery($expected, $actual) { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => $actual ]), - fn(#[FromQuery] ?int $int) => $int, + new Request(query: ['int' => $actual]), + fn (#[FromQuery] ?int $int) => $int, ); - $this->assertEquals([ $expected ], $args); + $this->assertEquals([$expected], $args); } /** @dataProvider floatValues */ public function testFloatFromQuery($expected, $actual) { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'float' => $actual ]), - fn(#[FromQuery] ?float $float) => $float, + new Request(query: ['float' => $actual]), + fn (#[FromQuery] ?float $float) => $float, ); - $this->assertEquals([ $expected ], $args); + $this->assertEquals([$expected], $args); } public function testFilterFlags() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => '0xff' ]), - fn(#[FromQuery(throwOnFilterFailure: false)] ?int $int) => $int, + new Request(query: ['int' => '0xff']), + fn (#[FromQuery(throwOnFilterFailure: false)] ?int $int) => $int, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => '0xff' ]), - fn(#[FromQuery(options: FILTER_FLAG_ALLOW_HEX)] ?int $int) => $int, + new Request(query: ['int' => '0xff']), + fn (#[FromQuery(options: \FILTER_FLAG_ALLOW_HEX)] ?int $int) => $int, ); - $this->assertEquals([ 255 ], $args); + $this->assertEquals([255], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => '0xff' ]), - fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX ])] ?int $int) => $int, + new Request(query: ['int' => '0xff']), + fn (#[FromQuery(options: ['flags' => \FILTER_FLAG_ALLOW_HEX])] ?int $int) => $int, ); - $this->assertEquals([ 255 ], $args); + $this->assertEquals([255], $args); } public function testFilterOptions() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery] ?int $int) => $int, + new Request(query: ['int' => 42]), + fn (#[FromQuery] ?int $int) => $int, ); - $this->assertEquals([ 42 ], $args); + $this->assertEquals([42], $args); $this->expectException(BadRequestHttpException::class); $this->argumentResolver->getArguments( - new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery(options: [ 'max_range' => 10 ])] ?int $int) => $int, + new Request(query: ['int' => 42]), + fn (#[FromQuery(options: ['max_range' => 10])] ?int $int) => $int, ); } public function testFilterFlagAndOptions() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 100 ])] ?int $int) => $int, + new Request(query: ['int' => 42]), + fn (#[FromQuery(options: ['flags' => \FILTER_FLAG_ALLOW_HEX, 'max_range' => 100])] ?int $int) => $int, ); - $this->assertEquals([ 42 ], $args); + $this->assertEquals([42], $args); $this->expectException(BadRequestHttpException::class); $this->argumentResolver->getArguments( - new Request(query: [ 'int' => 42 ]), - fn(#[FromQuery(options: [ 'flags' => FILTER_FLAG_ALLOW_HEX, 'max_range' => 10 ])] ?int $int) => $int, + new Request(query: ['int' => 42]), + fn (#[FromQuery(options: ['flags' => \FILTER_FLAG_ALLOW_HEX, 'max_range' => 10])] ?int $int) => $int, ); - } /** @dataProvider boolValues */ public function testBoolFromQuery($expected, $actual) { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'bool' => $actual ]), - fn(#[FromQuery] bool $bool) => $bool, + new Request(query: ['bool' => $actual]), + fn (#[FromQuery] bool $bool) => $bool, ); - $this->assertEquals([ $expected ], $args); + $this->assertEquals([$expected], $args); } public function testBoolFromQueryMissing() { $args = $this->argumentResolver->getArguments( new Request(query: []), - fn(#[FromQuery] ?bool $bool) => $bool, + fn (#[FromQuery] ?bool $bool) => $bool, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); $this->expectExceptionMessage('requires the "$bool"'); $args = $this->argumentResolver->getArguments( new Request(query: []), - fn(#[FromQuery] bool $bool) => $bool, + fn (#[FromQuery] bool $bool) => $bool, ); - $this->assertEquals([ null ], $args); + $this->assertEquals([null], $args); } public function testUidFromQuery() @@ -348,10 +354,10 @@ public function testUidFromQuery() $uid = new UuidV1(); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'uid' => $uid->toString() ]), - fn(#[FromQuery] AbstractUid $uid) => $uid, + new Request(query: ['uid' => (string) $uid]), + fn (#[FromQuery] AbstractUid $uid) => $uid, ); - $this->assertEquals([ $uid ], $args); + $this->assertEquals([$uid], $args); } public function testDateFromQuery() @@ -359,16 +365,16 @@ public function testDateFromQuery() $date = new \DateTimeImmutable('2021-01-01 01:02:03'); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'date' => $date->format(\DateTimeInterface::ATOM) ]), - fn(#[FromQuery] \DateTimeInterface $date) => $date, + new Request(query: ['date' => $date->format(\DateTimeInterface::ATOM)]), + fn (#[FromQuery] \DateTimeInterface $date) => $date, ); - $this->assertEquals([ $date ], $args); + $this->assertEquals([$date], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'date' => $date->getTimestamp() ]), - fn(#[FromQuery] \DateTimeInterface $date) => $date, + new Request(query: ['date' => $date->getTimestamp()]), + fn (#[FromQuery] \DateTimeInterface $date) => $date, ); - $this->assertEquals([ $date ], $args); + $this->assertEquals([$date], $args); } public function testDateWithMapDateTimeFromQuery() @@ -376,52 +382,52 @@ public function testDateWithMapDateTimeFromQuery() $date = new \DateTimeImmutable('2021-12-01 01:02:03'); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'date' => '2021/01/12 01:02:03' ]), - fn(#[FromQuery] #[MapDateTime(format: 'Y/d/m H:i:s')] \DateTimeInterface $date) => $date, + new Request(query: ['date' => '2021/01/12 01:02:03']), + fn (#[FromQuery] #[MapDateTime(format: 'Y/d/m H:i:s')] \DateTimeInterface $date) => $date, ); - $this->assertEquals([ $date ], $args); + $this->assertEquals([$date], $args); } public function testEnumFromQuery() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'suit' => 'H' ]), - fn(#[FromQuery] Suit $suit) => $suit, + new Request(query: ['suit' => 'H']), + fn (#[FromQuery] Suit $suit) => $suit, ); - $this->assertEquals([ Suit::Hearts ], $args); + $this->assertEquals([Suit::Hearts], $args); } public function testArrayFromQuery() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ '1', '2' ] ]), - fn(#[FromQuery('foo')] array $foo) => $foo, + new Request(query: ['foo' => ['1', '2']]), + fn (#[FromQuery('foo')] array $foo) => $foo, ); - $this->assertSame([ [ '1', '2' ] ], $args); + $this->assertSame([['1', '2']], $args); } public function testVariadic() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ '1', '2' ] ]), - fn(#[FromQuery('foo')] int ...$foo) => $foo, + new Request(query: ['foo' => ['1', '2']]), + fn (#[FromQuery('foo')] int ...$foo) => $foo, ); - $this->assertSame([ 1, 2 ], $args); + $this->assertSame([1, 2], $args); $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ '1', '0' ] ]), - fn(#[FromQuery('foo')] bool ...$foo) => $foo, + new Request(query: ['foo' => ['1', '0']]), + fn (#[FromQuery('foo')] bool ...$foo) => $foo, ); - $this->assertSame([ true, false ], $args); + $this->assertSame([true, false], $args); } public function testVariadicEnum() { $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ 'H', 'S' ] ]), - fn(#[FromQuery('foo')] Suit ...$foo) => $foo, + new Request(query: ['foo' => ['H', 'S']]), + fn (#[FromQuery('foo')] Suit ...$foo) => $foo, ); - $this->assertSame([ Suit::Hearts, Suit::Spades ], $args); + $this->assertSame([Suit::Hearts, Suit::Spades], $args); } public function testVariadicUid() @@ -431,8 +437,8 @@ public function testVariadicUid() new UuidV1(), ]; $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ $uids[0]->toString(), $uids[1]->toString() ] ]), - fn(#[FromQuery('foo')] AbstractUid ...$foo) => $foo, + new Request(query: ['foo' => [(string) $uids[0], (string) $uids[1]]]), + fn (#[FromQuery('foo')] AbstractUid ...$foo) => $foo, ); $this->assertEquals($uids, $args); } @@ -444,39 +450,39 @@ public function testVariadicDateTime() new \DateTimeImmutable('2021-02-02 02:02:02'), ]; $args = $this->argumentResolver->getArguments( - new Request(query: [ 'foo' => [ $dates[0]->getTimestamp(), $dates[1]->format(\DATE_ATOM) ] ]), - fn(#[FromQuery('foo')] \DateTimeInterface ...$foo) => $foo, + new Request(query: ['foo' => [$dates[0]->getTimestamp(), $dates[1]->format(\DATE_ATOM)]]), + fn (#[FromQuery('foo')] \DateTimeInterface ...$foo) => $foo, ); $this->assertEquals($dates, $args); } public function testExtraParametersAreKept() { - $resolver = new ArgumentResolver(new ArgumentMetadataFactory(), [ new test_AllBagParametersValueResolver() ]); + $resolver = new ArgumentResolver(new ArgumentMetadataFactory(), [new test_AllBagParametersValueResolver()]); $args = $resolver->getArguments( - new Request(attributes: [ 'country' => 'XX', 'area' => '42' ]), + new Request(attributes: ['country' => 'XX', 'area' => '42']), fn ($area) => $area ); - $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + $this->assertSame([['country' => 'XX', 'area' => '42']], $args); $args = $resolver->getArguments( - new Request(attributes: [ 'country' => 'XX', 'area' => '42' ]), + new Request(attributes: ['country' => 'XX', 'area' => '42']), fn (#[FromRoute] $area) => $area ); - $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + $this->assertSame([['country' => 'XX', 'area' => '42']], $args); $args = $resolver->getArguments( - new Request(query: [ 'country' => 'XX', 'area' => '42' ]), + new Request(query: ['country' => 'XX', 'area' => '42']), fn (#[FromQuery] $area) => $area ); - $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + $this->assertSame([['country' => 'XX', 'area' => '42']], $args); $args = $resolver->getArguments( - new Request(request: [ 'country' => 'XX', 'area' => '42' ]), + new Request(request: ['country' => 'XX', 'area' => '42']), fn (#[FromBody] $area) => $area ); - $this->assertSame([ [ 'country' => 'XX', 'area' => '42' ] ], $args); + $this->assertSame([['country' => 'XX', 'area' => '42']], $args); } protected function setUp(): void @@ -487,19 +493,18 @@ protected function setUp(): void new ArgumentResolver\BackedEnumValueResolver(), new ArgumentResolver\DateTimeValueResolver(), new ArgumentResolver\UidValueResolver(), - ...ArgumentResolver::getDefaultArgumentValueResolvers() + ...ArgumentResolver::getDefaultArgumentValueResolvers(), ] ); } - } -class test_AllBagParametersValueResolver implements ValueResolverInterface { +class test_AllBagParametersValueResolver implements ValueResolverInterface +{ use RequestParameterValueResolverTrait; public function resolveValue(Request $request, ArgumentMetadata $argument, ParameterBag $valueBag): array { - return [ $valueBag->all() ]; + return [$valueBag->all()]; } - } From 9e5b4ffa745635777b49a1e93658fd8316e074b1 Mon Sep 17 00:00:00 2001 From: klkvsk Date: Tue, 3 Dec 2024 22:45:28 +0300 Subject: [PATCH 6/6] target symfony 7.3 --- src/Symfony/Bridge/Doctrine/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index b893791dc0408..94a089685b853 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -31,7 +31,7 @@ "symfony/doctrine-messenger": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", "symfony/form": "^6.4.6|^7.0.6", - "symfony/http-kernel": "^7.2", + "symfony/http-kernel": "^7.3", "symfony/lock": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", @@ -58,7 +58,7 @@ "symfony/dependency-injection": "<6.4", "symfony/form": "<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.4", - "symfony/http-kernel": "<7.2", + "symfony/http-kernel": "<7.3", "symfony/lock": "<6.4", "symfony/messenger": "<6.4", "symfony/property-info": "<6.4",