Skip to content

[HttpKernel] Use TypeInfo for #[MapRequestPayload] type resolution #58036

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

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions UPGRADE-7.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ FrameworkBundle

* [BC BREAK] The `secrets:decrypt-to-local` command terminates with a non-zero exit code when a secret could not be read

HttpKernel
----------

* Deprecate the `$type` parameter of `#[MapRequestPayload]`, use the TypeInfo component for automatic type detection instead

Messenger
---------

Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
->tag('monolog.logger', ['channel' => 'request'])

->set('argument_metadata_factory', ArgumentMetadataFactory::class)
->args([
service('type_info.resolver')->nullOnInvalid(),
])

->set('argument_resolver', ArgumentResolver::class)
->args([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ public function __construct(
public readonly string|GroupSequence|array|null $validationGroups = null,
string $resolver = RequestPayloadValueResolver::class,
public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY,
/** @deprecated since Symfony 7.2 */
public readonly ?string $type = null,
) {
if ($type) {
trigger_deprecation('symfony/http-kernel', '7.2', 'The "type" parameter of the #[MapRequestPayload] attribute is deprecated and will be removed in Symfony 8.0. Try running "composer require symfony/type-info phpstan/phpdoc-parser" to get automatic type detection instead.');
}

parent::__construct($resolver);
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Remove `@internal` flag and add `@final` to `ServicesResetter`
* Deprecate the `$type` parameter of `#[MapRequestPayload]`, use the TypeInfo component for automatic type detection instead

7.1
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
throw new \LogicException(\sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
}

// @deprecated since Symfony 7.2, remove the if statement in 8.0
if ($attribute instanceof MapRequestPayload) {
if ('array' === $argument->getType()) {
if (!$attribute->type) {
Expand Down Expand Up @@ -202,6 +203,8 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format));
}

// @deprecated since Symfony 7.2. In 8.0, replace the whole if/else block by:
// $type = $argument->getType();
if ('array' === $argument->getType() && null !== $attribute->type) {
$type = $attribute->type.'[]';
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@

namespace Symfony\Component\HttpKernel\ControllerMetadata;

use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;

/**
* Builds {@see ArgumentMetadata} objects based on the given Controller.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface
{
public function __construct(private readonly ?TypeResolverInterface $typeResolver = null)
{
}

public function createArgumentMetadata(string|object|array $controller, ?\ReflectionFunctionAbstract $reflector = null): array
{
$arguments = [];
Expand Down Expand Up @@ -46,6 +53,17 @@ private function getType(\ReflectionParameter $parameter): ?string
if (!$type = $parameter->getType()) {
return null;
}

if ($this->typeResolver) {
$type = $this->typeResolver->resolve($parameter);

if ($type instanceof CollectionType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to "unwrap" a potential nullable type here I think

return (string) $type->getCollectionValueType().'[]';
}

return $type->getBaseType()->getTypeIdentifier()->value;
}

$name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;

return match (strtolower($name)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver;

use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
Expand Down Expand Up @@ -40,6 +41,8 @@

class RequestPayloadValueResolverTest extends TestCase
{
use ExpectDeprecationTrait;

public function testNotTypedArgument()
{
$resolver = new RequestPayloadValueResolver(
Expand Down Expand Up @@ -423,7 +426,10 @@ public function testRequestInputValidationPassed()
$this->assertEquals([$payload], $event->getArguments());
}

public function testRequestArrayDenormalization()
/**
* @group legacy
*/
public function testRequestArrayDenormalizationWithLegacyType()
{
$input = [
['price' => '50'],
Expand All @@ -443,6 +449,8 @@ public function testRequestArrayDenormalization()

$resolver = new RequestPayloadValueResolver($serializer, $validator);

$this->expectDeprecation('Since symfony/http-kernel 7.2: The "type" parameter of the #[MapRequestPayload] attribute is deprecated and will be removed in Symfony 8.0. Try running "composer require symfony/type-info phpstan/phpdoc-parser" to get automatic type detection instead.');

$argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [
MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class),
]);
Expand All @@ -457,6 +465,43 @@ public function testRequestArrayDenormalization()
$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', RequestPayload::class.'[]', false, false, null, false, [
MapRequestPayload::class => new MapRequestPayload(),
]);
$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());
}

/**
* @group legacy
*/
public function testItThrowsOnMissingAttributeType()
{
$serializer = new Serializer();
Expand All @@ -474,6 +519,9 @@ public function testItThrowsOnMissingAttributeType()
$resolver->resolve($request, $argument);
}

/**
* @group legacy
*/
public function testItThrowsOnInvalidAttributeTypeUsage()
{
$serializer = new Serializer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController;
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;

class ArgumentMetadataFactoryTest extends TestCase
{
Expand Down Expand Up @@ -151,6 +152,24 @@ public function testIssue41478()
], $arguments);
}

public function testListOfObjectsWithTypeInfo()
{
$arguments = (new ArgumentMetadataFactory(TypeResolver::create()))->createArgumentMetadata([$this, 'listOfObjects']);
$this->assertEquals([
new ArgumentMetadata('products', DummyProduct::class.'[]', false, false, null, false, [], controllerName: $this::class.'::listOfObjects'),
new ArgumentMetadata('bar', null, false, true, null, controllerName: $this::class.'::listOfObjects'),
], $arguments);
}

public function testListOfObjectsWithoutTypeInfo()
{
$arguments = $this->factory->createArgumentMetadata([$this, 'listOfObjects']);
$this->assertEquals([
new ArgumentMetadata('products', 'array', false, false, null, false, [], controllerName: $this::class.'::listOfObjects'),
new ArgumentMetadata('bar', null, false, true, null, controllerName: $this::class.'::listOfObjects'),
], $arguments);
}

public function signature1(self $foo, array $bar, callable $baz)
{
}
Expand All @@ -170,4 +189,16 @@ public function signature4($foo = 'default', $bar = 500, $baz = [])
public function signature5(?array $foo = null, $bar = null)
{
}

/**
* @param DummyProduct[] $products
*/
public function listOfObjects(array $products, $bar = null)
{
}
}

class DummyProduct
{
public $price;
}
3 changes: 3 additions & 0 deletions src/Symfony/Component/HttpKernel/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"psr/log": "^1|^2|^3"
},
"require-dev": {
"phpstan/phpdoc-parser": "^1.0",
"symfony/browser-kit": "^6.4|^7.0",
"symfony/clock": "^6.4|^7.0",
"symfony/config": "^6.4|^7.0",
Expand All @@ -42,6 +43,7 @@
"symfony/stopwatch": "^6.4|^7.0",
"symfony/translation": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3",
"symfony/type-info": "^7.2",
"symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
Expand All @@ -67,6 +69,7 @@
"symfony/translation": "<6.4",
"symfony/translation-contracts": "<2.5",
"symfony/twig-bridge": "<6.4",
"symfony/type-info": "<7.2",
"symfony/validator": "<6.4",
"symfony/var-dumper": "<6.4",
"twig/twig": "<3.0.4"
Expand Down
Loading