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