From 4d118c0f54ed6c5766b6f1d2fc266b21f326f22d Mon Sep 17 00:00:00 2001 From: zim32 Date: Sun, 25 Jun 2023 13:19:44 +0300 Subject: [PATCH] [HttpKernel] RequestPayloadValueResolver Add support for custom http status code --- .../HttpKernel/Attribute/MapQueryString.php | 2 + .../Attribute/MapRequestPayload.php | 2 + src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../RequestPayloadValueResolver.php | 4 +- .../RequestPayloadValueResolverTest.php | 77 ++++++++++++++++++- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapQueryString.php b/src/Symfony/Component/HttpKernel/Attribute/MapQueryString.php index 6ba85e1bddd9e..83722266ee4e3 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapQueryString.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapQueryString.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Attribute; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; @@ -29,6 +30,7 @@ public function __construct( public readonly array $serializationContext = [], public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, + public readonly int $validationFailedStatusCode = Response::HTTP_NOT_FOUND, ) { parent::__construct($resolver); } diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php index 02c01fa40bf39..cbac606e83fe1 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Attribute; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; @@ -30,6 +31,7 @@ public function __construct( public readonly array $serializationContext = [], public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, + public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, ) { parent::__construct($resolver); } diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index b3d888889f541..aed4b408cb845 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * `BundleInterface` no longer extends `ContainerAwareInterface` * Add optional `$className` parameter to `ControllerEvent::getAttributes()` * Add native return types to `TraceableEventDispatcher` and to `MergeExtensionConfigurationPass` + * Add argument `$validationFailedStatusCode` to `#[MapQueryString]` and `#[MapRequestPayload]` 6.3 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 370097cda4b08..0904a34232a60 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -88,10 +88,10 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo foreach ($arguments as $i => $argument) { if ($argument instanceof MapQueryString) { $payloadMapper = 'mapQueryString'; - $validationFailedCode = Response::HTTP_NOT_FOUND; + $validationFailedCode = $argument->validationFailedStatusCode; } elseif ($argument instanceof MapRequestPayload) { $payloadMapper = 'mapRequestPayload'; - $validationFailedCode = Response::HTTP_UNPROCESSABLE_ENTITY; + $validationFailedCode = $argument->validationFailedStatusCode; } else { continue; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 4ca326392be56..454170df1fb2e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -124,7 +124,7 @@ public function testNullableValueArgument() $resolver->onKernelControllerArguments($event); - $this->assertEquals([null], $event->getArguments()); + $this->assertSame([null], $event->getArguments()); } public function testQueryNullableValueArgument() @@ -251,6 +251,7 @@ public function testValidationNotPassed() $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); + $this->assertSame(404, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); @@ -601,6 +602,73 @@ public static function provideValidationGroupsOnManyTypes(): iterable new MapQueryString(validationGroups: new Assert\GroupSequence(['strict'])), ]; } + + public function testQueryValidationErrorCustomStatusCode() + { + $serializer = new Serializer([new ObjectNormalizer()], []); + + $validator = $this->createMock(ValidatorInterface::class); + + $validator->expects($this->once()) + ->method('validate') + ->willReturn(new ConstraintViolationList([new ConstraintViolation('Page is invalid', null, [], '', null, '')])); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('page', QueryPayload::class, false, false, null, false, [ + MapQueryString::class => new MapQueryString(validationFailedStatusCode: 400), + ]); + $request = Request::create('/?page=123'); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertSame(400, $e->getStatusCode()); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('Page is invalid', $validationFailedException->getViolations()[0]->getMessage()); + } + } + + public function testRequestPayloadValidationErrorCustomStatusCode() + { + $content = '{"price": 50, "title": ["not a string"]}'; + $payload = new RequestPayload(50); + $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once()) + ->method('validate') + ->with($payload) + ->willReturn(new ConstraintViolationList([new ConstraintViolation('Test', null, [], '', null, '')])); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(validationFailedStatusCode: 400), + ]); + $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertSame(400, $e->getStatusCode()); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage()); + } + } } class RequestPayload @@ -612,3 +680,10 @@ public function __construct(public readonly float $price) { } } + +class QueryPayload +{ + public function __construct(public readonly float $page) + { + } +}