8000 feature #54385 [HttpKernel] Map a list of items with `MapRequestPaylo… · symfony/symfony@9549cc2 · GitHub
[go: up one dir, main page]

Skip to content
10000

Commit 9549cc2

Browse files
committed
feature #54385 [HttpKernel] Map a list of items with MapRequestPayload attribute (yceruto)
This PR was merged into the 7.1 branch. Discussion ---------- [HttpKernel] Map a list of items with `MapRequestPayload` attribute | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT **Request:** ```json Content-Type: application/json [ {"price": 50}, {"price": 23} ] ``` **Controller:** ```php class MyController { public function __invoke( #[MapRequestPayload(type: Price::class)] array $prices, ): Response { // do something with $prices -> array<Price> } } ``` Cheers! Commits ------- 3f72143 map a list of items with MapRequestPayload attribute
2 parents c35173c + 3f72143 commit 9549cc2

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed

src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ class MapRequestPayload extends ValueResolver
3232
* @param string|GroupSequence|array<string>|null $validationGroups The validation groups to use when validating the query string mapping
3333
* @param class-string $resolver The class name of the resolver to use
3434
* @param int $validationFailedStatusCode The HTTP code to return if the validation fails
35+
* @param class-string|string|null $type The element type for array deserialization
3536
*/
3637
public function __construct(
3738
public readonly array|string|null $acceptFormat = null,
3839
public readonly array $serializationContext = [],
3940
public readonly string|GroupSequence|array|null $validationGroups = null,
4041
string $resolver = RequestPayloadValueResolver::class,
4142
public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY,
43+
public readonly ?string $type = null,
4244
) {
4345
parent::__construct($resolver);
4446
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `HttpException::fromStatusCode()`
99
* Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails
1010
* Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved
11+
* Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items
1112

1213
7.0
1314
---

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
2121
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
2222
use Symfony\Component\HttpKernel\Exception\HttpException;
23+
use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException;
2324
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
2425
use Symfony\Component\HttpKernel\KernelEvents;
2526
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
@@ -78,6 +79,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
7879
throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
7980
}
8081

82+
if ($attribute instanceof MapRequestPayload) {
83+
if ('array' === $argument->getType()) {
84+
if (!$attribute->type) {
85+
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));
86+
}
87+
} elseif ($attribute->type) {
88+
throw new NearMissValueResolverException(sprintf('Please set its type to "array" when using argument $type of #[%s].', MapRequestPayload::class));
89+
}
90+
}
91+
8192
$attribute->metadata = $argument;
8293

8394
return [$attribute];
@@ -170,7 +181,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $
170181
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
171182
}
172183

173-
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object
184+
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): object|array|null
174185
{
175186
if (null === $format = $request->getContentTypeFormat()) {
176187
throw new UnsupportedMediaTypeHttpException('Unsupported format.');
@@ -180,6 +191,10 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
180191
throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format));
181192
}
182193

194+
if ('array' === $type && null !== $attribute->type) {
195+
$type = $attribute->type.'[]';
196+
}
197+
183198
if ($data = $request->request->all()) {
184199
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
185200
}

src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
2121
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
2222
use Symfony\Component\HttpKernel\Exception\HttpException;
23+
use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException;
2324
use Symfony\Component\HttpKernel\HttpKernelInterface;
2425
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2526
use Symfony\Component\Serializer\Encoder\XmlEncoder;
2627
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
2728
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
29+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
2830
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2931
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
3032
use Symfony\Component\Serializer\Serializer;
@@ -421,6 +423,74 @@ public function testRequestInputValidationPassed()
421423
$this->assertEquals([$payload], $event->getArguments());
422424
}
423425

426+
public function testRequestArrayDenormalization()
427+
{
428+
$input = [
429+
['price' => '50'],
430+
['price' => '23'],
431+
];
432+
$payload = [
433+
new RequestPayload(50),
434+
new RequestPayload(23),
435+
];
436+
437+
$serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]);
438+
439+
$validator = $this->createMock(ValidatorInterface::class);
440+
$validator->expects($this->once())
441+
->method('validate')
442+
->willReturn(new ConstraintViolationList());
443+
444+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
445+
446+
$argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [
447+
MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class),
448+
]);
449+
$request = Request::create('/', 'POST', $input);
450+
451+
$kernel = $this->createMock(HttpKernelInterface::class);
452+
$arguments = $resolver->resolve($request, $argument);
453+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
454+
455+
$resolver->onKernelControllerArguments($event);
456+
457+
$this->assertEquals([$payload], $event->getArguments());
458+
}
459+
460+
public function testItThrowsOnMissingAttributeType()
461+
{
462+
$serializer = new Serializer();
463+
$validator = $this->createMock(ValidatorInterface::class);
464+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
465+
466+
$argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [
467+
MapRequestPayload::class => new MapRequestPayload(),
468+
]);
469+
$request = Request::create('/', 'POST');
470+
$request->attributes->set('_controller', 'App\Controller\SomeController::someMethod');
471+
472+
$this->expectException(NearMissValueResolverException::class);
473+
$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.');
474+
$resolver->resolve($request, $argument);
475+
}
476+
477+
public function testItThrowsOnInvalidAttributeTypeUsage()
478+
{
479+
$serializer = new Serializer();
480+
$validator = $this->createMock(ValidatorInterface::class);
481+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
482+
483+
$argument = new ArgumentMetadata('prices', null, false, false, null, false, [
484+
MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class),
485+
]);
486+
$request = Request::create('/', 'POST');
487+
$request->attributes->set('_controller', 'App\Controller\SomeController::someMethod');
488+
489+
$this->expectException(NearMissValueResolverException::class);
490+
$this->expectExceptionMessage('Please set its type to "array" when using argument $type of #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload].');
491+
$resolver->resolve($request, $argument);
492+
}
493+
424494
public function testItThrowsOnVariadicArgument()
425495
{
426496
$serializer = new Serializer();

0 commit comments

Comments
 (0)
0