Skip to content

Commit a170c98

Browse files
committed
[Serializer][PropertyAccessor] Allow to ignore generics
1 parent b85a083 commit a170c98

15 files changed

+145
-30
lines changed

UPGRADE-6.4.md

+5
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ MonologBridge
172172

173173
* [BC break] Add native return type to `Logger::clear()` and to `DebugProcessor::clear()`
174174

175+
PropertyInfo
176+
------------
177+
178+
* [BC Break] `PhpDocExtractor` and `PhpStanExtractor` no longer support the `ignore_docblock_generics` context value
179+
175180
PsrHttpMessageBridge
176181
--------------------
177182

src/Symfony/Component/PropertyInfo/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Make properties writable when a setter in camelCase exists, similar to the camelCase getter
8+
* Allow `PhpDocExtractor` and `PhpStanExtractor` to ignore non-collection generics using `ignore_docblock_generics` context value
89

910
6.1
1011
---

src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ interface ConstructorArgumentTypeExtractorInterface
2929
*
3030
* @internal
3131
*/
32-
public function getTypesFromConstructor(string $class, string $property): ?array;
32+
public function getTypesFromConstructor(string $class, string $property, array $context = []): ?array;
3333
}

src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131
public function getTypes(string $class, string $property, array $context = []): ?array
3232
{
3333
foreach ($this->extractors as $extractor) {
34-
$value = $extractor->getTypesFromConstructor($class, $property);
34+
$value = $extractor->getTypesFromConstructor($class, $property, $context);
3535
if (null !== $value) {
3636
return $value;
3737
}

src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php

+11-3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public function getLongDescription(string $class, string $property, array $conte
113113

114114
public function getTypes(string $class, string $property, array $context = []): ?array
115115
{
116+
if (true === $ignoreDocblockGenerics = ($context['ignore_docblock_generics'] ?? false)) {
117+
trigger_deprecation('symfony/property-info', '6.4', 'Ignoring docblock generics by setting "ignore_docblock_generics" context value to "true" is deprecated without replacement.');
118+
}
119+
116120
/** @var $docBlock DocBlock */
117121
[$docBlock, $source, $prefix] = $this->getDocBlock($class, $property);
118122
if (!$docBlock) {
@@ -130,7 +134,7 @@ public function getTypes(string $class, string $property, array $context = []):
130134
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
131135
foreach ($docBlock->getTagsByName($tag) as $tag) {
132136
if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
133-
foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) {
137+
foreach ($this->phpDocTypeHelper->getTypes($tag->getType(), $ignoreDocblockGenerics) as $type) {
134138
switch ($type->getClassName()) {
135139
case 'self':
136140
case 'static':
@@ -164,8 +168,12 @@ public function getTypes(string $class, string $property, array $context = []):
164168
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
165169
}
166170

167-
public function getTypesFromConstructor(string $class, string $property): ?array
171+
public function getTypesFromConstructor(string $class, string $property, array $context = []): ?array
168172
{
173+
if (true === $ignoreDocblockGenerics = ($context['ignore_docblock_generics'] ?? false)) {
174+
trigger_deprecation('symfony/property-info', '6.4', 'Ignoring docblock generics by setting "ignore_docblock_generics" context value to "true" is deprecated without replacement.');
175+
}
176+
169177
$docBlock = $this->getDocBlockFromConstructor($class, $property);
170178

171179
if (!$docBlock) {
@@ -176,7 +184,7 @@ public function getTypesFromConstructor(string $class, string $property): ?array
176184
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
177185
foreach ($docBlock->getTagsByName('param') as $tag) {
178186
if ($tag && null !== $tag->getType()) {
179-
$types[] = $this->phpDocTypeHelper->getTypes($tag->getType());
187+
$types[] = $this->phpDocTypeHelper->getTypes($tag->getType(), $ignoreDocblockGenerics);
180188
}
181189
}
182190

src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php

+11-3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public function __construct(array $mutatorPrefixes = null, array $accessorPrefix
7575

7676
public function getTypes(string $class, string $property, array $context = []): ?array
7777
{
78+
if (true === $ignoreDocblockGenerics = ($context['ignore_docblock_generics'] ?? false)) {
79+
trigger_deprecation('symfony/property-info', '6.4', 'Ignoring docblock generics by setting "ignore_docblock_generics" context value to "true" is deprecated without replacement.');
80+
}
81+
7882
/** @var PhpDocNode|null $docNode */
7983
[$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property);
8084
$nameScope = $this->nameScopeFactory->create($class, $declaringClass);
@@ -111,7 +115,7 @@ public function getTypes(string $class, string $property, array $context = []):
111115
continue;
112116
}
113117

114-
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope) as $type) {
118+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope, $ignoreDocblockGenerics) as $type) {
115119
switch ($type->getClassName()) {
116120
case 'self':
117121
case 'static':
@@ -144,14 +148,18 @@ public function getTypes(string $class, string $property, array $context = []):
144148
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
145149
}
146150

147-
public function getTypesFromConstructor(string $class, string $property): ?array
151+
public function getTypesFromConstructor(string $class, string $property, array $context = []): ?array
148152
{
153+
if (true === $ignoreDocblockGenerics = ($context['ignore_docblock_generics'] ?? false)) {
154+
trigger_deprecation('symfony/property-info', '6.4', 'Ignoring docblock generics by setting "ignore_docblock_generics" context value to "true" is deprecated without replacement.');
155+
}
156+
149157
if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) {
150158
return null;
151159
}
152160

153161
$types = [];
154-
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class)) as $type) {
162+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class), $ignoreDocblockGenerics) as $type) {
155163
$types[] = $type;
156164
}
157165

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public function getTypes(string $class, string $property, array $context = []):
156156
return null;
157157
}
158158

159-
public function getTypesFromConstructor(string $class, string $property): ?array
159+
public function getTypesFromConstructor(string $class, string $property, array $context = []): ?array
160160
{
161161
try {
162162
$reflection = new \ReflectionClass($class);

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,15 @@ public static function promotedPropertyProvider(): array
447447
['promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]],
448448
];
449449
}
450+
451+
/**
452+
* @group legacy
453+
*/
454+
public function testIgnoreGenerics()
455+
{
456+
$this->assertCount(1, $this->extractor->getTypes(PhpDocGenericsDocblock::class, 'foo'));
457+
$this->assertNull($this->extractor->getTypes(PhpDocGenericsDocblock::class, 'foo', ['ignore_docblock_generics' => true]));
458+
}
450459
}
451460

452461
class EmptyDocBlock
@@ -465,3 +474,9 @@ public function setOmittedType(array $omittedTagType)
465474
{
466475
}
467476
}
477+
478+
class PhpDocGenericsDocblock
479+
{
480+
/** @var \stcClass<int> */
481+
public $foo;
482+
}

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ public static function php80TypesProvider()
473473
[Php80PromotedDummy::class, 'promoted', null],
474474
];
475475
}
476+
477+
/**
478+
* @group legacy
479+
*/
480+
public function testIgnoreGenerics()
481+
{
482+
$this->assertCount(1, $this->extractor->getTypes(PhpDocGenericsDocblock::class, 'foo'));
483+
$this->assertNull($this->extractor->getTypes(PhpDocGenericsDocblock::class, 'foo', ['ignore_docblock_generics' => true]));
484+
}
476485
}
477486

