Skip to content

Commit 0643d9a

Browse files
committed
[HttpKernel] Use TypeInfo for #[MapRequestPayload] type resolution
1 parent 99b25a1 commit 0643d9a

File tree

9 files changed

+118
-1
lines changed

9 files changed

+118
-1
lines changed

UPGRADE-7.2.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ FrameworkBundle
2929

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

32+
HttpKernel
33+
----------
34+
35+
* Deprecate the `$type` parameter of `#[MapRequestPayload]`, use the TypeInfo component for automatic type detection instead
36+
3237
Messenger
3338
---------
3439

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
->tag('monolog.logger', ['channel' => 'request'])
4747

4848
->set('argument_metadata_factory', ArgumentMetadataFactory::class)
49+
->args([
50+
service('type_info.resolver')->nullOnInvalid(),
51+
])
4952

5053
->set('argument_resolver', ArgumentResolver::class)
5154
->args([

src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ public function __construct(
4040
public readonly string|GroupSequence|array|null $validationGroups = null,
4141
string $resolver = RequestPayloadValueResolver::class,
4242
public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY,
43+
/** @deprecated since Symfony 7.2 */
4344
public readonly ?string $type = null,
4445
) {
46+
if ($type) {
47+
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.');
48+
}
49+
4550
parent::__construct($resolver);
4651
}
4752
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

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

910
7.1
1011
---

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
8383
throw new \LogicException(\sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
8484
}
8585

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

206+
// @deprecated since Symfony 7.2. In 8.0, replace the whole if/else block by:
207+
// $type = $argument->getType();
205208
if ('array' === $argument->getType() && null !== $attribute->type) {
206209
$type = $attribute->type.'[]';
207210
} else {

src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@
1111

1212
namespace Symfony\Component\HttpKernel\ControllerMetadata;
1313

14+
use Symfony\Component\TypeInfo\Type\CollectionType;
15+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
16+
1417
/**
1518
* Builds {@see ArgumentMetadata} objects based on the given Controller.
1619
*
1720
* @author Iltar van der Berg <kjarli@gmail.com>
1821
*/
1922
final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface
2023
{
24+
public function __construct(private readonly ?TypeResolverInterface $typeResolver = null)
25+
{
26+
}
27+
2128
public function createArgumentMetadata(string|object|array $controller, ?\ReflectionFunctionAbstract $reflector = null): array
2229
{
2330
$arguments = [];
@@ -46,6 +53,17 @@ private function getType(\ReflectionParameter $parameter): ?string
4653
if (!$type = $parameter->getType()) {
4754
return null;
4855
}
56+
57+
if ($this->typeResolver) {
58+
$type = $this->typeResolver->resolve($parameter);
59+
60+
if ($type instanceof CollectionType) {
61+
return (string) $type->getCollectionValueType().'[]';
62+
}
63+
64+
return $type->getBaseType()->getTypeIdentifier()->value;
65+
}
66+
4967
$name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;
5068

5169
return match (strtolower($name)) {

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1516
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
1718
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
@@ -40,6 +41,8 @@
4041

4142
class RequestPayloadValueResolverTest extends TestCase
4243
{
44+
use ExpectDeprecationTrait;
45+
4346
public function testNotTypedArgument()
4447
{
4548
$resolver = new RequestPayloadValueResolver(
@@ -423,7 +426,10 @@ public function testRequestInputValidationPassed()
423426
$this->assertEquals([$payload], $event->getArguments());
424427
}
425428

426-
public function testRequestArrayDenormalization()
429+
/**
430+
* @group legacy
431+
*/
432+
public function testRequestArrayDenormalizationWithLegacyType()
427433
{
428434
$input = [
429435
['price' => '50'],
@@ -443,6 +449,8 @@ public function testRequestArrayDenormalization()
443449

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

452+
$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.');
453+
446454
$argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [
447455
MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class),
448456
]);
@@ -457,6 +465,43 @@ public function testRequestArrayDenormalization()
457465
$this->assertEquals([$payload], $event->getArguments());
458466
}
459467

468+
public function testRequestArrayDenormalization()
469+
{
470+
$input = [
471+
['price' => '50'],
472+
['price' => '23'],
473+
];
474+
$payload = [
475+
new RequestPayload(50),
476+
new RequestPayload(23),
477+
];
478+
479+
$serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]);
480+
481+
$validator = $this->createMock(ValidatorInterface::class);
482+
$validator->expects($this->once())
483+
->method('validate')
484+
->willReturn(new ConstraintViolationList());
485+
486+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
487+
488+
$argument = new ArgumentMetadata('prices', RequestPayload::class.'[]', false, false, null, false, [
489+
MapRequestPayload::class => new MapRequestPayload(),
490+
]);
491+
$request = Request::create('/', 'POST', $input);
492+
493+
$kernel = $this->createMock(HttpKernelInterface::class);
494+
$arguments = $resolver->resolve($request, $argument);
495+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
496+
497+
$resolver->onKernelControllerArguments($event);
498+
499+
$this->assertEquals([$payload], $event->getArguments());
500+
}
501+
502+
/**
503+
* @group legacy
504+
*/
460505
public function testItThrowsOnMissingAttributeType()
461506
{
462507
$serializer = new Serializer();
@@ -474,6 +519,9 @@ public function testItThrowsOnMissingAttributeType()
474519
$resolver->resolve($request, $argument);
475520
}
476521

522+
/**
523+
* @group legacy
524+
*/
477525
public function testItThrowsOnInvalidAttributeTypeUsage()
478526
{
479527
$serializer = new Serializer();

src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
2121
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
2222
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController;
23+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
2324

2425
class ArgumentMetadataFactoryTest extends TestCase
2526
{
@@ -151,6 +152,24 @@ public function testIssue41478()
151152
], $arguments);
152153
}
153154

155+
public function testListOfObjectsWithTypeInfo()
156+
{
157+
$arguments = (new ArgumentMetadataFactory(TypeResolver::create()))->createArgumentMetadata([$this, 'listOfObjects']);
158+
$this->assertEquals([
159+
new ArgumentMetadata('products', DummyProduct::class.'[]', false, false, null, false, [], controllerName: $this::class.'::listOfObjects'),
160+
new ArgumentMetadata('bar', null, false, true, null, controllerName: $this::class.'::listOfObjects'),
161+
], $arguments);
162+
}
163+
164+
public function testListOfObjectsWithoutTypeInfo()
165+
{
166+
$arguments = $this->factory->createArgumentMetadata([$this, 'listOfObjects']);
167+
$this->assertEquals([
168+
new ArgumentMetadata('products', 'array', false, false, null, false, [], controllerName: $this::class.'::listOfObjects'),
169+
new ArgumentMetadata('bar', null, false, true, null, controllerName: $this::class.'::listOfObjects'),
170+
], $arguments);
171+
}
172+
154173
public function signature1(self $foo, array $bar, callable $baz)
155174
{
156175
}
@@ -170,4 +189,16 @@ public function signature4($foo = 'default', $bar = 500, $baz = [])
170189
public function signature5(?array $foo = null, $bar = null)
171190
{
172191
}
192+
193+
/**
194+
* @param DummyProduct[] $products
195+
*/
196+
public function listOfObjects(array $products, $bar = null)
197+
{
198+
}
199+
}
200+
201+
class DummyProduct
202+
{
203+
public $price;
173204
}

src/Symfony/Component/HttpKernel/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"psr/log": "^1|^2|^3"
2626
},
2727
"require-dev": {
28+
"phpstan/phpdoc-parser": "^1.0",
2829
"symfony/browser-kit": "^6.4|^7.0",
2930
"symfony/clock": "^6.4|^7.0",
3031
"symfony/config": "^6.4|^7.0",
@@ -42,6 +43,7 @@
4243
"symfony/stopwatch": "^6.4|^7.0",
4344
"symfony/translation": "^6.4|^7.0",
4445
"symfony/translation-contracts": "^2.5|^3",
46+
"symfony/type-info": "^7.2",
4547
"symfony/uid": "^6.4|^7.0",
4648
"symfony/validator": "^6.4|^7.0",
4749
"symfony/var-dumper": "^6.4|^7.0",
@@ -67,6 +69,7 @@
6769
"symfony/translation": "<6.4",
6870
"symfony/translation-contracts": "<2.5",
6971
"symfony/twig-bridge": "<6.4",
72+
"symfony/type-info": "<7.2",
7073
"symfony/validator": "<6.4",
7174
"symfony/var-dumper": "<6.4",
7275
"twig/twig": "<3.0.4"

0 commit comments

Comments
 (0)