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