Skip to content

Commit 7dd49ed

Browse files
committed
[PropertyInfo] Support the list pseudo-type
Signed-off-by: Alexander M. Turek <me@derrabus.de>
1 parent d254e8d commit 7dd49ed

File tree

8 files changed

+60
-16
lines changed

8 files changed

+60
-16
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
3636
public const MUTATOR = 2;
3737

3838
/**
39-
* @var DocBlock[]
39+
* @var array<string, array{DocBlock|null, int|null, string|null}>
4040
*/
4141
private $docBlocks = [];
4242

@@ -238,6 +238,9 @@ private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam):
238238
$docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
239239
}
240240

241+
/**
242+
* @return array{DocBlock|null, int|null, string|null}
243+
*/
241244
private function getDocBlock(string $class, string $property): array
242245
{
243246
$propertyHash = sprintf('%s::%s', $class, $property);
@@ -287,13 +290,14 @@ private function getDocBlockFromProperty(string $class, string $property): ?DocB
287290

288291
try {
289292
return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector));
290-
} catch (\InvalidArgumentException $e) {
291-
return null;
292-
} catch (\RuntimeException $e) {
293+
} catch (\InvalidArgumentException|\RuntimeException $e) {
293294
return null;
294295
}
295296
}
296297

298+
/**
299+
* @return array{DocBlock, string}|null
300+
*/
297301
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
298302
{
299303
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
@@ -333,9 +337,7 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
333337

334338
try {
335339
return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix];
336-
} catch (\InvalidArgumentException $e) {
337-
return null;
338-
} catch (\RuntimeException $e) {
340+
} catch (\InvalidArgumentException|\RuntimeException $e) {
339341
return null;
340342
}
341343
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
*/
3333
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
3434
{
35-
public const PROPERTY = 0;
36-
public const ACCESSOR = 1;
37-
public const MUTATOR = 2;
35+
private const PROPERTY = 0;
36+
private const ACCESSOR = 1;
37+
private const MUTATOR = 2;
3838

3939
/** @var PhpDocParser */
4040
private $phpDocParser;
@@ -45,12 +45,18 @@ final class PhpStanExtractor implements PropertyTypeExtractorInterface, Construc
4545
/** @var NameScopeFactory */
4646
private $nameScopeFactory;
4747

48+
/** @var array<string, array{PhpDocNode|null, int|null, string|null}> */
4849
private $docBlocks = [];
4950
private $phpStanTypeHelper;
5051
private $mutatorPrefixes;
5152
private $accessorPrefixes;
5253
private $arrayMutatorPrefixes;
5354

55+
/**
56+
* @param list<string>|null $mutatorPrefixes
57+
* @param list<string>|null $accessorPrefixes
58+
* @param list<string>|null $arrayMutatorPrefixes
59+
*/
5460
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null)
5561
{
5662
$this->phpStanTypeHelper = new PhpStanTypeHelper();
@@ -65,7 +71,7 @@ public function __construct(array $mutatorPrefixes = null, array $accessorPrefix
6571

6672
public function getTypes(string $class, string $property, array $context = []): ?array
6773
{
68-
/** @var $docNode PhpDocNode */
74+
/** @var PhpDocNode|null $docNode */
6975
[$docNode, $source, $prefix] = $this->getDocBlock($class, $property);
7076
$nameScope = $this->nameScopeFactory->create($class);
7177
if (null === $docNode) {
@@ -177,6 +183,9 @@ private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam)
177183
return $tags[0]->value;
178184
}
179185

186+
/**
187+
* @return array{PhpDocNode|null, int|null, string|null}
188+
*/
180189
private function getDocBlock(string $class, string $property): array
181190
{
182191
$propertyHash = $class.'::'.$property;
@@ -220,6 +229,9 @@ private function getDocBlockFromProperty(string $class, string $property): ?PhpD
220229
return $phpDocNode;
221230
}
222231

232+
/**
233+
* @return array{PhpDocNode, string}|null
234+
*/
223235
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
224236
{
225237
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ public function typesProvider()
141141
null,
142142
null,
143143
],
144+
[
145+
'listOfStrings',
146+
$this->isPhpDocumentorV5() ? [
147+
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)),
148+
] : null,
149+
null,
150+
null,
151+
],
144152
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null],
145153
];
146154
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public function typesProvider()
114114
['emptyVar', null],
115115
['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))]],
116116
['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)]],
117+
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
117118
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]],
118119
];
119120
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function testGetProperties()
6868
'arrayWithKeys',
6969
'arrayWithKeysAndComplexValue',
7070
'arrayOfMixed',
71+
'listOfStrings',
7172
'parentAnnotation',
7273
'foo',
7374
'foo2',
@@ -127,6 +128,7 @@ public function testGetPropertiesWithCustomPrefixes()
127128
'arrayWithKeys',
128129
'arrayWithKeysAndComplexValue',
129130
'arrayOfMixed',
131+
'listOfStrings',
130132
'parentAnnotation',
131133
'foo',
132134
'foo2',
@@ -175,6 +177,7 @@ public function testGetPropertiesWithNoPrefixes()
175177
'arrayWithKeys',
176178
'arrayWithKeysAndComplexValue',
177179
'arrayOfMixed',
180+
'listOfStrings',
178181
'parentAnnotation',
179182
'foo',
180183
'foo2',

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ class Dummy extends ParentDummy
145145
*/
146146
public $arrayOfMixed;
147147

148+
/**
149+
* @var list<string>
150+
*/
151+
public $listOfStrings;
152+
148153
/**
149154
* @var parent
150155
*/

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\PropertyInfo\Util;
1313

14+
use phpDocumentor\Reflection\PseudoTypes\List_;
1415
use phpDocumentor\Reflection\Type as DocType;
1516
use phpDocumentor\Reflection\Types\Array_;
1617
use phpDocumentor\Reflection\Types\Collection;
@@ -19,6 +20,10 @@
1920
use phpDocumentor\Reflection\Types\Nullable;
2021
use Symfony\Component\PropertyInfo\Type;
2122

23+
// Workaround for phpdocumentor/type-resolver < 1.6
24+
// We trigger the autoloader here, son we don't need to trigger it inside the loop later.
25+
class_exists(List_::class);
26+
2227
/**
2328
* Transforms a php doc type to a {@link Type} instance.
2429
*
@@ -91,7 +96,13 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
9196
$docType = $docType ?? (string) $type;
9297

9398
if ($type instanceof Collection) {
94-
[$phpType, $class] = $this->getPhpTypeAndClass((string) $type->getFqsen());
99+
$fqsen = $type->getFqsen();
100+
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
101+
// Workaround for phpdocumentor/type-resolver < 1.6
102+
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType()));
103+
}
104+
105+
[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
95106

96107
$key = $this->getTypes($type->getKeyType());
97108
$value = $this->getTypes($type->getValueType());
@@ -116,7 +127,7 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
116127
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType);
117128
}
118129

119-
if (str_starts_with($docType, 'array<') && $type instanceof Array_) {
130+
if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) {
120131
// array<value> is converted to x[] which is handled above
121132
// so it's only necessary to handle array<key, value> here
122133
$collectionKeyType = $this->getTypes($type->getKeyType())[0];

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
110110
return $this->compressNullableType($types);
111111
}
112112
if ($node instanceof GenericTypeNode) {
113-
$mainTypes = $this->extractTypes($node->type, $nameScope);
113+
[$mainType] = $this->extractTypes($node->type, $nameScope);
114114

115-
$collectionKeyTypes = [];
115+
$collectionKeyTypes = $mainType->getCollectionKeyTypes();
116116
$collectionKeyValues = [];
117117
if (1 === \count($node->genericTypes)) {
118118
foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $subType) {
@@ -127,7 +127,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
127127
}
128128
}
129129

130-
return [new Type($mainTypes[0]->getBuiltinType(), $mainTypes[0]->isNullable(), $mainTypes[0]->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)];
130+
return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)];
131131
}
132132
if ($node instanceof ArrayShapeNode) {
133133
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)];
@@ -159,6 +159,8 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
159159
switch ($node->name) {
160160
case 'integer':
161161
return [new Type(Type::BUILTIN_TYPE_INT)];
162+
case 'list':
163+
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))];
162164
case 'mixed':
163165
return []; // mixed seems to be ignored in all other extractors
164166
case 'parent':

0 commit comments

Comments
 (0)