vendor/symfony/property-info/Extractor/PhpDocExtractor.php line 68

  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\DocBlock;
  12. use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
  13. use phpDocumentor\Reflection\DocBlockFactory;
  14. use phpDocumentor\Reflection\DocBlockFactoryInterface;
  15. use phpDocumentor\Reflection\Types\Context;
  16. use phpDocumentor\Reflection\Types\ContextFactory;
  17. use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
  18. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  19. use Symfony\Component\PropertyInfo\Type;
  20. use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
  21. /**
  22.  * Extracts data using a PHPDoc parser.
  23.  *
  24.  * @author Kévin Dunglas <dunglas@gmail.com>
  25.  *
  26.  * @final
  27.  */
  28. class PhpDocExtractor implements PropertyDescriptionExtractorInterfacePropertyTypeExtractorInterfaceConstructorArgumentTypeExtractorInterface
  29. {
  30.     public const PROPERTY 0;
  31.     public const ACCESSOR 1;
  32.     public const MUTATOR 2;
  33.     /**
  34.      * @var array<string, array{DocBlock|null, int|null, string|null}>
  35.      */
  36.     private $docBlocks = [];
  37.     /**
  38.      * @var Context[]
  39.      */
  40.     private $contexts = [];
  41.     private $docBlockFactory;
  42.     private $contextFactory;
  43.     private $phpDocTypeHelper;
  44.     private $mutatorPrefixes;
  45.     private $accessorPrefixes;
  46.     private $arrayMutatorPrefixes;
  47.     /**
  48.      * @param string[]|null $mutatorPrefixes
  49.      * @param string[]|null $accessorPrefixes
  50.      * @param string[]|null $arrayMutatorPrefixes
  51.      */
  52.     public function __construct(DocBlockFactoryInterface $docBlockFactory null, array $mutatorPrefixes null, array $accessorPrefixes null, array $arrayMutatorPrefixes null)
  53.     {
  54.         if (!class_exists(DocBlockFactory::class)) {
  55.             throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed. Try running composer require "phpdocumentor/reflection-docblock".'__CLASS__));
  56.         }
  57.         $this->docBlockFactory $docBlockFactory ?: DocBlockFactory::createInstance();
  58.         $this->contextFactory = new ContextFactory();
  59.         $this->phpDocTypeHelper = new PhpDocTypeHelper();
  60.         $this->mutatorPrefixes $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
  61.         $this->accessorPrefixes $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
  62.         $this->arrayMutatorPrefixes $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
  63.     }
  64.     public function getShortDescription(string $classstring $property, array $context = []): ?string
  65.     {
  66.         /** @var $docBlock DocBlock */
  67.         [$docBlock] = $this->getDocBlock($class$property);
  68.         if (!$docBlock) {
  69.             return null;
  70.         }
  71.         $shortDescription $docBlock->getSummary();
  72.         if (!empty($shortDescription)) {
  73.             return $shortDescription;
  74.         }
  75.         foreach ($docBlock->getTagsByName('var') as $var) {
  76.             if ($var && !$var instanceof InvalidTag) {
  77.                 $varDescription $var->getDescription()->render();
  78.                 if (!empty($varDescription)) {
  79.                     return $varDescription;
  80.                 }
  81.             }
  82.         }
  83.         return null;
  84.     }
  85.     public function getLongDescription(string $classstring $property, array $context = []): ?string
  86.     {
  87.         /** @var $docBlock DocBlock */
  88.         [$docBlock] = $this->getDocBlock($class$property);
  89.         if (!$docBlock) {
  90.             return null;
  91.         }
  92.         $contents $docBlock->getDescription()->render();
  93.         return '' === $contents null $contents;
  94.     }
  95.     public function getTypes(string $classstring $property, array $context = []): ?array
  96.     {
  97.         /** @var $docBlock DocBlock */
  98.         [$docBlock$source$prefix] = $this->getDocBlock($class$property);
  99.         if (!$docBlock) {
  100.             return null;
  101.         }
  102.         $tag = match ($source) {
  103.             self::PROPERTY => 'var',
  104.             self::ACCESSOR => 'return',
  105.             self::MUTATOR => 'param',
  106.         };
  107.         $parentClass null;
  108.         $types = [];
  109.         /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  110.         foreach ($docBlock->getTagsByName($tag) as $tag) {
  111.             if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
  112.                 foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) {
  113.                     switch ($type->getClassName()) {
  114.                         case 'self':
  115.                         case 'static':
  116.                             $resolvedClass $class;
  117.                             break;
  118.                         case 'parent':
  119.                             if (false !== $resolvedClass $parentClass ??= get_parent_class($class)) {
  120.                                 break;
  121.                             }
  122.                             // no break
  123.                         default:
  124.                             $types[] = $type;
  125.                             continue 2;
  126.                     }
  127.                     $types[] = new Type(Type::BUILTIN_TYPE_OBJECT$type->isNullable(), $resolvedClass$type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  128.                 }
  129.             }
  130.         }
  131.         if (!isset($types[0])) {
  132.             return null;
  133.         }
  134.         if (!\in_array($prefix$this->arrayMutatorPrefixes)) {
  135.             return $types;
  136.         }
  137.         return [new Type(Type::BUILTIN_TYPE_ARRAYfalsenulltrue, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  138.     }
  139.     public function getTypesFromConstructor(string $classstring $property): ?array
  140.     {
  141.         $docBlock $this->getDocBlockFromConstructor($class$property);
  142.         if (!$docBlock) {
  143.             return null;
  144.         }
  145.         $types = [];
  146.         /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  147.         foreach ($docBlock->getTagsByName('param') as $tag) {
  148.             if ($tag && null !== $tag->getType()) {
  149.                 $types[] = $this->phpDocTypeHelper->getTypes($tag->getType());
  150.             }
  151.         }
  152.         if (!isset($types[0]) || [] === $types[0]) {
  153.             return null;
  154.         }
  155.         return array_merge([], ...$types);
  156.     }
  157.     private function getDocBlockFromConstructor(string $classstring $property): ?DocBlock
  158.     {
  159.         try {
  160.             $reflectionClass = new \ReflectionClass($class);
  161.         } catch (\ReflectionException) {
  162.             return null;
  163.         }
  164.         $reflectionConstructor $reflectionClass->getConstructor();
  165.         if (!$reflectionConstructor) {
  166.             return null;
  167.         }
  168.         try {
  169.             $docBlock $this->docBlockFactory->create($reflectionConstructor$this->contextFactory->createFromReflector($reflectionConstructor));
  170.             return $this->filterDocBlockParams($docBlock$property);
  171.         } catch (\InvalidArgumentException) {
  172.             return null;
  173.         }
  174.     }
  175.     private function filterDocBlockParams(DocBlock $docBlockstring $allowedParam): DocBlock
  176.     {
  177.         $tags array_values(array_filter($docBlock->getTagsByName('param'), function ($tag) use ($allowedParam) {
  178.             return $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName();
  179.         }));
  180.         return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags$docBlock->getContext(),
  181.             $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
  182.     }
  183.     /**
  184.      * @return array{DocBlock|null, int|null, string|null}
  185.      */
  186.     private function getDocBlock(string $classstring $property): array
  187.     {
  188.         $propertyHash sprintf('%s::%s'$class$property);
  189.         if (isset($this->docBlocks[$propertyHash])) {
  190.             return $this->docBlocks[$propertyHash];
  191.         }
  192.         try {
  193.             $reflectionProperty = new \ReflectionProperty($class$property);
  194.         } catch (\ReflectionException) {
  195.             $reflectionProperty null;
  196.         }
  197.         $ucFirstProperty ucfirst($property);
  198.         switch (true) {
  199.             case $reflectionProperty?->isPromoted() && $docBlock $this->getDocBlockFromConstructor($class$property):
  200.                 $data = [$docBlockself::MUTATORnull];
  201.                 break;
  202.             case $docBlock $this->getDocBlockFromProperty($class$property):
  203.                 $data = [$docBlockself::PROPERTYnull];
  204.                 break;
  205.             case [$docBlock] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR):
  206.                 $data = [$docBlockself::ACCESSORnull];
  207.                 break;
  208.             case [$docBlock$prefix] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR):
  209.                 $data = [$docBlockself::MUTATOR$prefix];
  210.                 break;
  211.             default:
  212.                 $data = [nullnullnull];
  213.         }
  214.         return $this->docBlocks[$propertyHash] = $data;
  215.     }
  216.     private function getDocBlockFromProperty(string $classstring $property): ?DocBlock
  217.     {
  218.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  219.         try {
  220.             $reflectionProperty = new \ReflectionProperty($class$property);
  221.         } catch (\ReflectionException) {
  222.             return null;
  223.         }
  224.         $reflector $reflectionProperty->getDeclaringClass();
  225.         foreach ($reflector->getTraits() as $trait) {
  226.             if ($trait->hasProperty($property)) {
  227.                 return $this->getDocBlockFromProperty($trait->getName(), $property);
  228.             }
  229.         }
  230.         try {
  231.             return $this->docBlockFactory->create($reflectionProperty$this->createFromReflector($reflector));
  232.         } catch (\InvalidArgumentException|\RuntimeException) {
  233.             return null;
  234.         }
  235.     }
  236.     /**
  237.      * @return array{DocBlock, string}|null
  238.      */
  239.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  240.     {
  241.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  242.         $prefix null;
  243.         foreach ($prefixes as $prefix) {
  244.             $methodName $prefix.$ucFirstProperty;
  245.             try {
  246.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  247.                 if ($reflectionMethod->isStatic()) {
  248.                     continue;
  249.                 }
  250.                 if (
  251.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters()) ||
  252.                     (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  253.                 ) {
  254.                     break;
  255.                 }
  256.             } catch (\ReflectionException) {
  257.                 // Try the next prefix if the method doesn't exist
  258.             }
  259.         }
  260.         if (!isset($reflectionMethod)) {
  261.             return null;
  262.         }
  263.         $reflector $reflectionMethod->getDeclaringClass();
  264.         foreach ($reflector->getTraits() as $trait) {
  265.             if ($trait->hasMethod($methodName)) {
  266.                 return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty$type);
  267.             }
  268.         }
  269.         try {
  270.             return [$this->docBlockFactory->create($reflectionMethod$this->createFromReflector($reflector)), $prefix];
  271.         } catch (\InvalidArgumentException|\RuntimeException) {
  272.             return null;
  273.         }
  274.     }
  275.     /**
  276.      * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
  277.      */
  278.     private function createFromReflector(\ReflectionClass $reflector): Context
  279.     {
  280.         $cacheKey $reflector->getNamespaceName().':'.$reflector->getFileName();
  281.         if (isset($this->contexts[$cacheKey])) {
  282.             return $this->contexts[$cacheKey];
  283.         }
  284.         $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
  285.         return $this->contexts[$cacheKey];
  286.     }
  287. }