PluginProbe
Packeta / 1.6.2
Packeta v1.6.2
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 1.6.2, at deps/nette/utils/src/Utils/Reflection.php

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