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.         $rawDocNode $reflectionConstructor->getDocComment();
  151.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  152.         $phpDocNode $this->phpDocParser->parse($tokens);
  153.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  154.         return $this->filterDocBlockParams($phpDocNode$property);
  155.     }
  156.     private function filterDocBlockParams(PhpDocNode $docNodestring $allowedParam): ?ParamTagValueNode
  157.     {
  158.         $tags array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
  159.             return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
  160.         }));
  161.         if (!$tags) {
  162.             return null;
  163.         }
  164.         return $tags[0]->value;
  165.     }
  166.     /**
  167.      * @return array{PhpDocNode|null, int|null, string|null, string|null}
  168.      */
  169.     private function getDocBlock(string $classstring $property): array
  170.     {
  171.         $propertyHash $class.'::'.$property;
  172.         if (isset($this->docBlocks[$propertyHash])) {
  173.             return $this->docBlocks[$propertyHash];
  174.         }
  175.         $ucFirstProperty ucfirst($property);
  176.         if ([$docBlock$source$declaringClass] = $this->getDocBlockFromProperty($class$property)) {
  177.             $data = [$docBlock$sourcenull$declaringClass];
  178.         } elseif ([$docBlock$_$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR)) {
  179.             $data = [$docBlockself::ACCESSORnull$declaringClass];
  180.         } elseif ([$docBlock$prefix$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR)) {
  181.             $data = [$docBlockself::MUTATOR$prefix$declaringClass];
  182.         } else {
  183.             $data = [nullnullnullnull];
  184.         }
  185.         return $this->docBlocks[$propertyHash] = $data;
  186.     }
  187.     /**
  188.      * @return array{PhpDocNode, int, string}|null
  189.      */
  190.     private function getDocBlockFromProperty(string $classstring $property): ?array
  191.     {
  192.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  193.         try {
  194.             $reflectionProperty = new \ReflectionProperty($class$property);
  195.         } catch (\ReflectionException) {
  196.             return null;
  197.         }
  198.         $source self::PROPERTY;
  199.         if ($reflectionProperty->isPromoted()) {
  200.             $constructor = new \ReflectionMethod($class'__construct');
  201.             $rawDocNode $constructor->getDocComment();
  202.             $source self::MUTATOR;
  203.         } else {
  204.             $rawDocNode $reflectionProperty->getDocComment();
  205.         }
  206.         if (!$rawDocNode) {
  207.             return null;
  208.         }
  209.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  210.         $phpDocNode $this->phpDocParser->parse($tokens);
  211.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  212.         return [$phpDocNode$source$reflectionProperty->class];
  213.     }
  214.     /**
  215.      * @return array{PhpDocNode, string, string}|null
  216.      */
  217.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  218.     {
  219.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  220.         $prefix null;
  221.         foreach ($prefixes as $prefix) {
  222.             $methodName $prefix.$ucFirstProperty;
  223.             try {
  224.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  225.                 if ($reflectionMethod->isStatic()) {
  226.                     continue;
  227.                 }
  228.                 if (
  229.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters())
  230.                     || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  231.                 ) {
  232.                     break;
  233.                 }
  234.             } catch (\ReflectionException) {
  235.                 // Try the next prefix if the method doesn't exist
  236.             }
  237.         }
  238.         if (!isset($reflectionMethod)) {
  239.             return null;
  240.         }
  241.         if (null === $rawDocNode $reflectionMethod->getDocComment() ?: null) {
  242.             return null;
  243.         }
  244.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  245.         $phpDocNode $this->phpDocParser->parse($tokens);
  246.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  247.         return [$phpDocNode$prefix$reflectionMethod->class];
  248.     }
  249. }