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

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\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.'__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.     /**
  65.      * {@inheritdoc}
  66.      */
  67.     public function getShortDescription(string $classstring $property, array $context = []): ?string
  68.     {
  69.         /** @var $docBlock DocBlock */
  70.         [$docBlock] = $this->getDocBlock($class$property);
  71.         if (!$docBlock) {
  72.             return null;
  73.         }
  74.         $shortDescription $docBlock->getSummary();
  75.         if (!empty($shortDescription)) {
  76.             return $shortDescription;
  77.         }
  78.         foreach ($docBlock->getTagsByName('var') as $var) {
  79.             if ($var && !$var instanceof InvalidTag) {
  80.                 $varDescription $var->getDescription()->render();
  81.                 if (!empty($varDescription)) {
  82.                     return $varDescription;
  83.                 }
  84.             }
  85.         }
  86.         return null;
  87.     }
  88.     /**
  89.      * {@inheritdoc}
  90.      */
  91.     public function getLongDescription(string $classstring $property, array $context = []): ?string
  92.     {
  93.         /** @var $docBlock DocBlock */
  94.         [$docBlock] = $this->getDocBlock($class$property);
  95.         if (!$docBlock) {
  96.             return null;
  97.         }
  98.         $contents $docBlock->getDescription()->render();
  99.         return '' === $contents null $contents;
  100.     }
  101.     /**
  102.      * {@inheritdoc}
  103.      */
  104.     public function getTypes(string $classstring $property, array $context = []): ?array
  105.     {
  106.         /** @var $docBlock DocBlock */
  107.         [$docBlock$source$prefix] = $this->getDocBlock($class$property);
  108.         if (!$docBlock) {
  109.             return null;
  110.         }
  111.         $tag = match ($source) {
  112.             self::PROPERTY => 'var',
  113.             self::ACCESSOR => 'return',
  114.             self::MUTATOR => 'param',
  115.         };
  116.         $parentClass null;
  117.         $types = [];
  118.         /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  119.         foreach ($docBlock->getTagsByName($tag) as $tag) {
  120.             if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
  121.                 foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) {
  122.                     switch ($type->getClassName()) {
  123.                         case 'self':
  124.                         case 'static':
  125.                             $resolvedClass $class;
  126.                             break;
  127.                         case 'parent':
  128.                             if (false !== $resolvedClass $parentClass ??= get_parent_class($class)) {
  129.                                 break;
  130.                             }
  131.                             // no break
  132.                         default:
  133.                             $types[] = $type;
  134.                             continue 2;
  135.                     }
  136.                     $types[] = new Type(Type::BUILTIN_TYPE_OBJECT$type->isNullable(), $resolvedClass$type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  137.                 }
  138.             }
  139.         }
  140.         if (!isset($types[0])) {
  141.             return null;
  142.         }
  143.         if (!\in_array($prefix$this->arrayMutatorPrefixes)) {
  144.             return $types;
  145.         }
  146.         return [new Type(Type::BUILTIN_TYPE_ARRAYfalsenulltrue, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  147.     }
  148.     /**
  149.      * {@inheritdoc}
  150.      */
  151.     public function getTypesFromConstructor(string $classstring $property): ?array
  152.     {
  153.         $docBlock $this->getDocBlockFromConstructor($class$property);
  154.         if (!$docBlock) {
  155.             return null;
  156.         }
  157.         $types = [];
  158.         /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  159.         foreach ($docBlock->getTagsByName('param') as $tag) {
  160.             if ($tag && null !== $tag->getType()) {
  161.                 $types[] = $this->phpDocTypeHelper->getTypes($tag->getType());
  162.             }
  163.         }
  164.         if (!isset($types[0]) || [] === $types[0]) {
  165.             return null;
  166.         }
  167.         return array_merge([], ...$types);
  168.     }
  169.     private function getDocBlockFromConstructor(string $classstring $property): ?DocBlock
  170.     {
  171.         try {
  172.             $reflectionClass = new \ReflectionClass($class);
  173.         } catch (\ReflectionException) {
  174.             return null;
  175.         }
  176.         $reflectionConstructor $reflectionClass->getConstructor();
  177.         if (!$reflectionConstructor) {
  178.             return null;
  179.         }
  180.         try {
  181.             $docBlock $this->docBlockFactory->create($reflectionConstructor$this->contextFactory->createFromReflector($reflectionConstructor));
  182.             return $this->filterDocBlockParams($docBlock$property);
  183.         } catch (\InvalidArgumentException) {
  184.             return null;
  185.         }
  186.     }
  187.     private function filterDocBlockParams(DocBlock $docBlockstring $allowedParam): DocBlock
  188.     {
  189.         $tags array_values(array_filter($docBlock->getTagsByName('param'), function ($tag) use ($allowedParam) {
  190.             return $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName();
  191.         }));
  192.         return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags$docBlock->getContext(),
  193.             $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
  194.     }
  195.     /**
  196.      * @return array{DocBlock|null, int|null, string|null}
  197.      */
  198.     private function getDocBlock(string $classstring $property): array
  199.     {
  200.         $propertyHash sprintf('%s::%s'$class$property);
  201.         if (isset($this->docBlocks[$propertyHash])) {
  202.             return $this->docBlocks[$propertyHash];
  203.         }
  204.         try {
  205.             $reflectionProperty = new \ReflectionProperty($class$property);
  206.         } catch (\ReflectionException) {
  207.             $reflectionProperty null;
  208.         }
  209.         $ucFirstProperty ucfirst($property);
  210.         switch (true) {
  211.             case $reflectionProperty?->isPromoted() && $docBlock $this->getDocBlockFromConstructor($class$property):
  212.                 $data = [$docBlockself::MUTATORnull];
  213.                 break;
  214.             case $docBlock $this->getDocBlockFromProperty($class$property):
  215.                 $data = [$docBlockself::PROPERTYnull];
  216.                 break;
  217.             case [$docBlock] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::ACCESSOR):
  218.                 $data = [$docBlockself::ACCESSORnull];
  219.                 break;
  220.             case [$docBlock$prefix] = $this->getDocBlockFromMethod($class$ucFirstPropertyself::MUTATOR):
  221.                 $data = [$docBlockself::MUTATOR$prefix];
  222.                 break;
  223.             default:
  224.                 $data = [nullnullnull];
  225.         }
  226.         return $this->docBlocks[$propertyHash] = $data;
  227.     }
  228.     private function getDocBlockFromProperty(string $classstring $property): ?DocBlock
  229.     {
  230.         // Use a ReflectionProperty instead of $class to get the parent class if applicable
  231.         try {
  232.             $reflectionProperty = new \ReflectionProperty($class$property);
  233.         } catch (\ReflectionException) {
  234.             return null;
  235.         }
  236.         $reflector $reflectionProperty->getDeclaringClass();
  237.         foreach ($reflector->getTraits() as $trait) {
  238.             if ($trait->hasProperty($property)) {
  239.                 return $this->getDocBlockFromProperty($trait->getName(), $property);
  240.             }
  241.         }
  242.         try {
  243.             return $this->docBlockFactory->create($reflectionProperty$this->createFromReflector($reflector));
  244.         } catch (\InvalidArgumentException|\RuntimeException) {
  245.             return null;
  246.         }
  247.     }
  248.     /**
  249.      * @return array{DocBlock, string}|null
  250.      */
  251.     private function getDocBlockFromMethod(string $classstring $ucFirstPropertyint $type): ?array
  252.     {
  253.         $prefixes self::ACCESSOR === $type $this->accessorPrefixes $this->mutatorPrefixes;
  254.         $prefix null;
  255.         foreach ($prefixes as $prefix) {
  256.             $methodName $prefix.$ucFirstProperty;
  257.             try {
  258.                 $reflectionMethod = new \ReflectionMethod($class$methodName);
  259.                 if ($reflectionMethod->isStatic()) {
  260.                     continue;
  261.                 }
  262.                 if (
  263.                     (self::ACCESSOR === $type && === $reflectionMethod->getNumberOfRequiredParameters()) ||
  264.                     (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  265.                 ) {
  266.                     break;
  267.                 }
  268.             } catch (\ReflectionException) {
  269.                 // Try the next prefix if the method doesn't exist
  270.             }
  271.         }
  272.         if (!isset($reflectionMethod)) {
  273.             return null;
  274.         }
  275.         $reflector $reflectionMethod->getDeclaringClass();
  276.         foreach ($reflector->getTraits() as $trait) {
  277.             if ($trait->hasMethod($methodName)) {
  278.                 return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty$type);
  279.             }
  280.         }
  281.         try {
  282.             return [$this->docBlockFactory->create($reflectionMethod$this->createFromReflector($reflector)), $prefix];
  283.         } catch (\InvalidArgumentException|\RuntimeException) {
  284.             return null;
  285.         }
  286.     }
  287.     /**
  288.      * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
  289.      */
  290.     private function createFromReflector(\ReflectionClass $reflector): Context
  291.     {
  292.         $cacheKey $reflector->getNamespaceName().':'.$reflector->getFileName();
  293.         if (isset($this->contexts[$cacheKey])) {
  294.             return $this->contexts[$cacheKey];
  295.         }
  296.         $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
  297.         return $this->contexts[$cacheKey];
  298.     }
  299. }