PhpDocReader.php 8.0 KB

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