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