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