PhpDocExtractor.php 12 KB

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