PhpDocExtractor.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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, ConstructorArgumentTypeExtractorInterface
  29. {
  30. public const PROPERTY = 0;
  31. public const ACCESSOR = 1;
  32. public 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. [$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. [$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. [$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. /**
  138. * {@inheritdoc}
  139. */
  140. public function getTypesFromConstructor(string $class, string $property): ?array
  141. {
  142. $docBlock = $this->getDocBlockFromConstructor($class, $property);
  143. if (!$docBlock) {
  144. return null;
  145. }
  146. $types = [];
  147. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  148. foreach ($docBlock->getTagsByName('param') as $tag) {
  149. if ($tag && null !== $tag->getType()) {
  150. $types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
  151. }
  152. }
  153. if (!isset($types[0])) {
  154. return null;
  155. }
  156. return $types;
  157. }
  158. private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
  159. {
  160. try {
  161. $reflectionClass = new \ReflectionClass($class);
  162. } catch (\ReflectionException $e) {
  163. return null;
  164. }
  165. $reflectionConstructor = $reflectionClass->getConstructor();
  166. if (!$reflectionConstructor) {
  167. return null;
  168. }
  169. try {
  170. $docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor));
  171. return $this->filterDocBlockParams($docBlock, $property);
  172. } catch (\InvalidArgumentException $e) {
  173. return null;
  174. }
  175. }
  176. private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock
  177. {
  178. $tags = array_values(array_filter($docBlock->getTagsByName('param'), function ($tag) use ($allowedParam) {
  179. return $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName();
  180. }));
  181. return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(),
  182. $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
  183. }
  184. private function getDocBlock(string $class, string $property): array
  185. {
  186. $propertyHash = sprintf('%s::%s', $class, $property);
  187. if (isset($this->docBlocks[$propertyHash])) {
  188. return $this->docBlocks[$propertyHash];
  189. }
  190. $ucFirstProperty = ucfirst($property);
  191. switch (true) {
  192. case $docBlock = $this->getDocBlockFromProperty($class, $property):
  193. $data = [$docBlock, self::PROPERTY, null];
  194. break;
  195. case [$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
  196. $data = [$docBlock, self::ACCESSOR, null];
  197. break;
  198. case [$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
  199. $data = [$docBlock, self::MUTATOR, $prefix];
  200. break;
  201. default:
  202. $data = [null, null, null];
  203. }
  204. return $this->docBlocks[$propertyHash] = $data;
  205. }
  206. private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
  207. {
  208. // Use a ReflectionProperty instead of $class to get the parent class if applicable
  209. try {
  210. $reflectionProperty = new \ReflectionProperty($class, $property);
  211. } catch (\ReflectionException $e) {
  212. return null;
  213. }
  214. try {
  215. return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass()));
  216. } catch (\InvalidArgumentException $e) {
  217. return null;
  218. } catch (\RuntimeException $e) {
  219. return null;
  220. }
  221. }
  222. private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
  223. {
  224. $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
  225. $prefix = null;
  226. foreach ($prefixes as $prefix) {
  227. $methodName = $prefix.$ucFirstProperty;
  228. try {
  229. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  230. if ($reflectionMethod->isStatic()) {
  231. continue;
  232. }
  233. if (
  234. (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) ||
  235. (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  236. ) {
  237. break;
  238. }
  239. } catch (\ReflectionException $e) {
  240. // Try the next prefix if the method doesn't exist
  241. }
  242. }
  243. if (!isset($reflectionMethod)) {
  244. return null;
  245. }
  246. try {
  247. return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix];
  248. } catch (\InvalidArgumentException $e) {
  249. return null;
  250. } catch (\RuntimeException $e) {
  251. return null;
  252. }
  253. }
  254. /**
  255. * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
  256. */
  257. private function createFromReflector(\ReflectionClass $reflector): Context
  258. {
  259. $cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
  260. if (isset($this->contexts[$cacheKey])) {
  261. return $this->contexts[$cacheKey];
  262. }
  263. $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
  264. return $this->contexts[$cacheKey];
  265. }
  266. }