PhpDocExtractor.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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\PropertyDocBlockExtractorInterface;
  19. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  20. use Symfony\Component\PropertyInfo\Type as LegacyType;
  21. use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
  22. use Symfony\Component\TypeInfo\Exception\LogicException;
  23. use Symfony\Component\TypeInfo\Type;
  24. use Symfony\Component\TypeInfo\Type\ObjectType;
  25. use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
  26. /**
  27. * Extracts data using a PHPDoc parser.
  28. *
  29. * @author Kévin Dunglas <dunglas@gmail.com>
  30. *
  31. * @final
  32. */
  33. class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface, PropertyDocBlockExtractorInterface
  34. {
  35. public const PROPERTY = 0;
  36. public const ACCESSOR = 1;
  37. public const MUTATOR = 2;
  38. /**
  39. * @var array<string, array{DocBlock|null, int|null, string|null}>
  40. */
  41. private array $docBlocks = [];
  42. /**
  43. * @var Context[]
  44. */
  45. private array $contexts = [];
  46. private DocBlockFactoryInterface $docBlockFactory;
  47. private ContextFactory $contextFactory;
  48. private TypeContextFactory $typeContextFactory;
  49. private PhpDocTypeHelper $phpDocTypeHelper;
  50. private array $mutatorPrefixes;
  51. private array $accessorPrefixes;
  52. private array $arrayMutatorPrefixes;
  53. /**
  54. * @param string[]|null $mutatorPrefixes
  55. * @param string[]|null $accessorPrefixes
  56. * @param string[]|null $arrayMutatorPrefixes
  57. */
  58. public function __construct(?DocBlockFactoryInterface $docBlockFactory = null, ?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null)
  59. {
  60. if (!class_exists(DocBlockFactory::class)) {
  61. 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__));
  62. }
  63. $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
  64. $this->contextFactory = new ContextFactory();
  65. $this->typeContextFactory = new TypeContextFactory();
  66. $this->phpDocTypeHelper = new PhpDocTypeHelper();
  67. $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
  68. $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
  69. $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
  70. }
  71. public function getShortDescription(string $class, string $property, array $context = []): ?string
  72. {
  73. /** @var DocBlock $docBlock */
  74. [$docBlock] = $this->findDocBlock($class, $property);
  75. if (!$docBlock) {
  76. return null;
  77. }
  78. $shortDescription = $docBlock->getSummary();
  79. if ($shortDescription) {
  80. return $shortDescription;
  81. }
  82. foreach ($docBlock->getTagsByName('var') as $var) {
  83. if ($var && !$var instanceof InvalidTag) {
  84. $varDescription = $var->getDescription()->render();
  85. if ($varDescription) {
  86. return $varDescription;
  87. }
  88. }
  89. }
  90. return null;
  91. }
  92. public function getLongDescription(string $class, string $property, array $context = []): ?string
  93. {
  94. /** @var DocBlock $docBlock */
  95. [$docBlock] = $this->findDocBlock($class, $property);
  96. if (!$docBlock) {
  97. return null;
  98. }
  99. $contents = $docBlock->getDescription()->render();
  100. return '' === $contents ? null : $contents;
  101. }
  102. public function getTypes(string $class, string $property, array $context = []): ?array
  103. {
  104. /** @var DocBlock $docBlock */
  105. [$docBlock, $source, $prefix] = $this->findDocBlock($class, $property);
  106. if (!$docBlock) {
  107. return null;
  108. }
  109. $tag = match ($source) {
  110. self::PROPERTY => 'var',
  111. self::ACCESSOR => 'return',
  112. self::MUTATOR => 'param',
  113. };
  114. $parentClass = null;
  115. $types = [];
  116. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  117. foreach ($docBlock->getTagsByName($tag) as $tag) {
  118. if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
  119. foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) {
  120. switch ($type->getClassName()) {
  121. case 'self':
  122. case 'static':
  123. $resolvedClass = $class;
  124. break;
  125. case 'parent':
  126. if (false !== $resolvedClass = $parentClass ??= get_parent_class($class)) {
  127. break;
  128. }
  129. // no break
  130. default:
  131. $types[] = $type;
  132. continue 2;
  133. }
  134. $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
  135. }
  136. }
  137. }
  138. if (!isset($types[0])) {
  139. return null;
  140. }
  141. if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
  142. return $types;
  143. }
  144. return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $types[0])];
  145. }
  146. public function getTypesFromConstructor(string $class, string $property): ?array
  147. {
  148. $docBlock = $this->getDocBlockFromConstructor($class, $property);
  149. if (!$docBlock) {
  150. return null;
  151. }
  152. $types = [];
  153. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  154. foreach ($docBlock->getTagsByName('param') as $tag) {
  155. if ($tag && null !== $tag->getType()) {
  156. $types[] = $this->phpDocTypeHelper->getTypes($tag->getType());
  157. }
  158. }
  159. if (!isset($types[0]) || [] === $types[0]) {
  160. return null;
  161. }
  162. return array_merge([], ...$types);
  163. }
  164. public function getType(string $class, string $property, array $context = []): ?Type
  165. {
  166. /** @var DocBlock $docBlock */
  167. [$docBlock, $source, $prefix] = $this->findDocBlock($class, $property);
  168. if (!$docBlock) {
  169. return null;
  170. }
  171. $tag = match ($source) {
  172. self::PROPERTY => 'var',
  173. self::ACCESSOR => 'return',
  174. self::MUTATOR => 'param',
  175. };
  176. $types = [];
  177. $typeContext = $this->typeContextFactory->createFromClassName($class);
  178. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  179. foreach ($docBlock->getTagsByName($tag) as $tag) {
  180. if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) {
  181. continue;
  182. }
  183. $type = $this->phpDocTypeHelper->getType($tagType);
  184. if (!$type instanceof ObjectType) {
  185. $types[] = $type;
  186. continue;
  187. }
  188. $normalizedClassName = match ($type->getClassName()) {
  189. 'self' => $typeContext->getDeclaringClass(),
  190. 'static' => $typeContext->getCalledClass(),
  191. default => $type->getClassName(),
  192. };
  193. if ('parent' === $normalizedClassName) {
  194. try {
  195. $normalizedClassName = $typeContext->getParentClass();
  196. } catch (LogicException) {
  197. // if there is no parent for the current class, we keep the "parent" raw string
  198. }
  199. }
  200. $types[] = $type->isNullable() ? Type::nullable(Type::object($normalizedClassName)) : Type::object($normalizedClassName);
  201. }
  202. if (null === $type = $types[0] ?? null) {
  203. return null;
  204. }
  205. if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
  206. return $type;
  207. }
  208. return Type::list($type);
  209. }
  210. public function getTypeFromConstructor(string $class, string $property): ?Type
  211. {
  212. if (!$docBlock = $this->getDocBlockFromConstructor($class, $property)) {
  213. return null;
  214. }
  215. $types = [];
  216. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  217. foreach ($docBlock->getTagsByName('param') as $tag) {
  218. if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) {
  219. continue;
  220. }
  221. $types[] = $this->phpDocTypeHelper->getType($tagType);
  222. }
  223. return $types[0] ?? null;
  224. }
  225. public function getDocBlock(string $class, string $property): ?DocBlock
  226. {
  227. $output = $this->findDocBlock($class, $property);
  228. return $output[0];
  229. }
  230. private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
  231. {
  232. try {
  233. $reflectionClass = new \ReflectionClass($class);
  234. } catch (\ReflectionException) {
  235. return null;
  236. }
  237. $reflectionConstructor = $reflectionClass->getConstructor();
  238. if (!$reflectionConstructor) {
  239. return null;
  240. }
  241. try {
  242. $docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor));
  243. return $this->filterDocBlockParams($docBlock, $property);
  244. } catch (\InvalidArgumentException) {
  245. return null;
  246. }
  247. }
  248. private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock
  249. {
  250. $tags = array_values(array_filter($docBlock->getTagsByName('param'), fn ($tag) => $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName()));
  251. return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(),
  252. $docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
  253. }
  254. /**
  255. * @return array{DocBlock|null, int|null, string|null}
  256. */
  257. private function findDocBlock(string $class, string $property): array
  258. {
  259. $propertyHash = \sprintf('%s::%s', $class, $property);
  260. if (isset($this->docBlocks[$propertyHash])) {
  261. return $this->docBlocks[$propertyHash];
  262. }
  263. try {
  264. $reflectionProperty = new \ReflectionProperty($class, $property);
  265. } catch (\ReflectionException) {
  266. $reflectionProperty = null;
  267. }
  268. $ucFirstProperty = ucfirst($property);
  269. switch (true) {
  270. case $reflectionProperty?->isPromoted() && $docBlock = $this->getDocBlockFromConstructor($class, $property):
  271. $data = [$docBlock, self::MUTATOR, null];
  272. break;
  273. case $docBlock = $this->getDocBlockFromProperty($class, $property):
  274. $data = [$docBlock, self::PROPERTY, null];
  275. break;
  276. case [$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
  277. $data = [$docBlock, self::ACCESSOR, null];
  278. break;
  279. case [$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
  280. $data = [$docBlock, self::MUTATOR, $prefix];
  281. break;
  282. default:
  283. $data = [null, null, null];
  284. }
  285. return $this->docBlocks[$propertyHash] = $data;
  286. }
  287. private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
  288. {
  289. // Use a ReflectionProperty instead of $class to get the parent class if applicable
  290. try {
  291. $reflectionProperty = new \ReflectionProperty($class, $property);
  292. } catch (\ReflectionException) {
  293. return null;
  294. }
  295. $reflector = $reflectionProperty->getDeclaringClass();
  296. foreach ($reflector->getTraits() as $trait) {
  297. if ($trait->hasProperty($property)) {
  298. return $this->getDocBlockFromProperty($trait->getName(), $property);
  299. }
  300. }
  301. try {
  302. return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector));
  303. } catch (\InvalidArgumentException|\RuntimeException) {
  304. return null;
  305. }
  306. }
  307. /**
  308. * @return array{DocBlock, string}|null
  309. */
  310. private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
  311. {
  312. $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
  313. $prefix = null;
  314. foreach ($prefixes as $prefix) {
  315. $methodName = $prefix.$ucFirstProperty;
  316. try {
  317. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  318. if ($reflectionMethod->isStatic()) {
  319. continue;
  320. }
  321. if (
  322. (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
  323. || (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  324. ) {
  325. break;
  326. }
  327. } catch (\ReflectionException) {
  328. // Try the next prefix if the method doesn't exist
  329. }
  330. }
  331. if (!isset($reflectionMethod)) {
  332. return null;
  333. }
  334. $reflector = $reflectionMethod->getDeclaringClass();
  335. foreach ($reflector->getTraits() as $trait) {
  336. if ($trait->hasMethod($methodName)) {
  337. return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty, $type);
  338. }
  339. }
  340. try {
  341. return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix];
  342. } catch (\InvalidArgumentException|\RuntimeException) {
  343. return null;
  344. }
  345. }
  346. /**
  347. * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
  348. */
  349. private function createFromReflector(\ReflectionClass $reflector): Context
  350. {
  351. $cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
  352. if (isset($this->contexts[$cacheKey])) {
  353. return $this->contexts[$cacheKey];
  354. }
  355. $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
  356. return $this->contexts[$cacheKey];
  357. }
  358. }