PhpDocReader.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Utils\CodeGen;
  12. use PhpDocReader\AnnotationException;
  13. use PhpDocReader\PhpParser\UseStatementParser;
  14. use ReflectionClass;
  15. use ReflectionMethod;
  16. use ReflectionParameter;
  17. use ReflectionProperty;
  18. use Reflector;
  19. /**
  20. * @see https://github.com/PHP-DI/PhpDocReader
  21. */
  22. class PhpDocReader
  23. {
  24. private const PRIMITIVE_TYPES = [
  25. 'bool' => 'bool',
  26. 'boolean' => 'bool',
  27. 'string' => 'string',
  28. 'int' => 'int',
  29. 'integer' => 'int',
  30. 'float' => 'float',
  31. 'double' => 'float',
  32. 'array' => 'array',
  33. 'object' => 'object',
  34. 'callable' => 'callable',
  35. 'resource' => 'resource',
  36. 'mixed' => 'mixed',
  37. 'iterable' => 'iterable',
  38. ];
  39. /**
  40. * @var null|PhpDocReader
  41. */
  42. protected static $instance;
  43. /** @var UseStatementParser */
  44. private $parser;
  45. /** @var bool */
  46. private $ignorePhpDocErrors;
  47. /**
  48. * @param bool $ignorePhpDocErrors enable or disable throwing errors when PhpDoc errors occur (when parsing annotations)
  49. */
  50. public function __construct(bool $ignorePhpDocErrors = false)
  51. {
  52. $this->parser = new UseStatementParser();
  53. $this->ignorePhpDocErrors = $ignorePhpDocErrors;
  54. }
  55. public static function getInstance(): PhpDocReader
  56. {
  57. if (static::$instance) {
  58. return static::$instance;
  59. }
  60. return static::$instance = new static();
  61. }
  62. /**
  63. * Parse the docblock of the property to get the type (class or primitive type) of the param annotation.
  64. *
  65. * @throws AnnotationException
  66. */
  67. public function getReturnType(ReflectionMethod $method, bool $withoutNamespace = false): array
  68. {
  69. return $this->readReturnClass($method, true, $withoutNamespace);
  70. }
  71. /**
  72. * Parse the docblock of the property to get the class of the param annotation.
  73. *
  74. * @throws AnnotationException
  75. */
  76. public function getReturnClass(ReflectionMethod $method, bool $withoutNamespace = false): array
  77. {
  78. return $this->readReturnClass($method, false, $withoutNamespace);
  79. }
  80. protected function readReturnClass(ReflectionMethod $method, bool $allowPrimitiveTypes, bool $withoutNamespace = false): array
  81. {
  82. // Use reflection
  83. $returnType = $method->getReturnType();
  84. if ($returnType instanceof \ReflectionNamedType) {
  85. if (! $returnType->isBuiltin() || $allowPrimitiveTypes) {
  86. return [($returnType->allowsNull() ? '?' : '') . $returnType->getName()];
  87. }
  88. }
  89. $docComment = $method->getDocComment();
  90. if (! $docComment) {
  91. return ['mixed'];
  92. }
  93. if (preg_match('/@return\s+([^\s]+)\s+/', $docComment, $matches)) {
  94. [, $type] = $matches;
  95. } else {
  96. return ['mixed'];
  97. }
  98. $result = [];
  99. $class = $method->getDeclaringClass();
  100. $types = explode('|', $type);
  101. foreach ($types as $type) {
  102. // Ignore primitive types
  103. if (isset(self::PRIMITIVE_TYPES[$type])) {
  104. if ($allowPrimitiveTypes) {
  105. $result[] = self::PRIMITIVE_TYPES[$type];
  106. }
  107. continue;
  108. }
  109. // Ignore types containing special characters ([], <> ...)
  110. if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
  111. continue;
  112. }
  113. // If the class name is not fully qualified (i.e. doesn't start with a \)
  114. if ($type[0] !== '\\' && ! $withoutNamespace) {
  115. // Try to resolve the FQN using the class context
  116. $resolvedType = $this->tryResolveFqn($type, $class, $method);
  117. if (! $resolvedType && ! $this->ignorePhpDocErrors) {
  118. throw new AnnotationException(sprintf(
  119. 'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
  120. . 'Did you maybe forget to add a "use" statement for this annotation?',
  121. $method,
  122. $class->name,
  123. $method->name,
  124. $type
  125. ));
  126. }
  127. $type = $resolvedType;
  128. }
  129. if (! $this->ignorePhpDocErrors && ! $withoutNamespace && ! $this->classExists($type)) {
  130. throw new AnnotationException(sprintf(
  131. 'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s"',
  132. $method,
  133. $class->name,
  134. $method->name,
  135. $type
  136. ));
  137. }
  138. // Remove the leading \ (FQN shouldn't contain it)
  139. $result[] = is_string($type) ? ltrim($type, '\\') : null;
  140. }
  141. return $result;
  142. }
  143. /**
  144. * Attempts to resolve the FQN of the provided $type based on the $class and $member context.
  145. *
  146. * @return null|string Fully qualified name of the type, or null if it could not be resolved
  147. */
  148. protected function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string
  149. {
  150. $alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
  151. $loweredAlias = strtolower($alias);
  152. // Retrieve "use" statements
  153. $uses = $this->parser->parseUseStatements($class);
  154. if (isset($uses[$loweredAlias])) {
  155. // Imported classes
  156. if ($pos !== false) {
  157. return $uses[$loweredAlias] . substr($type, $pos);
  158. }
  159. return $uses[$loweredAlias];
  160. }
  161. if ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
  162. return $class->getNamespaceName() . '\\' . $type;
  163. }
  164. if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
  165. // Class namespace
  166. return $uses['__NAMESPACE__'] . '\\' . $type;
  167. }
  168. if ($this->classExists($type)) {
  169. // No namespace
  170. return $type;
  171. }
  172. // If all fail, try resolving through related traits
  173. return $this->tryResolveFqnInTraits($type, $class, $member);
  174. }
  175. /**
  176. * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
  177. * through the traits that are used by the provided $class.
  178. *
  179. * @return null|string Fully qualified name of the type, or null if it could not be resolved
  180. */
  181. protected function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string
  182. {
  183. /** @var ReflectionClass[] $traits */
  184. $traits = [];
  185. // Get traits for the class and its parents
  186. while ($class) {
  187. $traits = array_merge($traits, $class->getTraits());
  188. $class = $class->getParentClass();
  189. }
  190. foreach ($traits as $trait) {
  191. // Eliminate traits that don't have the property/method/parameter
  192. if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) {
  193. continue;
  194. }
  195. if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) {
  196. continue;
  197. }
  198. if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) {
  199. continue;
  200. }
  201. // Run the resolver again with the ReflectionClass instance for the trait
  202. $resolvedType = $this->tryResolveFqn($type, $trait, $member);
  203. if ($resolvedType) {
  204. return $resolvedType;
  205. }
  206. }
  207. return null;
  208. }
  209. protected function classExists(string $class): bool
  210. {
  211. return class_exists($class) || interface_exists($class);
  212. }
  213. }