478487
class PhpStanOmittedParamTagTypeDocBlock
@@ -486,3 +495,9 @@ public function setOmittedType(array $omittedTagType)
486495
{
487496
}
488497
}
498+
499+
class PhpStanGenericsDocblock
500+
{
501+
/** @var \stcClass<int> */
502+
public $foo;
503+
}

src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function getTypes($class, $property, array $context = []): ?array
3939
return [new Type(Type::BUILTIN_TYPE_INT)];
4040
}
4141

42-
public function getTypesFromConstructor(string $class, string $property): ?array
42+
public function getTypesFromConstructor(string $class, string $property, array $context = []): ?array
4343
{
4444
return [new Type(Type::BUILTIN_TYPE_STRING)];
4545
}

src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php

+13-5
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ final class PhpDocTypeHelper
3939
/**
4040
* Creates a {@see Type} from a PHPDoc type.
4141
*
42+
* @param bool $ignoreGenerics
43+
*
4244
* @return Type[]
4345
*/
44-
public function getTypes(DocType $varType): array
46+
public function getTypes(DocType $varType /*, bool $ignoreGenerics */): array
4547
{
48+
$ignoreGenerics = func_get_args()[1] ?? false;
49+
4650
if ($varType instanceof ConstExpression) {
4751
// It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment
4852
return [];
@@ -61,7 +65,7 @@ public function getTypes(DocType $varType): array
6165
$nullable = true;
6266
}
6367

64-
$type = $this->createType($varType, $nullable);
68+
$type = $this->createType($varType, $nullable, $ignoreGenerics);
6569
if (null !== $type) {
6670
$types[] = $type;
6771
}
@@ -93,7 +97,7 @@ public function getTypes(DocType $varType): array
9397
}
9498

