From 3f721434062045c1bad7849d1b412d07aa2b98e4 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Sat, 23 Mar 2024 18:27:37 -0400 Subject: [PATCH] map a list of items with MapRequestPayload attribute --- .../Attribute/MapRequestPayload.php | 2 + src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../RequestPayloadValueResolver.php | 17 ++++- .../RequestPayloadValueResolverTest.php | 70 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php index 5ee2ffeb64e5c..cf086380c03f0 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php @@ -32,6 +32,7 @@ class MapRequestPayload extends ValueResolver * @param string|GroupSequence|array|null $validationGroups The validation groups to use when validating the query string mapping * @param class-string $resolver The class name of the resolver to use * @param int $validationFailedStatusCode The HTTP code to return if the validation fails + * @param class-string|string|null $type The element type for array deserialization */ public function __construct( public readonly array|string|null $acceptFormat = null, @@ -39,6 +40,7 @@ public function __construct( public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, + public readonly ?string $type = null, ) { parent::__construct($resolver); } diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index e9d4f2da90630..f639e2ada7517 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add `HttpException::fromStatusCode()` * Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails * Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved + * Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items 7.0 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 6274703a1731a..83e8abe7ffa93 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; @@ -78,6 +79,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); } + if ($attribute instanceof MapRequestPayload) { + if ('array' === $argument->getType()) { + if (!$attribute->type) { + throw new NearMissValueResolverException(sprintf('Please set the $type argument of the #[%s] attribute to the type of the objects in the expected array.', MapRequestPayload::class)); + } + } elseif ($attribute->type) { + throw new NearMissValueResolverException(sprintf('Please set its type to "array" when using argument $type of #[%s].', MapRequestPayload::class)); + } + } + $attribute->metadata = $argument; return [$attribute]; @@ -170,7 +181,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $ return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]); } - private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object + private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): object|array|null { if (null === $format = $request->getContentTypeFormat()) { throw new UnsupportedMediaTypeHttpException('Unsupported format.'); @@ -180,6 +191,10 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); } + if ('array' === $type && null !== $attribute->type) { + $type = $attribute->type.'[]'; + } + if ($data = $request->request->all()) { return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : [])); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 4825ad890facd..b277650b44b45 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -20,11 +20,13 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -421,6 +423,74 @@ public function testRequestInputValidationPassed() $this->assertEquals([$payload], $event->getArguments()); } + public function testRequestArrayDenormalization() + { + $input = [ + ['price' => '50'], + ['price' => '23'], + ]; + $payload = [ + new RequestPayload(50), + new RequestPayload(23), + ]; + + $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once()) + ->method('validate') + ->willReturn(new ConstraintViolationList()); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), + ]); + $request = Request::create('/', 'POST', $input); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $this->assertEquals([$payload], $event->getArguments()); + } + + public function testItThrowsOnMissingAttributeType() + { + $serializer = new Serializer(); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + $request = Request::create('/', 'POST'); + $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); + + $this->expectException(NearMissValueResolverException::class); + $this->expectExceptionMessage('Please set the $type argument of the #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload] attribute to the type of the objects in the expected array.'); + $resolver->resolve($request, $argument); + } + + public function testItThrowsOnInvalidAttributeTypeUsage() + { + $serializer = new Serializer(); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', null, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), + ]); + $request = Request::create('/', 'POST'); + $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); + + $this->expectException(NearMissValueResolverException::class); + $this->expectExceptionMessage('Please set its type to "array" when using argument $type of #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload].'); + $resolver->resolve($request, $argument); + } + public function testItThrowsOnVariadicArgument() { $serializer = new Serializer(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy