PhpDocExtractor.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
  29. {
  30. const PROPERTY = 0;
  31. const ACCESSOR = 1;
  32. const MUTATOR = 2;
  33. /**
  34. * @var DocBlock[]
  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 = null !== $mutatorPrefixes ? $mutatorPrefixes : ReflectionExtractor::$defaultMutatorPrefixes;
  61. $this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : ReflectionExtractor::$defaultAccessorPrefixes;
  62. $this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : ReflectionExtractor::$defaultArrayMutatorPrefixes;
  63. }
  64. /**
  65. * {@inheritdoc}
  66. */
  67. public function getShortDescription(string $class, string $property, array $context = []): ?string
  68. {
  69. /** @var $docBlock DocBlock */
  70. list($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 $class, string $property, array $context = []): ?string
  92. {
  93. /** @var $docBlock DocBlock */
  94. list($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 $class, string $property, array $context = []): ?array
  105. {
  106. /** @var $docBlock DocBlock */
  107. list($docBlock, $source, $prefix) = $this->getDocBlock($class, $property);
  108. if (!$docBlock) {
  109. return null;
  110. }
  111. switch ($source) {
  112. case self::PROPERTY:
  113. $tag = 'var';
  114. break;
  115. case self::ACCESSOR:
  116. $tag = 'return';
  117. break;
  118. case self::MUTATOR:
  119. $tag = 'param';
  120. break;
  121. }
  122. $types = [];
  123. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  124. foreach ($docBlock->getTagsByName($tag) as $tag) {
  125. if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
  126. $types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
  127. }
  128. }
  129. if (!isset($types[0])) {
  130. return null;
  131. }
  132. if (!\in_array($prefix, $this->arrayMutatorPrefixes)) {
  133. return $types;
  134. }
  135. return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
  136. }
  137. private function getDocBlock(string $class, string $property): array
  138. {
  139. $propertyHash = sprintf('%s::%s', $class, $property);
  140. if (isset($this->docBlocks[$propertyHash])) {
  141. return $this->docBlocks[$propertyHash];
  142. }
  143. $ucFirstProperty = ucfirst($property);
  144. switch (true) {
  145. case $docBlock = $this->getDocBlockFromProperty($class, $property):
  146. $data = [$docBlock, self::PROPERTY, null];
  147. break;
  148. case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
  149. $data = [$docBlock, self::ACCESSOR, null];
  150. break;
  151. case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
  152. $data = [$docBlock, self::MUTATOR, $prefix];
  153. break;
  154. default:
  155. $data = [null, null, null];
  156. }
  157. return $this->docBlocks[$propertyHash] = $data;
  158. }
  159. private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
  160. {
  161. // Use a ReflectionProperty instead of $class to get the parent class if applicable
  162. try {
  163. $reflectionProperty = new \ReflectionProperty($class, $property);
  164. } catch (\ReflectionException $e) {
  165. return null;
  166. }
  167. try {
  168. return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass()));
  169. } catch (\InvalidArgumentException $e) {
  170. return null;
  171. } catch (\RuntimeException $e) {
  172. return null;
  173. }
  174. }
  175. private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
  176. {
  177. $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
  178. $prefix = null;
  179. foreach ($prefixes as $prefix) {
  180. $methodName = $prefix.$ucFirstProperty;
  181. try {
  182. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  183. if ($reflectionMethod->isStatic()) {
  184. continue;
  185. }
  186. if (
  187. (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) ||
  188. (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  189. ) {
  190. break;
  191. }
  192. } catch (\ReflectionException $e) {
  193. // Try the next prefix if the method doesn't exist
  194. }
  195. }
  196. if (!isset($reflectionMethod)) {
  197. return null;
  198. }
  199. try {
  200. return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix];
  201. } catch (\InvalidArgumentException $e) {
  202. return null;
  203. } catch (\RuntimeException $e) {
  204. return null;
  205. }
  206. }
  207. /**
  208. * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
  209. */
  210. private function createFromReflector(\ReflectionClass $reflector): Context
  211. {
  212. $cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
  213. if (isset($this->contexts[$cacheKey])) {
  214. return $this->contexts[$cacheKey];
  215. }
  216. $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
  217. return $this->contexts[$cacheKey];
  218. }
  219. }