1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette;
11: use Nette\MemberAccessException;
12:
13:
14: 15: 16:
17: class ObjectMixin
18: {
19: use Nette\StaticClass;
20:
21:
22: private static $extMethods = [];
23:
24:
25:
26:
27:
28: 29: 30:
31: public static function strictGet($class, $name)
32: {
33: $rc = new \ReflectionClass($class);
34: $hint = self::getSuggestion(array_merge(
35: array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }),
36: self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m')
37: ), $name);
38: throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
39: }
40:
41:
42: 43: 44:
45: public static function strictSet($class, $name)
46: {
47: $rc = new \ReflectionClass($class);
48: $hint = self::getSuggestion(array_merge(
49: array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$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: 58:
59: public static function strictCall($class, $method, $additionalMethods = [])
60: {
61: $hint = self::getSuggestion(array_merge(
62: get_class_methods($class),
63: self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:\S+[ \t]+)??(\w+)\(~m'),
64: $additionalMethods
65: ), $method);
66:
67: if (method_exists($class, $method)) {
68: $class = 'parent';
69: }
70: throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
71: }
72:
73:
74: 75: 76:
77: public static function strictStaticCall($class, $method)
78: {
79: $hint = self::getSuggestion(
80: array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }),
81: $method
82: );
83: throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
84: }
85:
86:
87:
88:
89:
90: 91: 92: 93: 94: 95: 96: 97:
98: public static function call($_this, $name, $args)
99: {
100: $class = get_class($_this);
101: $isProp = self::hasProperty($class, $name);
102:
103: if ($name === '') {
104: throw new MemberAccessException("Call to class '$class' method without name.");
105:
106: } elseif ($isProp === 'event') {
107: if (is_array($_this->$name) || $_this->$name instanceof \Traversable) {
108: foreach ($_this->$name as $handler) {
109: Callback::invokeArgs($handler, $args);
110: }
111: } elseif ($_this->$name !== null) {
112: throw new Nette\UnexpectedValueException("Property $class::$$name must be array or null, " . gettype($_this->$name) . ' given.');
113: }
114:
115: } elseif ($isProp && $_this->$name instanceof \Closure) {
116: return call_user_func_array($_this->$name, $args);
117:
118: } elseif (($methods = &self::getMethods($class)) && isset($methods[$name]) && is_array($methods[$name])) {
119: list($op, $rp, $type) = $methods[$name];
120: if (count($args) !== ($op === 'get' ? 0 : 1)) {
121: throw new Nette\InvalidArgumentException("$class::$name() expects " . ($op === 'get' ? 'no' : '1') . ' argument, ' . count($args) . ' given.');
122:
123: } elseif ($type && $args && !self::checkType($args[0], $type)) {
124: throw new Nette\InvalidArgumentException("Argument passed to $class::$name() must be $type, " . gettype($args[0]) . ' given.');
125: }
126:
127: if ($op === 'get') {
128: return $rp->getValue($_this);
129: } elseif ($op === 'set') {
130: $rp->setValue($_this, $args[0]);
131: } elseif ($op === 'add') {
132: $val = $rp->getValue($_this);
133: $val[] = $args[0];
134: $rp->setValue($_this, $val);
135: }
136: return $_this;
137:
138: } elseif ($cb = self::getExtensionMethod($class, $name)) {
139: return Callback::invoke($cb, $_this, ...$args);
140:
141: } else {
142: self::strictCall($class, $name, array_keys(self::getExtensionMethods($class)));
143: }
144: }
145:
146:
147: 148: 149: 150: 151: 152: 153: 154:
155: public static function callStatic($class, $method, $args)
156: {
157: self::strictStaticCall($class, $method);
158: }
159:
160:
161: 162: 163: 164: 165: 166: 167:
168: public static function &get($_this, $name)
169: {
170: $class = get_class($_this);
171: $uname = ucfirst($name);
172: $methods = &self::getMethods($class);
173:
174: if ($name === '') {
175: throw new MemberAccessException("Cannot read a class '$class' property without name.");
176:
177: } elseif (isset($methods[$m = 'get' . $uname]) || isset($methods[$m = 'is' . $uname])) {
178: if ($methods[$m] === 0) {
179: $methods[$m] = (new \ReflectionMethod($class, $m))->returnsReference();
180: }
181: if ($methods[$m] === true) {
182: return $_this->$m();
183: } else {
184: $val = $_this->$m();
185: return $val;
186: }
187:
188: } elseif (isset($methods[$name])) {
189: if (preg_match('#^(is|get|has)([A-Z]|$)#', $name) && !(new \ReflectionMethod($class, $name))->getNumberOfRequiredParameters()) {
190: trigger_error("Did you forget parentheses after $name" . self::getSource() . '?', E_USER_WARNING);
191: }
192: $val = Callback::closure($_this, $name);
193: return $val;
194:
195: } elseif (isset($methods['set' . $uname])) {
196: throw new MemberAccessException("Cannot read a write-only property $class::\$$name.");
197:
198: } else {
199: self::strictGet($class, $name);
200: }
201: }
202:
203:
204: 205: 206: 207: 208: 209: 210: 211:
212: public static function set($_this, $name, $value)
213: {
214: $class = get_class($_this);
215: $uname = ucfirst($name);
216: $methods = &self::getMethods($class);
217:
218: if ($name === '') {
219: throw new MemberAccessException("Cannot write to a class '$class' property without name.");
220:
221: } elseif (self::hasProperty($class, $name)) {
222: $_this->$name = $value;
223:
224: } elseif (isset($methods[$m = 'set' . $uname])) {
225: $_this->$m($value);
226:
227: } elseif (isset($methods['get' . $uname]) || isset($methods['is' . $uname])) {
228: throw new MemberAccessException("Cannot write to a read-only property $class::\$$name.");
229:
230: } else {
231: self::strictSet($class, $name);
232: }
233: }
234:
235:
236: 237: 238: 239: 240: 241: 242:
243: public static function remove($_this, $name)
244: {
245: $class = get_class($_this);
246: if (!self::hasProperty($class, $name)) {
247: throw new MemberAccessException("Cannot unset the property $class::\$$name.");
248: }
249: }
250:
251:
252: 253: 254: 255: 256: 257:
258: public static function has($_this, $name)
259: {
260: $name = ucfirst($name);
261: $methods = &self::getMethods(get_class($_this));
262: return $name !== '' && (isset($methods['get' . $name]) || isset($methods['is' . $name]));
263: }
264:
265:
266:
267:
268:
269: 270: 271: 272:
273: public static function getMagicProperties($class)
274: {
275: static $cache;
276: $props = &$cache[$class];
277: if ($props !== null) {
278: return $props;
279: }
280:
281: $rc = new \ReflectionClass($class);
282: preg_match_all(
283: '~^ [ \t*]* @property(|-read|-write) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx',
284: (string) $rc->getDocComment(), $matches, PREG_SET_ORDER
285: );
286:
287: $props = [];
288: foreach ($matches as list(, $type, $name)) {
289: $uname = ucfirst($name);
290: $write = $type !== '-read'
291: && $rc->hasMethod($nm = 'set' . $uname)
292: && ($rm = $rc->getMethod($nm)) && $rm->getName() === $nm && !$rm->isPrivate() && !$rm->isStatic();
293: $read = $type !== '-write'
294: && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname))
295: && ($rm = $rc->getMethod($nm)) && $rm->getName() === $nm && !$rm->isPrivate() && !$rm->isStatic();
296:
297: if ($read || $write) {
298: $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3;
299: }
300: }
301:
302: foreach ($rc->getTraits() as $trait) {
303: $props += self::getMagicProperties($trait->getName());
304: }
305:
306: if ($parent = get_parent_class($class)) {
307: $props += self::getMagicProperties($parent);
308: }
309: return $props;
310: }
311:
312:
313:
314: public static function getMagicProperty($class, $name)
315: {
316: $props = self::getMagicProperties($class);
317: return isset($props[$name]) ? $props[$name] : null;
318: }
319:
320:
321:
322:
323:
324: 325: 326: 327:
328: public static function getMagicMethods($class)
329: {
330: $rc = new \ReflectionClass($class);
331: preg_match_all('~^
332: [ \t*]* @method [ \t]+
333: (?: [^\s(]+ [ \t]+ )?
334: (set|get|is|add) ([A-Z]\w*)
335: (?: ([ \t]* \() [ \t]* ([^)$\s]*) )?
336: ()~mx', (string) $rc->getDocComment(), $matches, PREG_SET_ORDER);
337:
338: $methods = [];
339: foreach ($matches as list(, $op, $prop, $bracket, $type)) {
340: if ($bracket !== '(') {
341: trigger_error("Bracket must be immediately after @method $op$prop() in class $class.", E_USER_WARNING);
342: }
343: $name = $op . $prop;
344: $prop = strtolower($prop[0]) . substr($prop, 1) . ($op === 'add' ? 's' : '');
345: if ($rc->hasProperty($prop) && ($rp = $rc->getProperty($prop)) && !$rp->isStatic()) {
346: $rp->setAccessible(true);
347: if ($op === 'get' || $op === 'is') {
348: $type = null;
349: $op = 'get';
350: } elseif (!$type && preg_match('#@var[ \t]+(\S+)' . ($op === 'add' ? '\[\]#' : '#'), (string) $rp->getDocComment(), $m)) {
351: $type = $m[1];
352: }
353: if ($rc->inNamespace() && preg_match('#^[A-Z]\w+(\[|\||\z)#', (string) $type)) {
354: $type = $rc->getNamespaceName() . '\\' . $type;
355: }
356: $methods[$name] = [$op, $rp, $type];
357: }
358: }
359: return $methods;
360: }
361:
362:
363: 364: 365: 366: 367:
368: public static function checkType(&$val, $type)
369: {
370: if (strpos($type, '|') !== false) {
371: $found = null;
372: foreach (explode('|', $type) as $type) {
373: $tmp = $val;
374: if (self::checkType($tmp, $type)) {
375: if ($val === $tmp) {
376: return true;
377: }
378: $found[] = $tmp;
379: }
380: }
381: if ($found) {
382: $val = $found[0];
383: return true;
384: }
385: return false;
386:
387: } elseif (substr($type, -2) === '[]') {
388: if (!is_array($val)) {
389: return false;
390: }
391: $type = substr($type, 0, -2);
392: $res = [];
393: foreach ($val as $k => $v) {
394: if (!self::checkType($v, $type)) {
395: return false;
396: }
397: $res[$k] = $v;
398: }
399: $val = $res;
400: return true;
401: }
402:
403: switch (strtolower($type)) {
404: case null:
405: case 'mixed':
406: return true;
407: case 'bool':
408: case 'boolean':
409: return ($val === null || is_scalar($val)) && settype($val, 'bool');
410: case 'string':
411: return ($val === null || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) && settype($val, 'string');
412: case 'int':
413: case 'integer':
414: return ($val === null || is_bool($val) || is_numeric($val)) && ((float) (int) $val === (float) $val) && settype($val, 'int');
415: case 'float':
416: return ($val === null || is_bool($val) || is_numeric($val)) && settype($val, 'float');
417: case 'scalar':
418: case 'array':
419: case 'object':
420: case 'callable':
421: case 'resource':
422: case 'null':
423: return call_user_func("is_$type", $val);
424: default:
425: return $val instanceof $type;
426: }
427: }
428:
429:
430:
431:
432:
433: 434: 435: 436: 437: 438: 439:
440: public static function setExtensionMethod($class, $name, $callback)
441: {
442: $name = strtolower($name);
443: self::$extMethods[$name][$class] = Callback::check($callback);
444: self::$extMethods[$name][''] = null;
445: }
446:
447:
448: 449: 450: 451: 452: 453:
454: public static function getExtensionMethod($class, $name)
455: {
456: $list = &self::$extMethods[strtolower($name)];
457: $cache = &$list[''][$class];
458: if (isset($cache)) {
459: return $cache;
460: }
461:
462: foreach ([$class] + class_parents($class) + class_implements($class) as $cl) {
463: if (isset($list[$cl])) {
464: return $cache = $list[$cl];
465: }
466: }
467: return $cache = false;
468: }
469:
470:
471: 472: 473: 474: 475:
476: public static function getExtensionMethods($class)
477: {
478: $res = [];
479: foreach (array_keys(self::$extMethods) as $name) {
480: if ($cb = self::getExtensionMethod($class, $name)) {
481: $res[$name] = $cb;
482: }
483: }
484: return $res;
485: }
486:
487:
488:
489:
490:
491: 492: 493: 494: 495:
496: public static function getSuggestion(array $possibilities, $value)
497: {
498: $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '', $value);
499: $best = null;
500: $min = (strlen($value) / 4 + 1) * 10 + .1;
501: foreach (array_unique($possibilities, SORT_REGULAR) as $item) {
502: $item = $item instanceof \Reflector ? $item->getName() : $item;
503: if ($item !== $value && (
504: ($len = levenshtein($item, $value, 10, 11, 10)) < $min
505: || ($len = levenshtein(preg_replace($re, '', $item), $norm, 10, 11, 10) + 20) < $min
506: )) {
507: $min = $len;
508: $best = $item;
509: }
510: }
511: return $best;
512: }
513:
514:
515: private static function parseFullDoc(\ReflectionClass $rc, $pattern)
516: {
517: do {
518: $doc[] = $rc->getDocComment();
519: $traits = $rc->getTraits();
520: while ($trait = array_pop($traits)) {
521: $doc[] = $trait->getDocComment();
522: $traits += $trait->getTraits();
523: }
524: } while ($rc = $rc->getParentClass());
525: return preg_match_all($pattern, implode($doc), $m) ? $m[1] : [];
526: }
527:
528:
529: 530: 531: 532: 533:
534: public static function hasProperty($class, $name)
535: {
536: static $cache;
537: $prop = &$cache[$class][$name];
538: if ($prop === null) {
539: $prop = false;
540: try {
541: $rp = new \ReflectionProperty($class, $name);
542: if ($rp->isPublic() && !$rp->isStatic()) {
543: $prop = $name >= 'onA' && $name < 'on_' ? 'event' : true;
544: }
545: } catch (\ReflectionException $e) {
546: }
547: }
548: return $prop;
549: }
550:
551:
552: 553: 554: 555: 556:
557: public static function &getMethods($class)
558: {
559: static $cache;
560: if (!isset($cache[$class])) {
561: $cache[$class] = array_fill_keys(get_class_methods($class), 0) + self::getMagicMethods($class);
562: if ($parent = get_parent_class($class)) {
563: $cache[$class] += self::getMethods($parent);
564: }
565: }
566: return $cache[$class];
567: }
568:
569:
570:
571: public static function getSource()
572: {
573: foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
574: if (isset($item['file']) && dirname($item['file']) !== __DIR__) {
575: return " in $item[file]:$item[line]";
576: }
577: }
578: }
579: }
580: