1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17:
18: class ObjectMixin
19: {
20:
21: private static $methods;
22:
23:
24: private static $props;
25:
26:
27: private static $extMethods;
28:
29:
30: 31: 32:
33: final public function __construct()
34: {
35: throw new StaticClassException;
36: }
37:
38:
39: 40: 41: 42: 43: 44: 45: 46:
47: public static function call($_this, $name, $args)
48: {
49: $class = get_class($_this);
50: $isProp = self::hasProperty($class, $name);
51: $methods = & self::getMethods($class);
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: Nette\Utils\Callback::invokeArgs($handler, $args);
63: }
64: } elseif ($_this->$name !== NULL) {
65: throw new UnexpectedValueException("Property $class::$$name must be array or NULL, " . gettype($_this->$name) ." given.");
66: }
67:
68: } elseif (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 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 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 Nette\Utils\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 = Nette\Utils\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 = preg_match('#^on[A-Z]#', $name) ? 'event' : TRUE;
233: }
234: } catch (\ReflectionException $e) {}
235: }
236: return $prop;
237: }
238:
239:
240: 241: 242: 243:
244: private static function & getMethods($class)
245: {
246: if (!isset(self::$methods[$class])) {
247: self::$methods[$class] = array_fill_keys(get_class_methods($class), 0) + self::getMagicMethods($class);
248: if ($parent = get_parent_class($class)) {
249: self::$methods[$class] += self::getMethods($parent);
250: }
251: }
252: return self::$methods[$class];
253: }
254:
255:
256: 257: 258: 259:
260: public static function getMagicMethods($class)
261: {
262: $rc = new \ReflectionClass($class);
263: preg_match_all('~^
264: [ \t*]* @method [ \t]+
265: (?: [^\s(]+ [ \t]+ )?
266: (set|get|is|add) ([A-Z]\w*) [ \t]*
267: (?: \( [ \t]* ([^)$\s]+) )?
268: ()~mx', $rc->getDocComment(), $matches, PREG_SET_ORDER);
269:
270: $methods = array();
271: foreach ($matches as $m) {
272: list(, $op, $prop, $type) = $m;
273: $name = $op . $prop;
274: $prop = strtolower($prop[0]) . substr($prop, 1) . ($op === 'add' ? 's' : '');
275: if ($rc->hasProperty($prop) && ($rp = $rc->getProperty($prop)) && !$rp->isStatic()) {
276: $rp->setAccessible(TRUE);
277: if ($op === 'get' || $op === 'is') {
278: $type = NULL; $op = 'get';
279: } elseif (!$type && preg_match('#@var[ \t]+(\S+)' . ($op === 'add' ? '\[\]#' : '#'), $rp->getDocComment(), $m)) {
280: $type = $m[1];
281: }
282: if ($rc->inNamespace() && preg_match('#^[A-Z]\w+(\[|\||\z)#', $type)) {
283: $type = $rc->getNamespaceName() . '\\' . $type;
284: }
285: $methods[$name] = array($op, $rp, $type);
286: }
287: }
288: return $methods;
289: }
290:
291:
292: 293: 294: 295: 296:
297: public static function checkType(& $val, $type)
298: {
299: if (strpos($type, '|') !== FALSE) {
300: $found = NULL;
301: foreach (explode('|', $type) as $type) {
302: $tmp = $val;
303: if (self::checkType($tmp, $type)) {
304: if ($val === $tmp) {
305: return TRUE;
306: }
307: $found[] = $tmp;
308: }
309: }
310: if ($found) {
311: $val = $found[0];
312: return TRUE;
313: }
314: return FALSE;
315:
316: } elseif (substr($type, -2) === '[]') {
317: if (!is_array($val)) {
318: return FALSE;
319: }
320: $type = substr($type, 0, -2);
321: $res = array();
322: foreach ($val as $k => $v) {
323: if (!self::checkType($v, $type)) {
324: return FALSE;
325: }
326: $res[$k] = $v;
327: }
328: $val = $res;
329: return TRUE;
330: }
331:
332: switch (strtolower($type)) {
333: case NULL:
334: case 'mixed':
335: return TRUE;
336: case 'bool':
337: case 'boolean':
338: return ($val === NULL || is_scalar($val)) && settype($val, 'bool');
339: case 'string':
340: return ($val === NULL || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) && settype($val, 'string');
341: case 'int':
342: case 'integer':
343: return ($val === NULL || is_bool($val) || is_numeric($val)) && ((float) (int) $val === (float) $val) && settype($val, 'int');
344: case 'float':
345: return ($val === NULL || is_bool($val) || is_numeric($val)) && settype($val, 'float');
346: case 'scalar':
347: case 'array':
348: case 'object':
349: case 'callable':
350: case 'resource':
351: case 'null':
352: return call_user_func("is_$type", $val);
353: default:
354: return $val instanceof $type;
355: }
356: }
357:
358:
359: 360: 361: 362: 363: 364: 365:
366: public static function setExtensionMethod($class, $name, $callback)
367: {
368: $name = strtolower($name);
369: self::$extMethods[$name][$class] = Nette\Utils\Callback::closure($callback);
370: self::$extMethods[$name][''] = NULL;
371: }
372:
373:
374: 375: 376: 377: 378: 379:
380: public static function getExtensionMethod($class, $name)
381: {
382: $list = & self::$extMethods[strtolower($name)];
383: $cache = & $list[''][$class];
384: if (isset($cache)) {
385: return $cache;
386: }
387:
388: foreach (array($class) + class_parents($class) + class_implements($class) as $cl) {
389: if (isset($list[$cl])) {
390: return $cache = $list[$cl];
391: }
392: }
393: return $cache = FALSE;
394: }
395:
396: }
397: