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:
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: $val = Callback::closure($_this, $name);
145: return $val;
146:
147: } else {
148: $type = isset($methods['set' . $uname]) ? 'a write-only' : 'an undeclared';
149: throw new MemberAccessException("Cannot read $type property $class::\$$name.");
150: }
151: }
152:
153:
154: 155: 156: 157: 158: 159: 160: 161:
162: public static function set($_this, $name, $value)
163: {
164: $class = get_class($_this);
165: $uname = ucfirst($name);
166: $methods = & self::getMethods($class);
167:
168: if ($name === '') {
169: throw new MemberAccessException("Cannot write to a class '$class' property without name.");
170:
171: } elseif (self::hasProperty($class, $name)) {
172: $_this->$name = $value;
173:
174: } elseif (isset($methods[$m = 'set' . $uname])) {
175: $_this->$m($value);
176:
177: } else {
178: $type = isset($methods['get' . $uname]) || isset($methods['is' . $uname])
179: ? 'a read-only' : 'an undeclared';
180: throw new MemberAccessException("Cannot write to $type property $class::\$$name.");
181: }
182: }
183:
184:
185: 186: 187: 188: 189: 190: 191:
192: public static function remove($_this, $name)
193: {
194: $class = get_class($_this);
195: if (!self::hasProperty($class, $name)) {
196: throw new MemberAccessException("Cannot unset the property $class::\$$name.");
197: }
198: }
199:
200:
201: 202: 203: 204: 205: 206:
207: public static function has($_this, $name)
208: {
209: $name = ucfirst($name);
210: $methods = & self::getMethods(get_class($_this));
211: return $name !== '' && (isset($methods['get' . $name]) || isset($methods['is' . $name]));
212: }
213:
214:
215: 216: 217: 218:
219: private static function hasProperty($class, $name)
220: {
221: $prop = & self::$props[$class][$name];
222: if ($prop === NULL) {
223: $prop = FALSE;
224: try {
225: $rp = new \ReflectionProperty($class, $name);
226: if ($rp->isPublic() && !$rp->isStatic()) {
227: $prop = $name >= 'onA' && $name < 'on_' ? 'event' : TRUE;
228: }
229: } catch (\ReflectionException $e) {}
230: }
231: return $prop;
232: }
233:
234:
235: 236: 237: 238:
239: private static function & getMethods($class)
240: {
241: if (!isset(self::$methods[$class])) {
242: self::$methods[$class] = array_fill_keys(get_class_methods($class), 0) + self::getMagicMethods($class);
243: if ($parent = get_parent_class($class)) {
244: self::$methods[$class] += self::getMethods($parent);
245: }
246: }
247: return self::$methods[$class];
248: }
249:
250:
251: 252: 253: 254:
255: public static function getMagicMethods($class)
256: {
257: $rc = new \ReflectionClass($class);
258: preg_match_all('~^
259: [ \t*]* @method [ \t]+
260: (?: [^\s(]+ [ \t]+ )?
261: (set|get|is|add) ([A-Z]\w*) [ \t]*
262: (?: \( [ \t]* ([^)$\s]+) )?
263: ()~mx', $rc->getDocComment(), $matches, PREG_SET_ORDER);
264:
265: $methods = array();
266: foreach ($matches as $m) {
267: list(, $op, $prop, $type) = $m;
268: $name = $op . $prop;
269: $prop = strtolower($prop[0]) . substr($prop, 1) . ($op === 'add' ? 's' : '');
270: if ($rc->hasProperty($prop) && ($rp = $rc->getProperty($prop)) && !$rp->isStatic()) {
271: $rp->setAccessible(TRUE);
272: if ($op === 'get' || $op === 'is') {
273: $type = NULL; $op = 'get';
274: } elseif (!$type && preg_match('#@var[ \t]+(\S+)' . ($op === 'add' ? '\[\]#' : '#'), $rp->getDocComment(), $m)) {
275: $type = $m[1];
276: }
277: if ($rc->inNamespace() && preg_match('#^[A-Z]\w+(\[|\||\z)#', $type)) {
278: $type = $rc->getNamespaceName() . '\\' . $type;
279: }
280: $methods[$name] = array($op, $rp, $type);
281: }
282: }
283: return $methods;
284: }
285:
286:
287: 288: 289: 290: 291:
292: public static function checkType(& $val, $type)
293: {
294: if (strpos($type, '|') !== FALSE) {
295: $found = NULL;
296: foreach (explode('|', $type) as $type) {
297: $tmp = $val;
298: if (self::checkType($tmp, $type)) {
299: if ($val === $tmp) {
300: return TRUE;
301: }
302: $found[] = $tmp;
303: }
304: }
305: if ($found) {
306: $val = $found[0];
307: return TRUE;
308: }
309: return FALSE;
310:
311: } elseif (substr($type, -2) === '[]') {
312: if (!is_array($val)) {
313: return FALSE;
314: }
315: $type = substr($type, 0, -2);
316: $res = array();
317: foreach ($val as $k => $v) {
318: if (!self::checkType($v, $type)) {
319: return FALSE;
320: }
321: $res[$k] = $v;
322: }
323: $val = $res;
324: return TRUE;
325: }
326:
327: switch (strtolower($type)) {
328: case NULL:
329: case 'mixed':
330: return TRUE;
331: case 'bool':
332: case 'boolean':
333: return ($val === NULL || is_scalar($val)) && settype($val, 'bool');
334: case 'string':
335: return ($val === NULL || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) && settype($val, 'string');
336: case 'int':
337: case 'integer':
338: return ($val === NULL || is_bool($val) || is_numeric($val)) && ((float) (int) $val === (float) $val) && settype($val, 'int');
339: case 'float':
340: return ($val === NULL || is_bool($val) || is_numeric($val)) && settype($val, 'float');
341: case 'scalar':
342: case 'array':
343: case 'object':
344: case 'callable':
345: case 'resource':
346: case 'null':
347: return call_user_func("is_$type", $val);
348: default:
349: return $val instanceof $type;
350: }
351: }
352:
353:
354: 355: 356: 357: 358: 359: 360:
361: public static function setExtensionMethod($class, $name, $callback)
362: {
363: $name = strtolower($name);
364: self::$extMethods[$name][$class] = Callback::check($callback);
365: self::$extMethods[$name][''] = NULL;
366: }
367:
368:
369: 370: 371: 372: 373: 374:
375: public static function getExtensionMethod($class, $name)
376: {
377: $list = & self::$extMethods[strtolower($name)];
378: $cache = & $list[''][$class];
379: if (isset($cache)) {
380: return $cache;
381: }
382:
383: foreach (array($class) + class_parents($class) + class_implements($class) as $cl) {
384: if (isset($list[$cl])) {
385: return $cache = $list[$cl];
386: }
387: }
388: return $cache = FALSE;
389: }
390:
391: }
392: