diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index 61910527a64bf..0f2fba5ad4432 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -36,7 +36,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property public const MUTATOR = 2; /** - * @var DocBlock[] + * @var array */ private $docBlocks = []; @@ -238,6 +238,9 @@ private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd()); } + /** + * @return array{DocBlock|null, int|null, string|null} + */ private function getDocBlock(string $class, string $property): array { $propertyHash = sprintf('%s::%s', $class, $property); @@ -287,13 +290,14 @@ private function getDocBlockFromProperty(string $class, string $property): ?DocB try { return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector)); - } catch (\InvalidArgumentException $e) { - return null; - } catch (\RuntimeException $e) { + } catch (\InvalidArgumentException|\RuntimeException $e) { return null; } } + /** + * @return array{DocBlock, string}|null + */ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array { $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes; @@ -333,9 +337,7 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i try { return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix]; - } catch (\InvalidArgumentException $e) { - return null; - } catch (\RuntimeException $e) { + } catch (\InvalidArgumentException|\RuntimeException $e) { return null; } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 014a846315462..4a6a296784d6d 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -32,9 +32,9 @@ */ final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface { - public const PROPERTY = 0; - public const ACCESSOR = 1; - public const MUTATOR = 2; + private const PROPERTY = 0; + private const ACCESSOR = 1; + private const MUTATOR = 2; /** @var PhpDocParser */ private $phpDocParser; @@ -45,12 +45,18 @@ final class PhpStanExtractor implements PropertyTypeExtractorInterface, Construc /** @var NameScopeFactory */ private $nameScopeFactory; + /** @var array */ private $docBlocks = []; private $phpStanTypeHelper; private $mutatorPrefixes; private $accessorPrefixes; private $arrayMutatorPrefixes; + /** + * @param list|null $mutatorPrefixes + * @param list|null $accessorPrefixes + * @param list|null $arrayMutatorPrefixes + */ public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null) { $this->phpStanTypeHelper = new PhpStanTypeHelper(); @@ -65,7 +71,7 @@ public function __construct(array $mutatorPrefixes = null, array $accessorPrefix public function getTypes(string $class, string $property, array $context = []): ?array { - /** @var $docNode PhpDocNode */ + /** @var PhpDocNode|null $docNode */ [$docNode, $source, $prefix] = $this->getDocBlock($class, $property); $nameScope = $this->nameScopeFactory->create($class); if (null === $docNode) { @@ -177,6 +183,9 @@ private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam) return $tags[0]->value; } + /** + * @return array{PhpDocNode|null, int|null, string|null} + */ private function getDocBlock(string $class, string $property): array { $propertyHash = $class.'::'.$property; @@ -220,6 +229,9 @@ private function getDocBlockFromProperty(string $class, string $property): ?PhpD return $phpDocNode; } + /** + * @return array{PhpDocNode, string}|null + */ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array { $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 014bf2c9e8feb..2db0d791595d3 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -141,6 +141,14 @@ public function typesProvider() null, null, ], + [ + 'listOfStrings', + $this->isPhpDocumentorV5() ? [ + new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), + ] : null, + null, + null, + ], ['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null], ]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index c5c24254a7fbe..8b52433a54fe2 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -114,6 +114,7 @@ public function typesProvider() ['emptyVar', null], ['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))]], ['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)]], + ['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], ['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]], ]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 8ad3315834a8d..18c4dd588d7f8 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -68,6 +68,7 @@ public function testGetProperties() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'listOfStrings', 'parentAnnotation', 'foo', 'foo2', @@ -127,6 +128,7 @@ public function testGetPropertiesWithCustomPrefixes() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'listOfStrings', 'parentAnnotation', 'foo', 'foo2', @@ -175,6 +177,7 @@ public function testGetPropertiesWithNoPrefixes() 'arrayWithKeys', 'arrayWithKeysAndComplexValue', 'arrayOfMixed', + 'listOfStrings', 'parentAnnotation', 'foo', 'foo2', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index c27088798725d..00dea793e7169 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -145,6 +145,11 @@ class Dummy extends ParentDummy */ public $arrayOfMixed; + /** + * @var list + */ + public $listOfStrings; + /** * @var parent */ diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index e1ed3dd10a4e1..d1752d6c1b2fb 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyInfo\Util; +use phpDocumentor\Reflection\PseudoTypes\List_; use phpDocumentor\Reflection\Type as DocType; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\Collection; @@ -19,6 +20,10 @@ use phpDocumentor\Reflection\Types\Nullable; use Symfony\Component\PropertyInfo\Type; +// Workaround for phpdocumentor/type-resolver < 1.6 +// We trigger the autoloader here, so we don't need to trigger it inside the loop later. +class_exists(List_::class); + /** * Transforms a php doc type to a {@link Type} instance. * @@ -91,7 +96,13 @@ private function createType(DocType $type, bool $nullable, string $docType = nul $docType = $docType ?? (string) $type; if ($type instanceof Collection) { - [$phpType, $class] = $this->getPhpTypeAndClass((string) $type->getFqsen()); + $fqsen = $type->getFqsen(); + if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) { + // Workaround for phpdocumentor/type-resolver < 1.6 + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType())); + } + + [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); $key = $this->getTypes($type->getKeyType()); $value = $this->getTypes($type->getValueType()); @@ -116,7 +127,7 @@ private function createType(DocType $type, bool $nullable, string $docType = nul return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); } - if (str_starts_with($docType, 'array<') && $type instanceof Array_) { + if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) { // array is converted to x[] which is handled above // so it's only necessary to handle array here $collectionKeyType = $this->getTypes($type->getKeyType())[0]; diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 297fd542b7329..b5ed7bb5732ee 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -110,9 +110,9 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array return $this->compressNullableType($types); } if ($node instanceof GenericTypeNode) { - $mainTypes = $this->extractTypes($node->type, $nameScope); + [$mainType] = $this->extractTypes($node->type, $nameScope); - $collectionKeyTypes = []; + $collectionKeyTypes = $mainType->getCollectionKeyTypes(); $collectionKeyValues = []; if (1 === \count($node->genericTypes)) { foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $subType) { @@ -127,7 +127,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array } } - return [new Type($mainTypes[0]->getBuiltinType(), $mainTypes[0]->isNullable(), $mainTypes[0]->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)]; + return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)]; } if ($node instanceof ArrayShapeNode) { return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]; @@ -159,6 +159,8 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array switch ($node->name) { case 'integer': return [new Type(Type::BUILTIN_TYPE_INT)]; + case 'list': + return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))]; case 'mixed': return []; // mixed seems to be ignored in all other extractors case 'parent':