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