Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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