1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette,
11: 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: $methods = & self::getMethods($class);
53:
54: if ($name === '') {
55: throw new MemberAccessException("Call to class '$class' method without name.");
56:
57: } elseif ($isProp && $_this->$name instanceof \Closure) {
58: return call_user_func_array($_this->$name, $args);
59:
60: } elseif ($isProp === 'event') {
61: if (is_array($_this->$name) || $_this->$name instanceof \Traversable) {
62: foreach ($_this->$name as $handler) {
63: Callback::invokeArgs($handler, $args);
64: }
65: } elseif ($_this->$name !== NULL) {
66: throw new Nette\UnexpectedValueException("Property $class::$$name must be array or NULL, " . gettype($_this->$name) ." given.");
67: }
68:
69: } elseif (isset($methods[$name]) && is_array($methods[$name])) {
70: list($op, $rp, $type) = $methods[$name];
71: if (count($args) !== ($op === 'get' ? 0 : 1)) {
72: throw new Nette\InvalidArgumentException("$class::$name() expects " . ($op === 'get' ? 'no' : '1') . ' argument, ' . count($args) . ' given.');
73:
74: } elseif ($type && $args && !self::checkType($args[0], $type)) {
75: throw new Nette\InvalidArgumentException("Argument passed to $class::$name() must be $type, " . gettype($args[0]) . ' given.');
76: }
77:
78: if ($op === 'get') {
79: return $rp->getValue($_this);
80: } elseif ($op === 'set') {
81: $rp->setValue($_this, $args[0]);
82: } elseif ($op === 'add') {
83: $val = $rp->getValue($_this);
84: $val[] = $args[0];
85: $rp->setValue($_this, $val);
86: }
87: return $_this;
88:
89: } elseif ($cb = self::getExtensionMethod($class, $name)) {
90: array_unshift($args, $_this);
91: return Callback::invokeArgs($cb, $args);
92:
93: } else {
94: if (method_exists($class, $name)) {
95: $class = 'parent';
96: }
97: throw new MemberAccessException("Call to undefined method $class::$name().");
98: }
99: }
100:
101:
102: 103: 104: 105: 106: 107: 108: 109:
110: public static function callStatic($class, $method, $args)
111: {
112: throw new MemberAccessException("Call to undefined static method $class::$method().");
113: }
114:
115:
116: 117: 118: 119: 120: 121: 122:
123: public static function & get($_this, $name)
124: {
125: $class = get_class($_this);
126: $uname = ucfirst($name);
127: $methods = & self::getMethods($class);
128:
129: if ($name === '') {
130: throw new MemberAccessException("Cannot read a class '$class' property without name.");
131:
132: } elseif (isset($methods[$m = 'get' . $uname]) || isset($methods[$m = 'is' . $uname])) {
133: if ($methods[$m] === 0) {
134: $rm = new \ReflectionMethod($class, $m);
135: $methods[$m] = $rm->returnsReference();
136: }
137: if ($methods[$m] === TRUE) {
138: return $_this->$m();
139: } else {
140: $val = $_this->$m();
141: return $val;
142: }
143:
144: } elseif (isset($methods[$name])) {
145: if (PHP_VERSION_ID >= 50400) {
146: $rm = new \ReflectionMethod($class, $name);
147: $val = $rm->getClosure($_this);
148: } else {
149: $val = Callback::closure($_this, $name);
150: }
151: return $val;
152:
153: } else {
154: $type = isset($methods['set' . $uname]) ? 'a write-only' : 'an undeclared';
155: throw new MemberAccessException("Cannot read $type property $class::\$$name.");
156: }
157: }
158:
159:
160: 161: 162: 163: 164: 165: 166: 167:
168: public static function set($_this, $name, $value)
169: {
170: $class = get_class($_this);
171: $uname = ucfirst($name);
172: $methods = & self::getMethods($class);
173:
174: if ($name === '') {
175: throw new MemberAccessException("Cannot write to a class '$class' property without name.");
176:
177: } elseif (self::hasProperty($class, $name)) {
178: $_this->$name = $value;
179:
180: } elseif (isset($methods[$m = 'set' . $uname])) {
181: $_this->$m($value);
182:
183: } else {
184: $type = isset($methods['get' . $uname]) || isset($methods['is' . $uname])
185: ? 'a read-only' : 'an undeclared';
186: throw new MemberAccessException("Cannot write to $type property $class::\$$name.");
187: }
188: }
189:
190:
191: 192: 193: 194: 195: 196: 197:
198: public static function remove($_this, $name)
199: {
200: $class = get_class($_this);
201: if (!self::hasProperty($class, $name)) {
202: throw new MemberAccessException("Cannot unset the property $class::\$$name.");
203: }
204: }
205:
206:
207: 208: 209: 210: 211: 212:
213: public static function has($_this, $name)
214: {
215: $name = ucfirst($name);
216: $methods = & self::getMethods(get_class($_this));
217: return $name !== '' && (isset($methods['get' . $name]) || isset($methods['is' . $name]));
218: }
219:
220:
221: 222: 223: 224:
225: private static function hasProperty($class, $name)
226: {
227: $prop = & self::$props[$class][$name];
228: if ($prop === NULL) {
229: $prop = FALSE;
230: try {
231: $rp = new \ReflectionProperty($class, $name);
232: if ($rp->isPublic() && !$rp->isStatic()) {
233: $prop = preg_match('#^on[A-Z]#', $name) ? 'event' : TRUE;
234: }
235: } catch (\ReflectionException $e) {}
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; $op = 'get';
280: } elseif (!$type && preg_match('#@var[ \t]+(\S+)' . ($op === 'add' ? '\[\]#' : '#'), $rp->getDocComment(), $m)) {
281: $type = $m[1];
282: }
283: if ($rc->inNamespace() && preg_match('#^[A-Z]\w+(\[|\||\z)#', $type)) {
284: $type = $rc->getNamespaceName() . '\\' . $type;
285: }
286: $methods[$name] = array($op, $rp, $type);
287: }
288: }
289: return $methods;
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] = 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: