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: if (method_exists($class, $name)) {
92: $class = 'parent';
93: }
94: throw new MemberAccessException("Call to undefined method $class::$name().");
95: }
96: }
97:
98:
99: 100: 101: 102: 103: 104: 105: 106:
107: public static function callStatic($class, $method, $args)
108: {
109: throw new MemberAccessException("Call to undefined static method $class::$method().");
110: }
111:
112:
113: 114: 115: 116: 117: 118: 119:
120: public static function & get($_this, $name)
121: {
122: $class = get_class($_this);
123: $uname = ucfirst($name);
124: $methods = & self::getMethods($class);
125:
126: if ($name === '') {
127: throw new MemberAccessException("Cannot read a class '$class' property without name.");
128:
129: } elseif (isset($methods[$m = 'get' . $uname]) || isset($methods[$m = 'is' . $uname])) {
130: if ($methods[$m] === 0) {
131: $rm = new \ReflectionMethod($class, $m);
132: $methods[$m] = $rm->returnsReference();
133: }
134: if ($methods[$m] === TRUE) {
135: return $_this->$m();
136: } else {
137: $val = $_this->$m();
138: return $val;
139: }
140:
141: } elseif (isset($methods[$name])) {
142: $val = Callback::closure($_this, $name);
143: return $val;
144:
145: } else {
146: $type = isset($methods['set' . $uname]) ? 'a write-only' : 'an undeclared';
147: throw new MemberAccessException("Cannot read $type property $class::\$$name.");
148: }
149: }
150:
151:
152: 153: 154: 155: 156: 157: 158: 159:
160: public static function set($_this, $name, $value)
161: {
162: $class = get_class($_this);
163: $uname = ucfirst($name);
164: $methods = & self::getMethods($class);
165:
166: if ($name === '') {
167: throw new MemberAccessException("Cannot write to a class '$class' property without name.");
168:
169: } elseif (self::hasProperty($class, $name)) {
170: $_this->$name = $value;
171:
172: } elseif (isset($methods[$m = 'set' . $uname])) {
173: $_this->$m($value);
174:
175: } else {
176: $type = isset($methods['get' . $uname]) || isset($methods['is' . $uname])
177: ? 'a read-only' : 'an undeclared';
178: throw new MemberAccessException("Cannot write to $type property $class::\$$name.");
179: }
180: }
181:
182:
183: 184: 185: 186: 187: 188: 189:
190: public static function remove($_this, $name)
191: {
192: $class = get_class($_this);
193: if (!self::hasProperty($class, $name)) {
194: throw new MemberAccessException("Cannot unset the property $class::\$$name.");
195: }
196: }
197:
198:
199: 200: 201: 202: 203: 204:
205: public static function has($_this, $name)
206: {
207: $name = ucfirst($name);
208: $methods = & self::getMethods(get_class($_this));
209: return $name !== '' && (isset($methods['get' . $name]) || isset($methods['is' . $name]));
210: }
211:
212:
213: 214: 215: 216:
217: private static function hasProperty($class, $name)
218: {
219: $prop = & self::$props[$class][$name];
220: if ($prop === NULL) {
221: $prop = FALSE;
222: try {
223: $rp = new \ReflectionProperty($class, $name);
224: if ($rp->isPublic() && !$rp->isStatic()) {
225: $prop = $name >= 'onA' && $name < 'on_' ? 'event' : TRUE;
226: }
227: } catch (\ReflectionException $e) {
228: }
229: }
230: return $prop;
231: }
232:
233:
234: 235: 236: 237:
238: private static function & getMethods($class)
239: {
240: if (!isset(self::$methods[$class])) {
241: self::$methods[$class] = array_fill_keys(get_class_methods($class), 0) + self::getMagicMethods($class);
242: if ($parent = get_parent_class($class)) {
243: self::$methods[$class] += self::getMethods($parent);
244: }
245: }
246: return self::$methods[$class];
247: }
248:
249:
250: 251: 252: 253:
254: public static function getMagicMethods($class)
255: {
256: $rc = new \ReflectionClass($class);
257: preg_match_all('~^
258: [ \t*]* @method [ \t]+
259: (?: [^\s(]+ [ \t]+ )?
260: (set|get|is|add) ([A-Z]\w*) [ \t]*
261: (?: \( [ \t]* ([^)$\s]+) )?
262: ()~mx', $rc->getDocComment(), $matches, PREG_SET_ORDER);
263:
264: $methods = array();
265: foreach ($matches as $m) {
266: list(, $op, $prop, $type) = $m;
267: $name = $op . $prop;
268: $prop = strtolower($prop[0]) . substr($prop, 1) . ($op === 'add' ? 's' : '');
269: if ($rc->hasProperty($prop) && ($rp = $rc->getProperty($prop)) && !$rp->isStatic()) {
270: $rp->setAccessible(TRUE);
271: if ($op === 'get' || $op === 'is') {
272: $type = NULL;
273: $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: