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 Nette\MemberAccessException; 14 15 16/** 17 * Nette\SmartObject helpers. 18 * @internal 19 */ 20final class ObjectHelpers 21{ 22 use Nette\StaticClass; 23 24 /** 25 * @return never 26 * @throws MemberAccessException 27 */ 28 public static function strictGet(string $class, string $name): void 29 { 30 $rc = new \ReflectionClass($class); 31 $hint = self::getSuggestion(array_merge( 32 array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), 33 self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), 34 ), $name); 35 throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); 36 } 37 38 39 /** 40 * @return never 41 * @throws MemberAccessException 42 */ 43 public static function strictSet(string $class, string $name): void 44 { 45 $rc = new \ReflectionClass($class); 46 $hint = self::getSuggestion(array_merge( 47 array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), 48 self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), 49 ), $name); 50 throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); 51 } 52 53 54 /** 55 * @return never 56 * @throws MemberAccessException 57 */ 58 public static function strictCall(string $class, string $method, array $additionalMethods = []): void 59 { 60 $trace = debug_backtrace(0, 3); // suppose this method is called from __call() 61 $context = ($trace[1]['function'] ?? null) === '__call' 62 ? ($trace[2]['class'] ?? null) 63 : null; 64 65 if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() 66 $class = get_parent_class($context); 67 } 68 69 if (method_exists($class, $method)) { // insufficient visibility 70 $rm = new \ReflectionMethod($class, $method); 71 $visibility = $rm->isPrivate() 72 ? 'private ' 73 : ($rm->isProtected() ? 'protected ' : ''); 74 throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); 75 76 } else { 77 $hint = self::getSuggestion(array_merge( 78 get_class_methods($class), 79 self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:static[ \t]+)?(?:\S+[ \t]+)??(\w+)\(~m'), 80 $additionalMethods, 81 ), $method); 82 throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); 83 } 84 } 85 86 87 /** 88 * @return never 89 * @throws MemberAccessException 90 */ 91 public static function strictStaticCall(string $class, string $method): void 92 { 93 $trace = debug_backtrace(0, 3); // suppose this method is called from __callStatic() 94 $context = ($trace[1]['function'] ?? null) === '__callStatic' 95 ? ($trace[2]['class'] ?? null) 96 : null; 97 98 if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() 99 $class = get_parent_class($context); 100 } 101 102 if (method_exists($class, $method)) { // insufficient visibility 103 $rm = new \ReflectionMethod($class, $method); 104 $visibility = $rm->isPrivate() 105 ? 'private ' 106 : ($rm->isProtected() ? 'protected ' : ''); 107 throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); 108 109 } else { 110 $hint = self::getSuggestion( 111 array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), fn($m) => $m->isStatic()), 112 $method, 113 ); 114 throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); 115 } 116 } 117 118 119 /** 120 * Returns array of magic properties defined by annotation @property. 121 * @return array of [name => bit mask] 122 * @internal 123 */ 124 public static function getMagicProperties(string $class): array 125 { 126 static $cache; 127 $props = &$cache[$class]; 128 if ($props !== null) { 129 return $props; 130 } 131 132 $rc = new \ReflectionClass($class); 133 preg_match_all( 134 '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', 135 (string) $rc->getDocComment(), 136 $matches, 137 PREG_SET_ORDER, 138 ); 139 140 $props = []; 141 foreach ($matches as [, $type, $name]) { 142 $uname = ucfirst($name); 143 $write = $type !== '-read' 144 && $rc->hasMethod($nm = 'set' . $uname) 145 && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); 146 $read = $type !== '-write' 147 && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname)) 148 && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); 149 150 if ($read || $write) { 151 $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3 | ($type === '-deprecated') << 4; 152 } 153 } 154 155 foreach ($rc->getTraits() as $trait) { 156 $props += self::getMagicProperties($trait->name); 157 } 158 159 if ($parent = get_parent_class($class)) { 160 $props += self::getMagicProperties($parent); 161 } 162 163 return $props; 164 } 165 166 167 /** 168 * Finds the best suggestion (for 8-bit encoding). 169 * @param (\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionClass|\ReflectionProperty|string)[] $possibilities 170 * @internal 171 */ 172 public static function getSuggestion(array $possibilities, string $value): ?string 173 { 174 $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '+', $value); 175 $best = null; 176 $min = (strlen($value) / 4 + 1) * 10 + .1; 177 foreach (array_unique($possibilities, SORT_REGULAR) as $item) { 178 $item = $item instanceof \Reflector ? $item->name : $item; 179 if ($item !== $value && ( 180 ($len = levenshtein($item, $value, 10, 11, 10)) < $min 181 || ($len = levenshtein(preg_replace($re, '*', $item), $norm, 10, 11, 10)) < $min 182 )) { 183 $min = $len; 184 $best = $item; 185 } 186 } 187 188 return $best; 189 } 190 191 192 private static function parseFullDoc(\ReflectionClass $rc, string $pattern): array 193 { 194 do { 195 $doc[] = $rc->getDocComment(); 196 $traits = $rc->getTraits(); 197 while ($trait = array_pop($traits)) { 198 $doc[] = $trait->getDocComment(); 199 $traits += $trait->getTraits(); 200 } 201 } while ($rc = $rc->getParentClass()); 202 203 return preg_match_all($pattern, implode('', $doc), $m) ? $m[1] : []; 204 } 205 206 207 /** 208 * Checks if the public non-static property exists. 209 * Returns 'event' if the property exists and has event like name 210 * @internal 211 */ 212 public static function hasProperty(string $class, string $name): bool|string 213 { 214 static $cache; 215 $prop = &$cache[$class][$name]; 216 if ($prop === null) { 217 $prop = false; 218 try { 219 $rp = new \ReflectionProperty($class, $name); 220 if ($rp->isPublic() && !$rp->isStatic()) { 221 $prop = $name >= 'onA' && $name < 'on_' ? 'event' : true; 222 } 223 } catch (\ReflectionException $e) { 224 } 225 } 226 227 return $prop; 228 } 229} 230