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