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

Open in your IDE?
  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.             foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value$nameScope) as $type) {
  94.                 switch ($type->getClassName()) {
  95.                     case 'self':
  96.                     case 'static':
  97.                         $resolvedClass $class;
  98.                         break;
  99.                     case 'parent':
  100.                         if (false !== $resolvedClass $parentClass ?? $parentClass get_parent_class($class)) {
  101.                             break;
  102.                         }
  103.                         // no break
  104.                     default:
  105.                         $types[] = $type;
  106.                         continue 2;
  107.                 }
  108.                 $types[] = new Type(Type::BUILTIN_TYPE_OBJECT$type->isNullable(), $resolvedClass$type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  109.             }
  110.         }
  111.         if (!isset($types[0])) {
  112.             return null;
  113.         }
  114.         if (!\in_array($prefix$this->arrayMutatorPrefixestrue)) {
  115.             return $types;
  116.         }
  117.         return [new Type(Type::BUILTIN_TYPE_ARRAYfalsenulltrue, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  118.     }
  119.     public function getTypesFromConstructor(string $classstring $property): ?array
  120.     {
  121.         if (null === $tagDocNode $this->getDocBlockFromConstructor($class$property)) {
  122.             return null;
  123.         }
  124.         $types = [];
  125.         foreach ($this->phpStanTypeHelper->getTypes($tagDocNode$this->nameScopeFactory->create($class)) as $type) {
  126.             $types[] = $type;
  127.         }
  128.         if (!isset($types[0])) {
  129.             return null;
  130.         }
  131.         return $types;
  132.     }
  133.     private function getDocBlockFromConstructor(string $classstring $property): ?ParamTagValueNode
  134.     {
  135.         try {
  136.             $reflectionClass = new \ReflectionClass($class);
  137.         } catch (\ReflectionException $e) {
  138.             return null;
  139.         }
  140.         if (null === $reflectionConstructor $reflectionClass->getConstructor()) {
  141.             return null;
  142.         }
  143.         if (!$rawDocNode $reflectionConstructor->getDocComment()) {
  144.             return null;
  145.         }
  146.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  147.         $phpDocNode $this->phpDocParser->parse($tokens);
  148.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  149.         return $this->filterDocBlockParams($phpDocNode$property);
  150.     }
  151.     private function filterDocBlockParams(PhpDocNode $docNodestring $allowedParam): ?ParamTagValueNode
  152.     {
  153.         $tags array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
  154.             return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
  155.         }));
  156.         if (!$tags) {
  157.             return null;
  158.         }
  159.         return $tags[0]->value;
  160.     }
  161.     /**
  162.      * @return array{PhpDocNode|null, int|null, string|null, string|null}
  163.      */
  164.     private function getDocBlock(string $classstring $property): array
  165.     {
  166.         $propertyHash $class.'::'.$property;
  167.         if (isset($this->docBlocks[$propertyHash])) {
  168.             return $this->docBlocks[$propertyHash];
  169.         }
  170.         $ucFirstProperty ucfirst($property);
  171.         if ([$docBlock$declaringClass] = $this->getDocBlockFromProperty($class$property)) {
  172.             $data = [$docBlockself::PROPERTYnull$declaringClass];
  173.         } elseif ([$docBlock$_$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR)) {
  174.             $data = [$docBlockself::ACCESSORnull$declaringClass];
  175.         } elseif ([$docBlock$prefix$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR)) {
  176.             $data = [$docBlockself::MUTATOR$prefix$declaringClass];
  177.         } else {
  178.             $data = [nullnullnullnull];
  179.         }
  180.         return $this->docBlocks[$propertyHash] = $data;
  181.     }
  182.     /**
  183.      * @return array{PhpDocNode, string}|null
  184.      */
  185.     private function getDocBlockFromProperty(string $classstring $property): ?array
  186.     {
  187.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  188.         try {
  189.             $reflectionProperty = new \ReflectionProperty($class$property);
  190.         } catch (\ReflectionException $e) {
  191.             return null;
  192.         }
  193.         if (null === $rawDocNode $reflectionProperty->getDocComment() ?: null) {
  194.             return null;
  195.         }
  196.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  197.         $phpDocNode $this->phpDocParser->parse($tokens);
  198.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  199.         return [$phpDocNode$reflectionProperty->class];
  200.     }
  201.     /**
  202.      * @return array{PhpDocNode, string, string}|null
  203.      */
  204.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  205.     {
  206.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  207.         $prefix null;
  208.         foreach ($prefixes as $prefix) {
  209.             $methodName $prefix.$ucFirstProperty;
  210.             try {
  211.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  212.                 if ($reflectionMethod->isStatic()) {
  213.                     continue;
  214.                 }
  215.                 if (
  216.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters())
  217.                     || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  218.                 ) {
  219.                     break;
  220.                 }
  221.             } catch (\ReflectionException $e) {
  222.                 // Try the next prefix if the method doesn't exist
  223.             }
  224.         }
  225.         if (!isset($reflectionMethod)) {
  226.             return null;
  227.         }
  228.         if (null === $rawDocNode $reflectionMethod->getDocComment() ?: null) {
  229.             return null;
  230.         }
  231.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  232.         $phpDocNode $this->phpDocParser->parse($tokens);
  233.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  234.         return [$phpDocNode$prefix$reflectionMethod->class];
  235.     }
  236. }