PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / utils / src / Utils / Reflection.php

Reflection.php in Packeta trunk, at deps/nette/utils/src/Utils/Reflection.php

325 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7 declare (strict_types=1);
8 namespace Packetery\Nette\Utils;
9
10 use Packetery\Nette;
11 /**
12 * PHP reflection helpers.
13 */
14 final class Reflection
15 {
16 use \Packetery\Nette\StaticClass;
17 private const BuiltinTypes = ['string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1, 'never' => 1];
18 private const ClassKeywords = ['self' => 1, 'parent' => 1, 'static' => 1];
19 /**
20 * Determines if type is PHP built-in type. Otherwise, it is the class name.
21 */
22 public static function isBuiltinType(string $type) : bool
23 {
24 return isset(self::BuiltinTypes[\strtolower($type)]);
25 }
26 /**
27 * Determines if type is special class name self/parent/static.
28 */
29 public static function isClassKeyword(string $name) : bool
30 {
31 return isset(self::ClassKeywords[\strtolower($name)]);
32 }
33 /**
34 * Returns the type of return value of given function or method and normalizes `self`, `static`, and `parent` to actual class names.
35 * If the function does not have a return type, it returns null.
36 * If the function has union or intersection type, it throws \Packetery\Nette\InvalidStateException.
37 */
38 public static function getReturnType(\ReflectionFunctionAbstract $func) : ?string
39 {
40 $type = $func->getReturnType() ?? (\PHP_VERSION_ID >= 80100 && $func instanceof \ReflectionMethod ? $func->getTentativeReturnType() : null);
41 return self::getType($func, $type);
42 }
43 /**
44 * @deprecated
45 */
46 public static function getReturnTypes(\ReflectionFunctionAbstract $func) : array
47 {
48 $type = Type::fromReflection($func);
49 return $type ? $type->getNames() : [];
50 }
51 /**
52 * Returns the type of given parameter and normalizes `self` and `parent` to the actual class names.
53 * If the parameter does not have a type, it returns null.
54 * If the parameter has union or intersection type, it throws \Packetery\Nette\InvalidStateException.
55 */
56 public static function getParameterType(\ReflectionParameter $param) : ?string
57 {
58 return self::getType($param, $param->getType());
59 }
60 /**
61 * @deprecated
62 */
63 public static function getParameterTypes(\ReflectionParameter $param) : array
64 {
65 $type = Type::fromReflection($param);
66 return $type ? $type->getNames() : [];
67 }
68 /**
69 * Returns the type of given property and normalizes `self` and `parent` to the actual class names.
70 * If the property does not have a type, it returns null.
71 * If the property has union or intersection type, it throws \Packetery\Nette\InvalidStateException.
72 */
73 public static function getPropertyType(\ReflectionProperty $prop) : ?string
74 {
75 return self::getType($prop, \PHP_VERSION_ID >= 70400 ? $prop->getType() : null);
76 }
77 /**
78 * @deprecated
79 */
80 public static function getPropertyTypes(\ReflectionProperty $prop) : array
81 {
82 $type = Type::fromReflection($prop);
83 return $type ? $type->getNames() : [];
84 }
85 /**
86 * @param \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflection
87 */
88 private static function getType($reflection, ?\ReflectionType $type) : ?string
89 {
90 if ($type === null) {
91 return null;
92 } elseif ($type instanceof \ReflectionNamedType) {
93 return Type::resolve($type->getName(), $reflection);
94 } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
95 throw new \Packetery\Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union or intersection type.');
96 } else {
97 throw new \Packetery\Nette\InvalidStateException('Unexpected type of ' . self::toString($reflection));
98 }
99 }
100 /**
101 * Returns the default value of parameter. If it is a constant, it returns its value.
102 * @return mixed
103 * @throws \ReflectionException If the parameter does not have a default value or the constant cannot be resolved
104 */
105 public static function getParameterDefaultValue(\ReflectionParameter $param)
106 {
107 if ($param->isDefaultValueConstant()) {
108 $const = $orig = $param->getDefaultValueConstantName();
109 $pair = \explode('::', $const);
110 if (isset($pair[1])) {
111 $pair[0] = Type::resolve($pair[0], $param);
112 try {
113 $rcc = new \ReflectionClassConstant($pair[0], $pair[1]);
114 } catch (\ReflectionException $e) {
115 $name = self::toString($param);
116 throw new \ReflectionException("Unable to resolve constant {$orig} used as default value of {$name}.", 0, $e);
117 }
118 return $rcc->getValue();
119 } elseif (!\defined($const)) {
120 $const = \substr((string) \strrchr($const, '\\'), 1);
121 if (!\defined($const)) {
122 $name = self::toString($param);
123 throw new \ReflectionException("Unable to resolve constant {$orig} used as default value of {$name}.");
124 }
125 }
126 return \constant($const);
127 }
128 return $param->getDefaultValue();
129 }
130 /**
131 * Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait.
132 */
133 public static function getPropertyDeclaringClass(\ReflectionProperty $prop) : \ReflectionClass
134 {
135 foreach ($prop->getDeclaringClass()->getTraits() as $trait) {
136 if ($trait->hasProperty($prop->name) && $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment()) {
137 return self::getPropertyDeclaringClass($trait->getProperty($prop->name));
138 }
139 }
140 return $prop->getDeclaringClass();
141 }
142 /**
143 * Returns a reflection of a method that contains a declaration of $method.
144 * Usually, each method is its own declaration, but the body of the method can also be in the trait and under a different name.
145 */
146 public static function getMethodDeclaringMethod(\ReflectionMethod $method) : \ReflectionMethod
147 {
148 // file & line guessing as workaround for insufficient PHP reflection
149 $decl = $method->getDeclaringClass();
150 if ($decl->getFileName() === $method->getFileName() && $decl->getStartLine() <= $method->getStartLine() && $decl->getEndLine() >= $method->getEndLine()) {
151 return $method;
152 }
153 $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()];
154 if (($alias = $decl->getTraitAliases()[$method->name] ?? null) && ($m = new \ReflectionMethod($alias)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]) {
155 return self::getMethodDeclaringMethod($m);
156 }
157 foreach ($decl->getTraits() as $trait) {
158 if ($trait->hasMethod($method->name) && ($m = $trait->getMethod($method->name)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]) {
159 return self::getMethodDeclaringMethod($m);
160 }
161 }
162 return $method;
163 }
164 /**
165 * Finds out if reflection has access to PHPdoc comments. Comments may not be available due to the opcode cache.
166 */
167 public static function areCommentsAvailable() : bool
168 {
169 static $res;
170 return $res ?? ($res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment());
171 }
172 public static function toString(\Reflector $ref) : string
173 {
174 if ($ref instanceof \ReflectionClass) {
175 return $ref->name;
176 } elseif ($ref instanceof \ReflectionMethod) {
177 return $ref->getDeclaringClass()->name . '::' . $ref->name . '()';
178 } elseif ($ref instanceof \ReflectionFunction) {
179 return $ref->name . '()';
180 } elseif ($ref instanceof \ReflectionProperty) {
181 return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name;
182 } elseif ($ref instanceof \ReflectionParameter) {
183 return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction());
184 } else {
185 throw new \Packetery\Nette\InvalidArgumentException();
186 }
187 }
188 /**
189 * Expands the name of the class to full name in the given context of given class.
190 * Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context.
191 * @throws \Packetery\Nette\InvalidArgumentException
192 */
193 public static function expandClassName(string $name, \ReflectionClass $context) : string
194 {
195 $lower = \strtolower($name);
196 if (empty($name)) {
197 throw new \Packetery\Nette\InvalidArgumentException('Class name must not be empty.');
198 } elseif (isset(self::BuiltinTypes[$lower])) {
199 return $lower;
200 } elseif ($lower === 'self' || $lower === 'static') {
201 return $context->name;
202 } elseif ($lower === 'parent') {
203 return $context->getParentClass() ? $context->getParentClass()->name : 'parent';
204 } elseif ($name[0] === '\\') {
205 // fully qualified name
206 return \ltrim($name, '\\');
207 }
208 $uses = self::getUseStatements($context);
209 $parts = \explode('\\', $name, 2);
210 if (isset($uses[$parts[0]])) {
211 $parts[0] = $uses[$parts[0]];
212 return \implode('\\', $parts);
213 } elseif ($context->inNamespace()) {
214 return $context->getNamespaceName() . '\\' . $name;
215 } else {
216 return $name;
217 }
218 }
219 /** @return array of [alias => class] */
220 public static function getUseStatements(\ReflectionClass $class) : array
221 {
222 if ($class->isAnonymous()) {
223 throw new \Packetery\Nette\NotImplementedException('Anonymous classes are not supported.');
224 }
225 static $cache = [];
226 if (!isset($cache[$name = $class->name])) {
227 if ($class->isInternal()) {
228 $cache[$name] = [];
229 } else {
230 $code = \file_get_contents($class->getFileName());
231 $cache = self::parseUseStatements($code, $name) + $cache;
232 }
233 }
234 return $cache[$name];
235 }
236 /**
237 * Parses PHP code to [class => [alias => class, ...]]
238 */
239 private static function parseUseStatements(string $code, ?string $forClass = null) : array
240 {
241 try {
242 $tokens = \token_get_all($code, \TOKEN_PARSE);
243 } catch (\ParseError $e) {
244 \trigger_error($e->getMessage(), \E_USER_NOTICE);
245 $tokens = [];
246 }
247 $namespace = $class = $classLevel = $level = null;
248 $res = $uses = [];
249 $nameTokens = \PHP_VERSION_ID < 80000 ? [\T_STRING, \T_NS_SEPARATOR] : [\T_STRING, \T_NS_SEPARATOR, \T_NAME_QUALIFIED, \T_NAME_FULLY_QUALIFIED];
250 while ($token = \current($tokens)) {
251 \next($tokens);
252 switch (\is_array($token) ? $token[0] : $token) {
253 case \T_NAMESPACE:
254 $namespace = \ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\');
255 $uses = [];
256 break;
257 case \T_CLASS:
258 case \T_INTERFACE:
259 case \T_TRAIT:
260 case \PHP_VERSION_ID < 80100 ? \T_CLASS : \T_ENUM:
261 if ($name = self::fetch($tokens, \T_STRING)) {
262 $class = $namespace . $name;
263 $classLevel = $level + 1;
264 $res[$class] = $uses;
265 if ($class === $forClass) {
266 return $res;
267 }
268 }
269 break;
270 case \T_USE:
271 while (!$class && ($name = self::fetch($tokens, $nameTokens))) {
272 $name = \ltrim($name, '\\');
273 if (self::fetch($tokens, '{')) {
274 while ($suffix = self::fetch($tokens, $nameTokens)) {
275 if (self::fetch($tokens, \T_AS)) {
276 $uses[self::fetch($tokens, \T_STRING)] = $name . $suffix;
277 } else {
278 $tmp = \explode('\\', $suffix);
279 $uses[\end($tmp)] = $name . $suffix;
280 }
281 if (!self::fetch($tokens, ',')) {
282 break;
283 }
284 }
285 } elseif (self::fetch($tokens, \T_AS)) {
286 $uses[self::fetch($tokens, \T_STRING)] = $name;
287 } else {
288 $tmp = \explode('\\', $name);
289 $uses[\end($tmp)] = $name;
290 }
291 if (!self::fetch($tokens, ',')) {
292 break;
293 }
294 }
295 break;
296 case \T_CURLY_OPEN:
297 case \T_DOLLAR_OPEN_CURLY_BRACES:
298 case '{':
299 $level++;
300 break;
301 case '}':
302 if ($level === $classLevel) {
303 $class = $classLevel = null;
304 }
305 $level--;
306 }
307 }
308 return $res;
309 }
310 private static function fetch(array &$tokens, $take) : ?string
311 {
312 $res = null;
313 while ($token = \current($tokens)) {
314 [$token, $s] = \is_array($token) ? $token : [$token, $token];
315 if (\in_array($token, (array) $take, \true)) {
316 $res .= $s;
317 } elseif (!\in_array($token, [\T_DOC_COMMENT, \T_WHITESPACE, \T_COMMENT], \true)) {
318 break;
319 }
320 \next($tokens);
321 }
322 return $res;
323 }
324 }
325