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