Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[HttpKernel] Map a list of items with MapRequestPayload attribute #54385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ class MapRequestPayload extends ValueResolver
* @param string|GroupSequence|array<string>|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,
public readonly array $serializationContext = [],
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);
}
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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.');
Expand All @@ -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] : []));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down