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

Skip to content

Commit 00ec79a

Browse files
committed
map a list of items with MapRequestPayload attribute
1 parent b5ee977 commit 00ec79a

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-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: 33 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('Controller "%s" requires the "$%s" argument that could not be resolved. Please set the $type argument of the #[MapRequestPayload] attribute to the type of the objects in the expected array.', $this->getPrettyName($request->attributes->get('_controller')), $argument->getName()));
86+
}
87+
} elseif ($attribute->type) {
88+
throw new NearMissValueResolverException(sprintf('Controller "%s" requires the "$%s" argument that could not be resolved. Please set its type to "array" when using argument $type of #[MapRequestPayload].', $this->getPrettyName($request->attributes->get('_controller')), $argument->getName()));
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
}
@@ -202,4 +217,21 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
202217
throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" property.', $e->property), $e);
203218
}
204219
}
220+
221+
private function getPrettyName(mixed $controller): string
222+
{
223+
if (\is_array($controller)) {
224+
if (\is_object($controller[0])) {
225+
$controller[0] = get_debug_type($controller[0]);
226+
}
227+
228+
return $controller[0].'::'.$controller[1];
229+
}
230+
231+
if (\is_object($controller)) {
232+
return get_debug_type($controller);
233+
}
234+
235+
return $controller;
236+
}
205237
}

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('Controller "App\Controller\SomeController::someMethod" requires the "$prices" argument that could not be resolved. Please set the $type argument of the #[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('Controller "App\Controller\SomeController::someMethod" requires the "$prices" argument that could not be resolved. Please set its type to "array" when using argument $type of #[MapRequestPayload].');
491+
$resolver->resolve($request, $argument);
492+
}
493+
424494
public function testItThrowsOnVariadicArgument()
425495
{
426496
$serializer = new Serializer();

0 commit comments

Comments
 (0)
0