8000 map a list of items with MapRequestPayload attribute · symfony/symfony@948e65b · GitHub
[go: up one dir, main page]

Skip to content

Commit 948e65b

Browse files
committed
map a list of items with MapRequestPayload attribute
1 parent b5ee977 commit 948e65b

File tree

4 files changed

+104
-1
lines changed

4 files changed

+104
-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: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
7878
throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
7979
}
8080

81+
if ($attribute instanceof MapRequestPayload) {
82+
if ('array' === $argument->getType()) {
83+
if (!$attribute->type) {
84+
throw new \LogicException(sprintf('Specify the #[MapRequestPayload]\'s $type argument for $%s argument in %s when the type is "array" to ensure correct data deserialization.', $argument->getName(), $this->getController($request)));
85+
}
86+
} elseif ($attribute->type) {
87+
throw new \LogicException(sprintf('The #[MapRequestPayload]\'s $type argument is only supported when the $%s argument type is "array" in %s, "%s" given.', $argument->getName(), $this->getController($request), $argument->getType() ?? 'null'));
88+
}
89+
}
90+
8191
$attribute->metadata = $argument;
8292

8393
return [$attribute];
@@ -170,7 +180,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $
170180
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
171181
}
172182

173-
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object
183+
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): object|array|null
174184
{
175185
if (null === $format = $request->getContentTypeFormat()) {
176186
throw new UnsupportedMediaTypeHttpException('Unsupported format.');
@@ -180,6 +190,10 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
180190
throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format));
181191
}
182192

193+
if ('array' === $type && null !== $attribute->type) {
194+
$type = $attribute->type.'[]';
195+
}
196+
183197
if ($data = $request->request->all()) {
184198
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
185199
}
@@ -202,4 +216,21 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
202216
throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" property.', $e->property), $e);
203217
}
204218
}
219+
220+
private function getController(Request $request): string
221+
{
222+
if (null === $callable = $request->attributes->get('_controller')) {
223+
return 'the controller';
224+
}
225+
226+
if (\is_string($callable)) {
227+
return sprintf('%s()', $callable);
228+
}
229+
230+
if (\is_array($callable) && \is_string($callable[0])) {
231+
return sprintf('%s::%s()', $callable[0], $callable[1]);
232+
}
233+
234+
return 'the controller';
235+
}
205236
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Serializer\Encoder\XmlEncoder;
2626
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
2727
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
28+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
2829
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2930
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
3031
use Symfony\Component\Serializer\Serializer;
@@ -421,6 +422,74 @@ public function testRequestInputValidationPassed()
421422
$this->assertEquals([$payload], $event->getArguments());
422423
}
423424

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

0 commit comments

Comments
 (0)
0