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