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

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 PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
  12. use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
  13. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
  14. use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
  15. use PHPStan\PhpDocParser\Lexer\Lexer;
  16. use PHPStan\PhpDocParser\Parser\ConstExprParser;
  17. use PHPStan\PhpDocParser\Parser\PhpDocParser;
  18. use PHPStan\PhpDocParser\Parser\TokenIterator;
  19. use PHPStan\PhpDocParser\Parser\TypeParser;
  20. use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
  21. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  22. use Symfony\Component\PropertyInfo\Type;
  23. use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
  24. /**
  25.  * Extracts data using PHPStan parser.
  26.  *
  27.  * @author Baptiste Leduc <baptiste.leduc@gmail.com>
  28.  */
  29. final class PhpStanExtractor implements PropertyTypeExtractorInterfaceConstructorArgumentTypeExtractorInterface
  30. {
  31.     private const PROPERTY 0;
  32.     private const ACCESSOR 1;
  33.     private const MUTATOR 2;
  34.     /** @var PhpDocParser */
  35.     private $phpDocParser;
  36.     /** @var Lexer */
  37.     private $lexer;
  38.     /** @var NameScopeFactory */
  39.     private $nameScopeFactory;
  40.     /** @var array<string, array{PhpDocNode|null, int|null, string|null, string|null}> */
  41.     private $docBlocks = [];
  42.     private $phpStanTypeHelper;
  43.     private $mutatorPrefixes;
  44.     private $accessorPrefixes;
  45.     private $arrayMutatorPrefixes;
  46.     /**
  47.      * @param list<string>|null $mutatorPrefixes
  48.      * @param list<string>|null $accessorPrefixes
  49.      * @param list<string>|null $arrayMutatorPrefixes
  50.      */
  51.     public function __construct(array $mutatorPrefixes null, array $accessorPrefixes null, array $arrayMutatorPrefixes null)
  52.     {
  53.         $this->phpStanTypeHelper = new PhpStanTypeHelper();
  54.         $this->mutatorPrefixes $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
  55.         $this->accessorPrefixes $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
  56.         $this->arrayMutatorPrefixes $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
  57.         $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
  58.         $this->lexer = new Lexer();
  59.         $this->nameScopeFactory = new NameScopeFactory();
  60.     }
  61.     public function getTypes(string $classstring $property, array $context = []): ?array
  62.     {
  63.         /** @var PhpDocNode|null $docNode */
  64.         [$docNode$source$prefix$declaringClass] = $this->getDocBlock($class$property);
  65.         $nameScope $this->nameScopeFactory->create($class$declaringClass);
  66.         if (null === $docNode) {
  67.             return null;
  68.         }
  69.         switch ($source) {
  70.             case self::PROPERTY:
  71.                 $tag '@var';
  72.                 break;
  73.             case self::ACCESSOR:
  74.                 $tag '@return';
  75.                 break;
  76.             case self::MUTATOR:
  77.                 $tag '@param';
  78.                 break;
  79.         }
  80.         $parentClass null;
  81.         $types = [];
  82.         foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
  83.             if ($tagDocNode->value instanceof InvalidTagValueNode) {
  84.                 continue;
  85.             }
  86.             if (
  87.                 $tagDocNode->value instanceof ParamTagValueNode
  88.                 && null === $prefix
  89.                 && $tagDocNode->value->parameterName !== '$'.$property
  90.             ) {
  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 ??= 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) {
  138.             return null;
  139.         }
  140.         if (null === $reflectionConstructor $reflectionClass->getConstructor()) {
  141.             return null;
  142.         }
  143.         $rawDocNode $reflectionConstructor->getDocComment();
  144.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  145.         $phpDocNode $this->phpDocParser->parse($tokens);
  146.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  147.         return $this->filterDocBlockParams($phpDocNode$property);
  148.     }
  149.     private function filterDocBlockParams(PhpDocNode $docNodestring $allowedParam): ?ParamTagValueNode
  150.     {
  151.         $tags array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
  152.             return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
  153.         }));
  154.         if (!$tags) {
  155.             return null;
  156.         }
  157.         return $tags[0]->value;
  158.     }
  159.     /**
  160.      * @return array{PhpDocNode|null, int|null, string|null, string|null}
  161.      */
  162.     private function getDocBlock(string $classstring $property): array
  163.     {
  164.         $propertyHash $class.'::'.$property;
  165.         if (isset($this->docBlocks[$propertyHash])) {
  166.             return $this->docBlocks[$propertyHash];
  167.         }
  168.         $ucFirstProperty ucfirst($property);
  169.         if ([$docBlock$source$declaringClass] = $this->getDocBlockFromProperty($class$property)) {
  170.             $data = [$docBlock$sourcenull$declaringClass];
  171.         } elseif ([$docBlock$_$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR)) {
  172.             $data = [$docBlockself::ACCESSORnull$declaringClass];
  173.         } elseif ([$docBlock$prefix$declaringClass] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR)) {
  174.             $data = [$docBlockself::MUTATOR$prefix$declaringClass];
  175.         } else {
  176.             $data = [nullnullnullnull];
  177.         }
  178.         return $this->docBlocks[$propertyHash] = $data;
  179.     }
  180.     /**
  181.      * @return array{PhpDocNode, int, string}|null
  182.      */
  183.     private function getDocBlockFromProperty(string $classstring $property): ?array
  184.     {
  185.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  186.         try {
  187.             $reflectionProperty = new \ReflectionProperty($class$property);
  188.         } catch (\ReflectionException) {
  189.             return null;
  190.         }
  191.         $source self::PROPERTY;
  192.         if ($reflectionProperty->isPromoted()) {
  193.             $constructor = new \ReflectionMethod($class'__construct');
  194.             $rawDocNode $constructor->getDocComment();
  195.             $source self::MUTATOR;
  196.         } else {
  197.             $rawDocNode $reflectionProperty->getDocComment();
  198.         }
  199.         if (!$rawDocNode) {
  200.             return null;
  201.         }
  202.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  203.         $phpDocNode $this->phpDocParser->parse($tokens);
  204.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  205.         return [$phpDocNode$source$reflectionProperty->class];
  206.     }
  207.     /**
  208.      * @return array{PhpDocNode, string, string}|null
  209.      */
  210.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  211.     {
  212.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  213.         $prefix null;
  214.         foreach ($prefixes as $prefix) {
  215.             $methodName $prefix.$ucFirstProperty;
  216.             try {
  217.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  218.                 if ($reflectionMethod->isStatic()) {
  219.                     continue;
  220.                 }
  221.                 if (
  222.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters())
  223.                     || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  224.                 ) {
  225.                     break;
  226.                 }
  227.             } catch (\ReflectionException) {
  228.                 // Try the next prefix if the method doesn't exist
  229.             }
  230.         }
  231.         if (!isset($reflectionMethod)) {
  232.             return null;
  233.         }
  234.         if (null === $rawDocNode $reflectionMethod->getDocComment() ?: null) {
  235.             return null;
  236.         }
  237.         $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
  238.         $phpDocNode $this->phpDocParser->parse($tokens);
  239.         $tokens->consumeTokenType(Lexer::TOKEN_END);
  240.         return [$phpDocNode$prefix$reflectionMethod->class];
  241.     }
  242. }