9599
foreach ($varTypes as $varType) {
96-
$type = $this->createType($varType, $nullable);
100+
$type = $this->createType($varType, $nullable, $ignoreGenerics);
97101
if (null !== $type) {
98102
$types[] = $type;
99103
}
@@ -105,11 +109,15 @@ public function getTypes(DocType $varType): array
105109
/**
106110
* Creates a {@see Type} from a PHPDoc type.
107111
*/
108-
private function createType(DocType $type, bool $nullable, string $docType = null): ?Type
112+
private function createType(DocType $type, bool $nullable, bool $ignoreGenerics): ?Type
109113
{
110-
$docType ??= (string) $type;
114+
$docType = (string) $type;
111115

112116
if ($type instanceof Collection) {
117+
if ($ignoreGenerics) {
118+
return null;
119+
}
120+
113121
$fqsen = $type->getFqsen();
114122
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
115123
// Workaround for phpdocumentor/type-resolver < 1.6

src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php

+18-10
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ final class PhpStanTypeHelper
4141
/**
4242
* Creates a {@see Type} from a PhpDocTagValueNode type.
4343
*
44+
* @param bool $ignoreGenerics
45+
*
4446
* @return Type[]
4547
*/
46-
public function getTypes(PhpDocTagValueNode $node, NameScope $nameScope): array
48+
public function getTypes(PhpDocTagValueNode $node, NameScope $nameScope /*, bool $ignoreGenerics */): array
4749
{
50+
$ignoreGenerics = func_get_args()[2] ?? false;
51+
4852
if ($node instanceof ParamTagValueNode || $node instanceof ReturnTagValueNode || $node instanceof VarTagValueNode) {
49-
return $this->compressNullableType($this->extractTypes($node->type, $nameScope));
53+
return $this->compressNullableType($this->extractTypes($node->type, $nameScope, $ignoreGenerics));
5054
}
5155

5256
return [];
@@ -98,7 +102,7 @@ private function compressNullableType(array $types): array
98102
/**
99103
* @return Type[]
100104
*/
101-
private function extractTypes(TypeNode $node, NameScope $nameScope): array
105+
private function extractTypes(TypeNode $node, NameScope $nameScope, bool $ignoreGenerics): array
102106
{
103107
if ($node instanceof UnionTypeNode) {
104108
$types = [];
@@ -107,7 +111,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
107111
// It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment
108112
return [];
109113
}
110-
foreach ($this->extractTypes($type, $nameScope) as $subType) {
114+
foreach ($this->extractTypes($type, $nameScope, $ignoreGenerics) as $subType) {
111115
$types[] = $subType;
112116
}
113117
}
@@ -119,23 +123,27 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
119123
return [new Type(Type::BUILTIN_TYPE_STRING)];
120124
}
121125

122-
[$mainType] = $this->extractTypes($node->type, $nameScope);
126+
[$mainType] = $this->extractTypes($node->type, $nameScope, $ignoreGenerics);
123127

124128
if (Type::BUILTIN_TYPE_INT === $mainType->getBuiltinType()) {
125129
return [$mainType];
126130
}
127131

132+
if ($ignoreGenerics && !\in_array($mainType->getClassName(), [\Traversable::class, \Iterator::class, \IteratorAggregate::class, null], true)) {
133+
return [];
134+
}
135+
128136
$collectionKeyTypes = $mainType->getCollectionKeyTypes();
129137
$collectionKeyValues = [];
130138
if (1 === \count($node->genericTypes)) {
131-
foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $subType) {
139+
foreach ($this->extractTypes($node->genericTypes[0], $nameScope, $ignoreGenerics) as $subType) {
132140
$collectionKeyValues[] = $subType;
133141
}
134142
} elseif (2 === \count($node->genericTypes)) {
135-
foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $keySubType) {
143+
foreach ($this->extractTypes($node->genericTypes[0], $nameScope, $ignoreGenerics) as $keySubType) {
136144
$collectionKeyTypes[] = $keySubType;
137145
}
138-
foreach ($this->extractTypes($node->genericTypes[1], $nameScope) as $valueSubType) {
146+
foreach ($this->extractTypes($node->genericTypes[1], $nameScope, $ignoreGenerics) as $valueSubType) {
139147
$collectionKeyValues[] = $valueSubType;
140148
}
141149
}
@@ -146,13 +154,13 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
146154
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)];
147155
}
148156
if ($node instanceof ArrayTypeNode) {
149-
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], $this->extractTypes($node->type, $nameScope))];
157+
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], $this->extractTypes($node->type, $nameScope, $ignoreGenerics))];
150158
}
151159
if ($node instanceof CallableTypeNode || $node instanceof CallableTypeParameterNode) {
152160
return [new Type(Type::BUILTIN_TYPE_CALLABLE)];
153161
}
154162
if ($node instanceof NullableTypeNode) {
155-
$subTypes = $this->extractTypes($node->type, $nameScope);
163+
$subTypes = $this->extractTypes($node->type, $nameScope, $ignoreGenerics);
156164
if (\count($subTypes) > 1) {
157165
$subTypes[] = new Type(Type::BUILTIN_TYPE_NULL);
158166

src/Symfony/Component/PropertyInfo/composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
],
2525
"require": {
2626
"php": ">=8.1",
27+
"symfony/deprecation-contracts": "^2.5|^3",
2728
"symfony/string": "^5.4|^6.0|^7.0"
2829
},
2930
"require-dev": {

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
370370
}
371371
}
372372

