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