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