diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/PostValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/PostValueResolver.php new file mode 100644 index 0000000000000..e1063db3068dd --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/PostValueResolver.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +final class PostValueResolver implements ArgumentValueResolverInterface +{ + public function supports(Request $request, ArgumentMetadata $argument): bool + { + return [] !== $argument->getAttributesOfType(ResolvePostValue::class); + } + + public function resolve(Request $request, ArgumentMetadata $argument): array + { + /** + * @psalm-ignore-var + * + * @var list $resolveRequestValues + */ + $resolveRequestValues = $argument->getAttributesOfType(ResolvePostValue::class); + if ([] === $resolveRequestValues) { + throw new \LogicException(sprintf('Argument does not have a "%s" attribute.', ResolvePostValue::class)); + } + $resolveRequestValue = $resolveRequestValues[0]; + $key = $resolveRequestValue->name ?? $argument->getName(); + /** @var mixed $default */ + $default = $resolveRequestValue->default ?? ($argument->hasDefaultValue() ? $argument->getDefaultValue() : null); + /** @psalm-suppress MixedArgument */ + $value = $request->request->get($key, $default); + if (null === $value) { + if ($argument->isNullable()) { + return [null]; + } else { + throw new BadRequestHttpException(sprintf('Request param "%s" does not exist.', $key)); + } + } + $coercedValue = match ($argument->getType()) { + 'bool' => filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE), + 'int' => filter_var($value, \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE), + 'float' => filter_var($value, \FILTER_VALIDATE_FLOAT, \FILTER_NULL_ON_FAILURE), + default => $value, + }; + if (null === $coercedValue) { + throw new BadRequestHttpException(sprintf('Request param "%s" could not be coerced to a "%s".', $key, $argument->getType())); + } + + return [$coercedValue]; + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ResolvePostValue.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ResolvePostValue.php new file mode 100644 index 0000000000000..cb5e548e55efa --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ResolvePostValue.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +final class ResolvePostValue +{ + public function __construct( + public readonly string|null $name = null, + public readonly mixed $default = null, + ) { + } +} diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php index df791db3d7e55..70dbe0b2751f8 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php @@ -121,7 +121,7 @@ public function getAttributes(string $name = null, int $flags = 0): array * @param class-string $name * @param self::IS_INSTANCEOF|0 $flags * - * @return array + * @return list */ public function getAttributesOfType(string $name, int $flags = 0): array { diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/PostValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/PostValueResolverTest.php new file mode 100644 index 0000000000000..b8a12f63132c6 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/PostValueResolverTest.php @@ -0,0 +1,211 @@ + + * + * 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\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\PostValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ResolvePostValue; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +class PostValueResolverTest extends TestCase +{ + public function testSupports() + { + $sut = new PostValueResolver(); + $request = new Request(); + $argumentMetadata = new ArgumentMetadata( + '', + null, + false, + false, + null, + attributes: [new ResolvePostValue()], + ); + $this->assertTrue($sut->supports($request, $argumentMetadata)); + $argumentMetadata = new ArgumentMetadata( + '', + null, + false, + false, + null, + ); + $this->assertFalse($sut->supports($request, $argumentMetadata)); + } + + /** + * @dataProvider provideForTestResolve + */ + public function testResolve( + array $post, + ArgumentMetadata $argumentMetadata, + mixed $expectedValue, + ) { + $sut = new PostValueResolver(); + $request = new Request(request: $post); + $this->assertSame([$expectedValue], $sut->resolve($request, $argumentMetadata)); + } + + public function provideForTestResolve(): iterable + { + yield 'arg name' => [ + ['arg_name' => 'value'], + new ArgumentMetadata( + 'arg_name', + 'string', + false, + false, + null, + false, + [new ResolvePostValue()], + ), + 'value', + ]; + yield 'attribute name' => [ + ['post_name' => 'value'], + new ArgumentMetadata( + 'arg_name', + 'string', + false, + false, + null, + false, + [new ResolvePostValue('post_name')], + ), + 'value', + ]; + yield 'attribute default' => [ + [], + new ArgumentMetadata( + 'arg_name', + 'string', + false, + false, + null, + false, + [new ResolvePostValue(default: 'value')], + ), + 'value', + ]; + yield 'argument default' => [ + [], + new ArgumentMetadata( + 'arg_name', + 'string', + false, + true, + 'value', + false, + [new ResolvePostValue()], + ), + 'value', + ]; + yield 'nullable argument' => [ + [], + new ArgumentMetadata( + 'arg_name', + 'string', + false, + false, + null, + true, + [new ResolvePostValue()], + ), + null, + ]; + yield 'bool coercion - false' => [ + ['arg_name' => '0'], + new ArgumentMetadata( + 'arg_name', + 'bool', + false, + false, + null, + false, + [new ResolvePostValue()], + ), + false, + ]; + yield 'bool coercion - true' => [ + ['arg_name' => '1'], + new ArgumentMetadata( + 'arg_name', + 'bool', + false, + false, + null, + false, + [new ResolvePostValue()], + ), + true, + ]; + yield 'int coercion' => [ + ['arg_name' => '13'], + new ArgumentMetadata( + 'arg_name', + 'int', + false, + false, + null, + false, + [new ResolvePostValue()], + ), + 13, + ]; + yield 'float coercion' => [ + ['arg_name' => '13.0'], + new ArgumentMetadata( + 'arg_name', + 'float', + false, + false, + null, + false, + [new ResolvePostValue()], + ), + 13.0, + ]; + } + + public function testLogicException() + { + $sut = new PostValueResolver(); + $request = new Request(); + $argumentMetadata = new ArgumentMetadata('', null, false, false, null); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Argument does not have a "Symfony\Component\HttpKernel\Controller\ArgumentResolver\ResolvePostValue" attribute.'); + $sut->resolve($request, $argumentMetadata); + } + + public function testPostParamDoesNotExistException() + { + $sut = new PostValueResolver(); + $request = new Request(); + $argumentMetadata = new ArgumentMetadata('arg_name', 'string', false, false, null, attributes: [new ResolvePostValue()]); + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('Request param "arg_name" does not exist.'); + $sut->resolve($request, $argumentMetadata); + } + + public function testPostParamCanNotBeCoercedException() + { + $sut = new PostValueResolver(); + $request = new Request(request: ['arg_name' => 'bogus']); + $argumentMetadata = new ArgumentMetadata('arg_name', 'bool', false, false, null, attributes: [new ResolvePostValue()]); + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('Request param "arg_name" could not be coerced to a "bool".'); + $sut->resolve($request, $argumentMetadata); + } +}