/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php

https://github.com/deviantintegral/symfony · PHP · 231 lines · 152 code · 40 blank · 39 comment · 18 complexity · 44dab39eb622d75e288d4de0ba12860c MD5 · raw file

  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\DocBlockFactory;
  13. use phpDocumentor\Reflection\DocBlockFactoryInterface;
  14. use phpDocumentor\Reflection\Types\ContextFactory;
  15. use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
  16. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  17. use Symfony\Component\PropertyInfo\Type;
  18. use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
  19. /**
  20. * Extracts data using a PHPDoc parser.
  21. *
  22. * @author Kévin Dunglas <dunglas@gmail.com>
  23. *
  24. * @final
  25. */
  26. class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
  27. {
  28. const PROPERTY = 0;
  29. const ACCESSOR = 1;
  30. const MUTATOR = 2;
  31. /**
  32. * @var DocBlock[]
  33. */
  34. private $docBlocks = array();
  35. private $docBlockFactory;
  36. private $contextFactory;
  37. private $phpDocTypeHelper;
  38. private $mutatorPrefixes;
  39. private $accessorPrefixes;
  40. private $arrayMutatorPrefixes;
  41. /**
  42. * @param DocBlockFactoryInterface $docBlockFactory
  43. * @param string[]|null $mutatorPrefixes
  44. * @param string[]|null $accessorPrefixes
  45. * @param string[]|null $arrayMutatorPrefixes
  46. */
  47. public function __construct(DocBlockFactoryInterface $docBlockFactory = null, array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null)
  48. {
  49. if (!class_exists(DocBlockFactory::class)) {
  50. throw new \RuntimeException(sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed.', __CLASS__));
  51. }
  52. $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
  53. $this->contextFactory = new ContextFactory();
  54. $this->phpDocTypeHelper = new PhpDocTypeHelper();
  55. $this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : ReflectionExtractor::$defaultMutatorPrefixes;
  56. $this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : ReflectionExtractor::$defaultAccessorPrefixes;
  57. $this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : ReflectionExtractor::$defaultArrayMutatorPrefixes;
  58. }
  59. /**
  60. * {@inheritdoc}
  61. */
  62. public function getShortDescription($class, $property, array $context = array())
  63. {
  64. /** @var $docBlock DocBlock */
  65. list($docBlock) = $this->getDocBlock($class, $property);
  66. if (!$docBlock) {
  67. return;
  68. }
  69. $shortDescription = $docBlock->getSummary();
  70. if (!empty($shortDescription)) {
  71. return $shortDescription;
  72. }
  73. foreach ($docBlock->getTagsByName('var') as $var) {
  74. $varDescription = $var->getDescription()->render();
  75. if (!empty($varDescription)) {
  76. return $varDescription;
  77. }
  78. }
  79. }
  80. /**
  81. * {@inheritdoc}
  82. */
  83. public function getLongDescription($class, $property, array $context = array())
  84. {
  85. /** @var $docBlock DocBlock */
  86. list($docBlock) = $this->getDocBlock($class, $property);
  87. if (!$docBlock) {
  88. return;
  89. }
  90. $contents = $docBlock->getDescription()->render();
  91. return '' === $contents ? null : $contents;
  92. }
  93. /**
  94. * {@inheritdoc}
  95. */
  96. public function getTypes($class, $property, array $context = array())
  97. {
  98. /** @var $docBlock DocBlock */
  99. list($docBlock, $source, $prefix) = $this->getDocBlock($class, $property);
  100. if (!$docBlock) {
  101. return;
  102. }
  103. switch ($source) {
  104. case self::PROPERTY:
  105. $tag = 'var';
  106. break;
  107. case self::ACCESSOR:
  108. $tag = 'return';
  109. break;
  110. case self::MUTATOR:
  111. $tag = 'param';
  112. break;
  113. }
  114. $types = array();
  115. /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
  116. foreach ($docBlock->getTagsByName($tag) as $tag) {
  117. if ($tag && null !== $tag->getType()) {
  118. $types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
  119. }
  120. }
  121. if (!isset($types[0])) {
  122. return;
  123. }
  124. if (!in_array($prefix, $this->arrayMutatorPrefixes)) {
  125. return $types;
  126. }
  127. return array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0]));
  128. }
  129. private function getDocBlock(string $class, string $property): array
  130. {
  131. $propertyHash = sprintf('%s::%s', $class, $property);
  132. if (isset($this->docBlocks[$propertyHash])) {
  133. return $this->docBlocks[$propertyHash];
  134. }
  135. $ucFirstProperty = ucfirst($property);
  136. try {
  137. switch (true) {
  138. case $docBlock = $this->getDocBlockFromProperty($class, $property):
  139. $data = array($docBlock, self::PROPERTY, null);
  140. break;
  141. case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
  142. $data = array($docBlock, self::ACCESSOR, null);
  143. break;
  144. case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
  145. $data = array($docBlock, self::MUTATOR, $prefix);
  146. break;
  147. default:
  148. $data = array(null, null, null);
  149. }
  150. } catch (\InvalidArgumentException $e) {
  151. $data = array(null, null, null);
  152. }
  153. return $this->docBlocks[$propertyHash] = $data;
  154. }
  155. private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
  156. {
  157. // Use a ReflectionProperty instead of $class to get the parent class if applicable
  158. try {
  159. $reflectionProperty = new \ReflectionProperty($class, $property);
  160. } catch (\ReflectionException $e) {
  161. return null;
  162. }
  163. return $this->docBlockFactory->create($reflectionProperty, $this->contextFactory->createFromReflector($reflectionProperty->getDeclaringClass()));
  164. }
  165. private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
  166. {
  167. $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
  168. $prefix = null;
  169. foreach ($prefixes as $prefix) {
  170. $methodName = $prefix.$ucFirstProperty;
  171. try {
  172. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  173. if ($reflectionMethod->isStatic()) {
  174. continue;
  175. }
  176. if (
  177. (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) ||
  178. (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
  179. ) {
  180. break;
  181. }
  182. } catch (\ReflectionException $e) {
  183. // Try the next prefix if the method doesn't exist
  184. }
  185. }
  186. if (!isset($reflectionMethod)) {
  187. return null;
  188. }
  189. return array($this->docBlockFactory->create($reflectionMethod, $this->contextFactory->createFromReflector($reflectionMethod)), $prefix);
  190. }
  191. }