Skip to content

[HttpKernel] Fix handling of MapRequest* attributes #50125

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
May 2, 2023
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 @@ -381,7 +381,8 @@ public function load(array $configs, ContainerBuilder $container)
);

$container->getDefinition('argument_resolver.request_payload')
->replaceArgument(0, new Reference('.argument_resolver.request_payload.no_serializer', ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE));
->replaceArgument(0, new Reference('.argument_resolver.request_payload.no_serializer', ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))
->clearTag('kernel.event_subscriber');

$container->removeDefinition('console.command.serializer_debug');
}
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
service('translator')->nullOnInvalid(),
])
->tag('controller.targeted_value_resolver', ['name' => RequestPayloadValueResolver::class])
->tag('kernel.event_subscriber')
->lazy()

->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class])
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/HttpKernel/Attribute/MapQueryString.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\Attribute;

use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Validator\Constraints\GroupSequence;

/**
Expand All @@ -22,6 +23,8 @@
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MapQueryString extends ValueResolver
{
public ArgumentMetadata $metadata;

public function __construct(
public readonly array $serializationContext = [],
public readonly string|GroupSequence|array|null $validationGroups = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\Attribute;

use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Validator\Constraints\GroupSequence;

/**
Expand All @@ -22,6 +23,8 @@
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MapRequestPayload extends ValueResolver
{
public ArgumentMetadata $metadata;

public function __construct(
public readonly array|string|null $acceptFormat = null,
public readonly array $serializationContext = [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@

namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
use Symfony\Component\Serializer\Exception\UnsupportedFormatException;
Expand All @@ -31,8 +34,10 @@

/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*
* @final
*/
final class RequestPayloadValueResolver implements ValueResolverInterface
class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface
{
/**
* @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT
Expand All @@ -59,24 +64,43 @@ public function __construct(

public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
$payloadMappers = [
MapQueryString::class => ['mapQueryString', Response::HTTP_NOT_FOUND],
MapRequestPayload::class => ['mapRequestPayload', Response::HTTP_UNPROCESSABLE_ENTITY],
];
$attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0]
?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0]
?? null;

if (!$attribute) {
return [];
}

$attribute->metadata = $argument;

return [$attribute];
}

foreach ($payloadMappers as $mappingAttribute => [$payloadMapper, $validationFailedCode]) {
if (!$attributes = $argument->getAttributesOfType($mappingAttribute, ArgumentMetadata::IS_INSTANCEOF)) {
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
$arguments = $event->getArguments();

foreach ($arguments as $i => $argument) {
if ($argument instanceof MapQueryString) {
$payloadMapper = 'mapQueryString';
$validationFailedCode = Response::HTTP_NOT_FOUND;
} elseif ($argument instanceof MapRequestPayload) {
$payloadMapper = 'mapRequestPayload';
$validationFailedCode = Response::HTTP_UNPROCESSABLE_ENTITY;
} else {
continue;
}
$request = $event->getRequest();

if (!$type = $argument->getType()) {
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->getName()));
if (!$type = $argument->metadata->getType()) {
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName()));
}

if ($this->validator) {
$violations = new ConstraintViolationList();
try {
$payload = $this->$payloadMapper($request, $type, $attributes[0]);
$payload = $this->$payloadMapper($request, $type, $argument);
} catch (PartialDenormalizationException $e) {
$trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p);
foreach ($e->getErrors() as $error) {
Expand All @@ -92,26 +116,35 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
}

if (null !== $payload) {
$violations->addAll($this->validator->validate($payload, null, $attributes[0]->validationGroups ?? null));
$violations->addAll($this->validator->validate($payload, null, $argument->validationGroups ?? null));
}

if (\count($violations)) {
throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations));
}
} else {
try {
$payload = $this->$payloadMapper($request, $type, $attributes[0]);
$payload = $this->$payloadMapper($request, $type, $argument);
} catch (PartialDenormalizationException $e) {
throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e);
}
}

if (null !== $payload || $argument->isNullable()) {
return [$payload];
if (null === $payload && !$argument->metadata->isNullable()) {
throw new HttpException($validationFailedCode);
}

$arguments[$i] = $payload;
}

return [];
$event->setArguments($arguments);
}

public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments',
];
}

private function mapQueryString(Request $request, string $type, MapQueryString $attribute): ?object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
Expand All @@ -45,10 +47,14 @@ public function testNotTypedArgument()
]);
$request = Request::create('/', 'POST', server: ['HTTP_CONTENT_TYPE' => 'application/json']);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Could not resolve the "$notTyped" controller argument: argument should be typed.');

$resolver->resolve($request, $argument);
$resolver->onKernelControllerArguments($event);
}

public function testWithoutValidatorAndCouldNotDenormalize()
Expand All @@ -63,8 +69,12 @@ public function testWithoutValidatorAndCouldNotDenormalize()
]);
$request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

try {
$resolver->resolve($request, $argument);
$resolver->onKernelControllerArguments($event);
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
$this->assertInstanceOf(PartialDenormalizationException::class, $e->getPrevious());
Expand All @@ -90,8 +100,12 @@ public function testValidationNotPassed()
]);
$request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

try {
$resolver->resolve($request, $argument);
$resolver->onKernelControllerArguments($event);
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
$validationFailedException = $e->getPrevious();
Expand All @@ -112,9 +126,12 @@ public function testUnsupportedMedia()
]);
$request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'foo/bar'], content: 'foo-bar');

try {
$resolver->resolve($request, $argument);
$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

try {
$resolver->onKernelControllerArguments($event);
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
$this->assertSame(415, $e->getStatusCode());
Expand All @@ -139,7 +156,13 @@ public function testRequestContentValidationPassed()
]);
$request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content);

$this->assertEquals($payload, $resolver->resolve($request, $argument)[0]);
$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 testQueryStringValidationPassed()
Expand All @@ -161,7 +184,13 @@ public function testQueryStringValidationPassed()
]);
$request = Request::create('/', 'GET', $query);

$this->assertEquals($payload, $resolver->resolve($request, $argument)[0]);
$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 testRequestInputValidationPassed()
Expand All @@ -183,7 +212,13 @@ public function testRequestInputValidationPassed()
]);
$request = Request::create('/', 'POST', $input);

$this->assertEquals($payload, $resolver->resolve($request, $argument)[0]);
$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());
}

/**
Expand All @@ -202,10 +237,13 @@ public function testAcceptFormatPassed(mixed $acceptFormat, string $contentType,
MapRequestPayload::class => new MapRequestPayload(acceptFormat: $acceptFormat),
]);

$resolved = $resolver->resolve($request, $argument);
$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$resolver->onKernelControllerArguments($event);

$this->assertCount(1, $resolved);
$this->assertEquals(new RequestPayload(50), $resolved[0]);
$this->assertEquals([new RequestPayload(50)], $event->getArguments());
}

public static function provideMatchedFormatContext(): iterable
Expand Down Expand Up @@ -262,9 +300,12 @@ public function testAcceptFormatNotPassed(mixed $acceptFormat, string $contentTy
MapRequestPayload::class => new MapRequestPayload(acceptFormat: $acceptFormat),
]);

try {
$resolver->resolve($request, $argument);
$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

try {
$resolver->onKernelControllerArguments($event);
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
$this->assertSame(415, $e->getStatusCode());
Expand Down Expand Up @@ -330,10 +371,13 @@ public function testValidationGroupsPassed(string $method, ValueResolver $attrib
$attribute::class => $attribute,
]);

$resolved = $resolver->resolve($request, $argument);
$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$this->assertCount(1, $resolved);
$this->assertEquals($payload, $resolved[0]);
$resolver->onKernelControllerArguments($event);

$this->assertEquals([$payload], $event->getArguments());
}

/**
Expand All @@ -352,8 +396,12 @@ public function testValidationGroupsNotPassed(string $method, ValueResolver $att
]);
$request = Request::create('/', $method, $input);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

try {
$resolver->resolve($request, $argument);
$resolver->onKernelControllerArguments($event);
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
$validationFailedException = $e->getPrevious();
Expand Down