373-
$types = $this->getTypes($resolvedClass, $attribute);
373+
$types = $this->getTypes($resolvedClass, $attribute, $context);
374374

375375
if (null !== $types) {
376376
try {
@@ -602,7 +602,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
602602
*/
603603
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, string $format = null): mixed
604604
{
605-
if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types = $this->getTypes($class->getName(), $parameterName)) {
605+
if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types = $this->getTypes($class->getName(), $parameterName, $context)) {
606606
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
607607
}
608608

@@ -614,7 +614,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
614614
/**
615615
* @return Type[]|null
616616
*/
617-
private function getTypes(string $currentClass, string $attribute): ?array
617+
private function getTypes(string $currentClass, string $attribute, array $context): ?array
618618
{
619619
if (null === $this->propertyTypeExtractor) {
620620
return null;
@@ -625,7 +625,7 @@ private function getTypes(string $currentClass, string $attribute): ?array
625625
return false === $this->typesCache[$key] ? null : $this->typesCache[$key];
626626
}
627627

628-
if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
628+
if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute, $context)) {
629629
return $this->typesCache[$key] = $types;
630630
}
631631

@@ -637,7 +637,7 @@ private function getTypes(string $currentClass, string $attribute): ?array
637637
}
638638

639639
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
640-
if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) {
640+
if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute, $context)) {
641641
return $this->typesCache[$key] = $types;
642642
}
643643
}

0 commit comments

Comments
 (0)