vendor/symfony/property-info/Extractor/PhpStanExtractor.php line 78

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\PropertyInfo\Extractor;
  11. use phpDocumentor\Reflection\Types\ContextFactory;
  12. use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
  13. use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
  14. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
  15. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
  16. use PHPStan\PhpDocParser\Lexer\Lexer;
  17. use PHPStan\PhpDocParser\Parser\ConstExprParser;
  18. use PHPStan\PhpDocParser\Parser\PhpDocParser;
  19. use PHPStan\PhpDocParser\Parser\TokenIterator;
  20. use PHPStan\PhpDocParser\Parser\TypeParser;
  21. use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
  22. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  23. use Symfony\Component\PropertyInfo\Type;
  24. use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
  25. /**
  26.  * Extracts data using PHPStan parser.
  27.  *
  28.  * @author Baptiste Leduc <baptiste.leduc@gmail.com>
  29.  */
  30. final class PhpStanExtractor implements PropertyTypeExtractorInterfaceConstructorArgumentTypeExtractorInterface
  31. {
  32.     private const PROPERTY 0;
  33.     private const ACCESSOR 1;
  34.     private const MUTATOR 2;
  35.     /** @var PhpDocParser */
  36.     private $phpDocParser;
  37.     /** @var Lexer */
  38.     private $lexer;
  39.     /** @var NameScopeFactory */
  40.     private $nameScopeFactory;
  41.     /** @var array<string, array{PhpDocNode|null, int|null, string|null, string|null}> */
  42.     private $docBlocks = [];
  43.     private $phpStanTypeHelper;
  44.     private $mutatorPrefixes;
  45.     private $accessorPrefixes;
  46.     private $arrayMutatorPrefixes;
  47.     /**
  48.      * @param list<string>|null $mutatorPrefixes
  49.      * @param list<string>|null $accessorPrefixes
  50.      * @param list<string>|null $arrayMutatorPrefixes
  51.      */
  52.     public function __construct(array $mutatorPrefixes null, array $accessorPrefixes null, array $arrayMutatorPrefixes null)
  53.     {
  54.         if (!class_exists(ContextFactory::class)) {
  55.             throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".'__CLASS__));
  56.         }
  57.         if (!class_exists(PhpDocParser::class)) {
  58.             throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".'__CLASS__));
  59.         }
  60.         $this->phpStanTypeHelper = new PhpStanTypeHelper();
  61.         $this->mutatorPrefixes $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
  62.         $this->accessorPrefixes $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
  63.         $this->arrayMutatorPrefixes $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
  64.         $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
  65.         $this->lexer = new Lexer();
  66.         $this->nameScopeFactory = new NameScopeFactory();
  67.     }
  68.     public function getTypes(string $classstring $property, array $context = []): ?array
  69.     {
  70.         /** @var PhpDocNode|null $docNode */
  71.         [$docNode$source$prefix$declaringClass] = $this->getDocBlock($class$property);
  72.         $nameScope $this->nameScopeFactory->create($class$declaringClass);
  73.         if (null === $docNode) {
  74.             return null;
  75.         }
  76.         switch ($source) {
  77.             case self::PROPERTY:
  78.                 $tag '@var';
  79.                 break;
  80.             case self::ACCESSOR:
  81.                 $tag '@return';
  82.                 break;
  83.             case self::MUTATOR:
  84.                 $tag '@param';
  85.                 break;
  86.         }
  87.         $parentClass null;
  88.         $types = [];
  89.         foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
  90.             if ($tagDocNode->value instanceof InvalidTagValueNode) {
  91.                 continue;
  92.             }
  93.             if (
  94.                 $tagDocNode->value instanceof ParamTagValueNode
  95.                 && null === $prefix
  96.                 && $tagDocNode->value->parameterName !== '$'.$property
  97.             ) {
  98.                 continue;
  99.             }
  100.             foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value$nameScope) as $type) {
  101.                 switch ($type->getClassName()) {
  102.                     case 'self':
  103.                     case 'static':
  104.                         $resolvedClass $class;
  105.                         break;
  106.                     case 'parent':
  107.                         if (false !== $resolvedClass $parentClass ??= get_parent_class($class)) {
  108.                             break;
  109.                         }
  110.                         // no break
  111.                     default:
  112.                         $types[] = $type;
  113.                         continue 2;
  114.                 }
  115.                 $types[] = new Type(Type::BUILTIN_TYPE_OBJECT$type->isNullable(), $resolvedClass$type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  116.             }
  117.         }
  118.         if (!isset($types[0])) {
  119.             return null;
  120.         }
  121.         if (!\in_array($prefix$this->arrayMutatorPrefixestrue)) {
  122.             return $types;
  123.         }
  124.         return [new Type(Type::BUILTIN_TYPE_ARRAYfalsenulltrue, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  125.     }
  126.     public function getTypesFromConstructor(string $classstring $property): ?array
  127.     {
  128.         if (null === $tagDocNode $this->getDocBlockFromConstructor($class$property)) {
  129.             return null;
  130.         }
  131.         $types = [];
  132.         foreach ($this->phpStanTypeHelper->getTypes($tagDocNode$this->nameScopeFactory->create($class)) as $type) {
  133.             $types[] = $type;
  134.         }
  135.         if (!isset($types[0])) {
  136.             return null;
  137.         }
  138.         return $types;
  139.     }
  140.     private function getDocBlockFromConstructor(string $classstring $property): ?ParamTagValueNode
  141.     {
  142.         try {
  143.             $reflectionClass = new \ReflectionClass($class);
  144.         } catch (\ReflectionException) {
  145.             return null;
  146.         }
  147.         if (null === $reflectionConstructor $reflectionClass->getConstructor()) {
  148.             return null;
  149.         }
  150.         if (!$rawDocNode $reflectionConstructor->getDocComment()) {
  151.             return null;
  152.         }
  153.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  154.         $phpDocNode $this->phpDocParser->parse($tokens);
  155.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  156.         return $this->filterDocBlockParams($phpDocNode$property);
  157.     }
  158.     private function filterDocBlockParams(PhpDocNode $docNodestring $allowedParam): ?ParamTagValueNode
  159.     {
  160.         $tags array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
  161.             return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
  162.         }));
  163.         if (!$tags) {
  164.             return null;
  165.         }
  166.         return $tags[0]->value;
  167.     }
  168.     /**
  169.      * @return array{PhpDocNode|null, int|null, string|null, string|null}
  170.      */
  171.     private function getDocBlock(string $classstring $property): array
  172.     {
  173.         $propertyHash $class.'::'.$property;
  174.         if (isset($this->docBlocks[$propertyHash])) {
  175.             return $this->docBlocks[$propertyHash];
  176.         }
  177.         $ucFirstProperty ucfirst($property);
  178.         if ([$docBlock$source$declaringClass] = $this->getDocBlockFromProperty($class$property)) {
  179.             $data = [$docBlock$sourcenull$declaringClass];
  180.         } elseif ([$docBlock$_$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR)) {
  181.             $data = [$docBlockself::ACCESSORnull$declaringClass];
  182.         } elseif ([$docBlock$prefix$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR)) {
  183.             $data = [$docBlockself::MUTATOR$prefix$declaringClass];
  184.         } else {
  185.             $data = [nullnullnullnull];
  186.         }
  187.         return $this->docBlocks[$propertyHash] = $data;
  188.     }
  189.     /**
  190.      * @return array{PhpDocNode, int, string}|null
  191.      */
  192.     private function getDocBlockFromProperty(string $classstring $property): ?array
  193.     {
  194.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  195.         try {
  196.             $reflectionProperty = new \ReflectionProperty($class$property);
  197.         } catch (\ReflectionException) {
  198.             return null;
  199.         }
  200.         $source self::PROPERTY;
  201.         if ($reflectionProperty->isPromoted()) {
  202.             $constructor = new \ReflectionMethod($class'__construct');
  203.             $rawDocNode $constructor->getDocComment();
  204.             $source self::MUTATOR;
  205.         } else {
  206.             $rawDocNode $reflectionProperty->getDocComment();
  207.         }
  208.         if (!$rawDocNode) {
  209.             return null;
  210.         }
  211.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  212.         $phpDocNode $this->phpDocParser->parse($tokens);
  213.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  214.         return [$phpDocNode$source$reflectionProperty->class];
  215.     }
  216.     /**
  217.      * @return array{PhpDocNode, string, string}|null
  218.      */
  219.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  220.     {
  221.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  222.         $prefix null;
  223.         foreach ($prefixes as $prefix) {
  224.             $methodName $prefix.$ucFirstProperty;
  225.             try {
  226.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  227.                 if ($reflectionMethod->isStatic()) {
  228.                     continue;
  229.                 }
  230.                 if (
  231.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters())
  232.                     || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  233.                 ) {
  234.                     break;
  235.                 }
  236.             } catch (\ReflectionException) {
  237.                 // Try the next prefix if the method doesn't exist
  238.             }
  239.         }
  240.         if (!isset($reflectionMethod)) {
  241.             return null;
  242.         }
  243.         if (null === $rawDocNode $reflectionMethod->getDocComment() ?: null) {
  244.             return null;
  245.         }
  246.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  247.         $phpDocNode $this->phpDocParser->parse($tokens);
  248.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  249.         return [$phpDocNode$prefix$reflectionMethod->class];
  250.     }
  251. }