1 <?php
2 /**
3 * A base class for all sapphire objects to inherit from.
4 *
5 * This class provides a number of pattern implementations, as well as methods and fixes to add extra psuedo-static
6 * and method functionality to PHP.
7 *
8 * See {@link Extension} on how to implement a custom multiple
9 * inheritance for object instances based on PHP5 method call overloading.
10 *
11 * @todo Create instance-specific removeExtension() which removes an extension from $extension_instances,
12 * but not from static $extensions, and clears everything added through defineMethods(), mainly $extra_methods.
13 *
14 * @package sapphire
15 * @subpackage core
16 */
17 abstract class Object {
18
19 /**
20 * An array of extension names and parameters to be applied to this object upon construction.
21 *
22 * Example:
23 * <code>
24 * public static $extensions = array (
25 * 'Hierachy',
26 * "Version('Stage', 'Live')"
27 * );
28 * </code>
29 *
30 * Use {@link Object::add_extension()} to add extensions without access to the class code,
31 * e.g. to extend core classes.
32 *
33 * Extensions are instanciated together with the object and stored in {@link $extension_instances}.
34 *
35 * @var array $extensions
36 */
37 public static $extensions = null;
38
39 /**#@+
40 * @var array
41 */
42
43 private static
44 $statics = array(),
45 $cached_statics = array(),
46 $uninherited_statics = array(),
47 $cached_uninherited_statics = array(),
48 $extra_statics = array(),
49 $replaced_statics = array(),
50 $_cache_statics_prepared = array();
51
52 private static
53 $classes_constructed = array(),
54 $extra_methods = array(),
55 $built_in_methods = array();
56
57 private static
58 $custom_classes = array(),
59 $strong_classes = array();
60
61 /**#@-*/
62
63 /**
64 * @var string the class name
65 */
66 public $class;
67
68 /**
69 * @var array all current extension instances.
70 */
71 protected $extension_instances = array();
72
73 /**
74 * An implementation of the factory method, allows you to create an instance of a class
75 *
76 * This method first for strong class overloads (singletons & DB interaction), then custom class overloads. If an
77 * overload is found, an instance of this is returned rather than the original class. To overload a class, use
78 * {@link Object::useCustomClass()}
79 *
80 * @param string $class the class name
81 * @param mixed $arguments,... arguments to pass to the constructor
82 * @return Object
83 */
84 public static function create() {
85 $args = func_get_args();
86 $class = self::getCustomClass(array_shift($args));
87
88 if(version_compare(PHP_VERSION, '5.1.3', '>=')) {
89 $reflector = new ReflectionClass($class);
90 return $reflector->newInstanceArgs($args);
91 } else {
92 // we're using a PHP install that doesn't support ReflectionClass->newInstanceArgs()
93
94 $args = $args + array_fill(0, 9, null);
95 return new $class($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7], $args[8]);
96 }
97 }
98
99 private static $_cache_inst_args = array();
100
101 /**
102 * Create an object from a string representation. It treats it as a PHP constructor without the
103 * 'new' keyword. It also manages to construct the object without the use of eval().
104 *
105 * Construction itself is done with Object::create(), so that Object::useCustomClass() calls
106 * are respected.
107 *
108 * `Object::create_from_string("Versioned('Stage','Live')")` will return the result of
109 * `Object::create('Versioned', 'Stage', 'Live);`
110 *
111 * It is designed for simple, clonable objects. The first time this method is called for a given
112 * string it is cached, and clones of that object are returned.
113 *
114 * If you pass the $firstArg argument, this will be prepended to the constructor arguments. It's
115 * impossible to pass null as the firstArg argument.
116 *
117 * `Object::create_from_string("Varchar(50)", "MyField")` will return the result of
118 * `Object::create('Vachar', 'MyField', '50');`
119 *
120 * Arguments are always strings, although this is a quirk of the current implementation rather
121 * than something that can be relied upon.
122 */
123 static function create_from_string($classSpec, $firstArg = null) {
124 if(!isset(self::$_cache_inst_args[$classSpec.$firstArg])) {
125 // an $extension value can contain parameters as a string,
126 // e.g. "Versioned('Stage','Live')"
127 if(strpos($classSpec,'(') === false) {
128 if($firstArg === null) self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec);
129 else self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec, $firstArg);
130
131 } else {
132 list($class, $args) = self::parse_class_spec($classSpec);
133
134 if($firstArg !== null) array_unshift($args, $firstArg);
135 array_unshift($args, $class);
136
137 self::$_cache_inst_args[$classSpec.$firstArg] = call_user_func_array(array('Object','create'), $args);
138 }
139 }
140
141 return clone self::$_cache_inst_args[$classSpec.$firstArg];
142 }
143
144 /**
145 * Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
146 * Returns a 2-elemnent array, with classname and arguments
147 */
148 static function parse_class_spec($classSpec) {
149 $tokens = token_get_all("<?php $classSpec");
150 $class = null;
151 $args = array();
152 $passedBracket = false;
153
154 // Keep track of the current bucket that we're putting data into
155 $bucket = &$args;
156 $bucketStack = array();
157
158 foreach($tokens as $token) {
159 $tName = is_array($token) ? $token[0] : $token;
160 // Get the class naem
161 if($class == null && is_array($token) && $token[0] == T_STRING) {
162 $class = $token[1];
163 // Get arguments
164 } else if(is_array($token)) {
165 switch($token[0]) {
166 case T_CONSTANT_ENCAPSED_STRING:
167 $argString = $token[1];
168 switch($argString[0]) {
169 case '"': $argString = stripcslashes(substr($argString,1,-1)); break;
170 case "'": $argString = str_replace(array("\\\\", "\\'"),array("\\", "'"), substr($argString,1,-1)); break;
171 default: throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
172 }
173 $bucket[] = $argString;
174 break;
175
176 case T_DNUMBER:
177 $bucket[] = (double)$token[1];
178 break;
179
180 case T_LNUMBER:
181 $bucket[] = (int)$token[1];
182 break;
183
184 case T_STRING:
185 switch($token[1]) {
186 case 'true': $args[] = true; break;
187 case 'false': $args[] = false; break;
188 default: throw new Exception("Bad T_STRING arg '{$token[1]}'");
189 }
190
191 case T_ARRAY:
192 // Add an empty array to the bucket
193 $bucket[] = array();
194 $bucketStack[] = &$bucket;
195 $bucket = &$bucket[sizeof($bucket)-1];
196
197 }
198
199 } else {
200 if($tName == ')') {
201 // Pop-by-reference
202 $bucket = &$bucketStack[sizeof($bucketStack)-1];
203 array_pop($bucketStack);
204 }
205 }
206 }
207
208 return array($class, $args);
209 }
210
211 /**
212 * Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to
213 * TRUE when using {@link Object::useCustomClass()}
214 *
215 * @param string $class the class name
216 * @param mixed $arguments,... arguments to pass to the constructor
217 * @return Object
218 */
219 public static function strong_create() {
220 $args = func_get_args();
221 $class = array_shift($args);
222
223 if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
224 $class = self::$strong_classes[$class];
225 }
226
227 if(version_compare(PHP_VERSION, '5.1.3', '>=')) {
228 $reflector = new ReflectionClass($class);
229 return $reflector->newInstanceArgs($args);
230 } else {
231 $args = $args + array_fill(0, 9, null);
232 return new $class($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7], $args[8]);
233 }
234 }
235
236 /**
237 * This class allows you to overload classes with other classes when they are constructed using the factory method
238 * {@link Object::create()}
239 *
240 * @param string $oldClass the class to replace
241 * @param string $newClass the class to replace it with
242 * @param bool $strong allows you to enforce a certain class replacement under all circumstances. This is used in
243 * singletons and DB interaction classes
244 */
245 public static function useCustomClass($oldClass, $newClass, $strong = false) {
246 if($strong) {
247 self::$strong_classes[$oldClass] = $newClass;
248 } else {
249 self::$custom_classes[$oldClass] = $newClass;
250 }
251 }
252
253 /**
254 * If a class has been overloaded, get the class name it has been overloaded with - otherwise return the class name
255 *
256 * @param string $class the class to check
257 * @return string the class that would be created if you called {@link Object::create()} with the class
258 */
259 public static function getCustomClass($class) {
260 if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
261 return self::$strong_classes[$class];
262 } elseif(isset(self::$custom_classes[$class]) && ClassInfo::exists(self::$custom_classes[$class])) {
263 return self::$custom_classes[$class];
264 }
265
266 return $class;
267 }
268
269 /**
270 * Get a static variable, taking into account SS's inbuild static caches and pseudo-statics
271 *
272 * This method first checks for any extra values added by {@link Object::add_static_var()}, and attemps to traverse
273 * up the extra static var chain until it reaches the top, or it reaches a replacement static.
274 *
275 * If any extra values are discovered, they are then merged with the default PHP static values, or in some cases
276 * completely replace the default PHP static when you set $replace = true, and do not define extra data on any child
277 * classes
278 *
279 * Note that from SilverStripe 2.3.2, Object::get_static() can only be used to get public
280 * static variables, not protected ones.
281 *
282 * @param string $class
283 * @param string $name the property name
284 * @param bool $uncached if set to TRUE, force a regeneration of the static cache
285 * @return mixed
286 */
287 public static function get_static($class, $name, $uncached = false) {
288 if(!isset(self::$_cache_statics_prepared[$class])) {
289 Object::prepare_statics($class);
290 }
291
292 if(!isset(self::$cached_statics[$class][$name]) || $uncached) {
293 //if($class == 'DataObjectDecoratorTest_MyObject') Debug::message("$class - $name");
294 $extra = $builtIn = $break = $replacedAt = false;
295 $ancestry = array_reverse(ClassInfo::ancestry($class));
296
297 // traverse up the class tree and build extra static and stop information
298 foreach($ancestry as $ancestor) {
299 if(isset(self::$extra_statics[$ancestor][$name])) {
300 $toMerge = self::$extra_statics[$ancestor][$name];
301
302 if(is_array($toMerge) && is_array($extra)) {
303 $extra = array_merge($toMerge, $extra);
304 } elseif(!$extra) {
305 $extra = $toMerge;
306 } else {
307 $break = true;
308 }
309
310 if(isset(self::$replaced_statics[$ancestor][$name])) $replacedAt = $break = $ancestor;
311
312 if($break) break;
313 }
314 }
315
316 // check whether to merge in the default value
317 if($replacedAt && ($replacedAt == $class || !is_array($extra))) {
318 $value = $extra;
319 } elseif($replacedAt) {
320 // determine whether to merge in lower-class variables
321 $ancestorRef = new ReflectionClass(reset($ancestry));
322 $ancestorProps = $ancestorRef->getStaticProperties();
323 $ancestorInbuilt = array_key_exists($name, $ancestorProps) ? $ancestorProps[$name] : null;
324
325 $replacedRef = new ReflectionClass($replacedAt);
326 $replacedProps = $replacedRef->getStaticProperties();
327 $replacedInbuilt = array_key_exists($name, $replacedProps) ? $replacedProps[$name] : null;
328
329 if($ancestorInbuilt != $replacedInbuilt) {
330 $value = is_array($ancestorInbuilt) ? array_merge($ancestorInbuilt, (array) $extra) : $extra;
331 } else {
332 $value = $extra;
333 }
334 } else {
335 // get a built-in value
336 $reflector = new ReflectionClass($class);
337 $props = $reflector->getStaticProperties();
338 $inbuilt = array_key_exists($name, $props) ? $props[$name] : null;
339 $value = isset($extra) && is_array($extra) ? array_merge($extra, (array) $inbuilt) : $inbuilt;
340 }
341
342 self::$cached_statics[$class][$name] = true;
343 self::$statics[$class][$name] = $value;
344 }
345
346 return self::$statics[$class][$name];
347 }
348
349 /**
350 * Set a static variable
351 *
352 * @param string $class
353 * @param string $name the property name to set
354 * @param mixed $value
355 */
356 public static function set_static($class, $name, $value) {
357 if(!isset(self::$_cache_statics_prepared[$class])) {
358 Object::prepare_statics($class);
359 }
360
361 self::$statics[$class][$name] = $value;
362 self::$uninherited_statics[$class][$name] = $value;
363 self::$cached_statics[$class][$name] = true;
364 self::$cached_uninherited_statics[$class][$name] = true;
365 }
366
367 /**
368 * Get an uninherited static variable - a variable that is explicity set in this class, and not in the parent class.
369 *
370 * Note that from SilverStripe 2.3.2, Object::uninherited_static() can only be used to get public
371 * static variables, not protected ones.
372 *
373 * @todo Recursively filter out parent statics, currently only inspects the parent class
374 *
375 * @param string $class
376 * @param string $name
377 * @return mixed
378 */
379 public static function uninherited_static($class, $name, $uncached = false) {
380 if(!isset(self::$_cache_statics_prepared[$class])) {
381 Object::prepare_statics($class);
382 }
383
384 if(!isset(self::$cached_uninherited_statics[$class][$name]) || $uncached) {
385 $classRef = new ReflectionClass($class);
386 $classProp = $classRef->getStaticPropertyValue($name, null);
387
388 $parentClass = get_parent_class($class);
389 if($parentClass) {
390 $parentRef = new ReflectionClass($parentClass);
391 $parentProp = $parentRef->getStaticPropertyValue($name, null);
392 if($parentProp == $classProp) $classProp = null;
393 }
394
395 // Add data from extra_statics if it has been applied to this specific class (it
396 // wouldn't make sense to have them inherit in this method). This is kept separate
397 // from the equivalent get_static code because it's so much simpler
398 if(isset(self::$extra_statics[$class][$name])) {
399 $toMerge = self::$extra_statics[$class][$name];
400
401 if(is_array($toMerge) && is_array($classProp)) {
402 $classProp = array_merge($toMerge, $classProp);
403 } elseif(!$classProp) {
404 $classProp = $toMerge;
405 }
406 }
407
408 self::$cached_uninherited_statics[$class][$name] = true;
409 self::$uninherited_statics[$class][$name] = $classProp;
410 }
411
412 return self::$uninherited_statics[$class][$name];
413 }
414
415 /**
416 * Traverse down a class ancestry and attempt to merge all the uninherited static values for a particular static
417 * into a single variable
418 *
419 * @param string $class
420 * @param string $name the static name
421 * @param string $ceiling an optional parent class name to begin merging statics down from, rather than traversing
422 * the entire hierarchy
423 * @return mixed
424 */
425 public static function combined_static($class, $name, $ceiling = false) {
426 $ancestry = ClassInfo::ancestry($class);
427 $values = null;
428
429 if($ceiling) while(current($ancestry) != $ceiling && $ancestry) {
430 array_shift($ancestry);
431 }
432
433 if($ancestry) foreach($ancestry as $ancestor) {
434 $merge = self::uninherited_static($ancestor, $name);
435
436 if(is_array($values) && is_array($merge)) {
437 $values = array_merge($values, $merge);
438 } elseif($merge) {
439 $values = $merge;
440 }
441 }
442
443 return $values;
444 }
445
446 /**
447 * Merge in a set of additional static variables
448 *
449 * @param string $class
450 * @param array $properties in a [property name] => [value] format
451 * @param bool $replace replace existing static vars
452 */
453 public static function addStaticVars($class, $properties, $replace = false) {
454 foreach($properties as $prop => $value) self::add_static_var($class, $prop, $value, $replace);
455 }
456
457 /**
458 * Add a static variable without replacing it completely if possible, but merging in with both existing PHP statics
459 * and existing psuedo-statics. Uses PHP's array_merge_recursive() with if the $replace argument is FALSE.
460 *
461 * Documentation from http://php.net/array_merge_recursive:
462 * If the input arrays have the same string keys, then the values for these keys are merged together
463 * into an array, and this is done recursively, so that if one of the values is an array itself,
464 * the function will merge it with a corresponding entry in another array too.
465 * If, however, the arrays have the same numeric key, the later value will not overwrite the original value,
466 * but will be appended.
467 *
468 * @param string $class
469 * @param string $name the static name
470 * @param mixed $value
471 * @param bool $replace completely replace existing static values
472 */
473 public static function add_static_var($class, $name, $value, $replace = false) {
474 if(is_array($value) && isset(self::$extra_statics[$class][$name]) && !$replace) {
475 self::$extra_statics[$class][$name] = array_merge_recursive(self::$extra_statics[$class][$name], $value);
476 } else {
477 self::$extra_statics[$class][$name] = $value;
478 }
479
480 if ($replace) {
481 self::set_static($class, $name, $value);
482 self::$replaced_statics[$class][$name] = true;
483
484 // Clear caches
485 } else {
486 self::$cached_statics[$class][$name] = null;
487 self::$cached_uninherited_statics[$class][$name] = null;
488 }
489 }
490
491 /**
492 * Return TRUE if a class has a specified extension
493 *
494 * @param string $class
495 * @param string $requiredExtension the class name of the extension to check for.
496 */
497 public static function has_extension($class, $requiredExtension) {
498 $requiredExtension = strtolower($requiredExtension);
499 if($extensions = self::combined_static($class, 'extensions')) foreach($extensions as $extension) {
500 $left = strtolower(Extension::get_classname_without_arguments($extension));
501 $right = strtolower(Extension::get_classname_without_arguments($requiredExtension));
502 if($left == $right) return true;
503 }
504
505 return false;
506 }
507
508 /**
509 * Add an extension to a specific class.
510 * As an alternative, extensions can be added to a specific class
511 * directly in the {@link Object::$extensions} array.
512 * See {@link SiteTree::$extensions} for examples.
513 * Keep in mind that the extension will only be applied to new
514 * instances, not existing ones (including all instances created through {@link singleton()}).
515 *
516 * @param string $class Class that should be decorated - has to be a subclass of {@link Object}
517 * @param string $extension Subclass of {@link Extension} with optional parameters
518 * as a string, e.g. "Versioned" or "Translatable('Param')"
519 */
520 public static function add_extension($class, $extension) {
521 if(!preg_match('/^([^(]*)/', $extension, $matches)) {
522 return false;
523 }
524 $extensionClass = $matches[1];
525 if(!class_exists($extensionClass)) {
526 user_error(sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), E_USER_ERROR);
527 }
528
529 if(!ClassInfo::is_subclass_of($extensionClass, 'Extension')) {
530 user_error(sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass), E_USER_ERROR);
531 }
532
533 // unset some caches
534 self::$cached_statics[$class]['extensions'] = null;
535 $subclasses = ClassInfo::subclassesFor($class);
536 $subclasses[] = $class;
537 if($subclasses) foreach($subclasses as $subclass) {
538 unset(self::$classes_constructed[$subclass]);
539 unset(self::$extra_methods[$subclass]);
540 }
541
542 // merge with existing static vars
543 $extensions = self::uninherited_static($class, 'extensions');
544
545 // We use unshift rather than push so that module extensions are added before built-in ones.
546 // in particular, this ensures that the Versioned rewriting is done last.
547 if($extensions) array_unshift($extensions, $extension);
548 else $extensions = array($extension);
549
550 self::set_static($class, 'extensions', $extensions);
551
552 // load statics now for DataObject classes
553 if(ClassInfo::is_subclass_of($class, 'DataObject')) {
554 DataObjectDecorator::load_extra_statics($class, $extension);
555 }
556 }
557
558 /**
559 * Prepare static variables before processing a {@link get_static} or {@link set_static}
560 * call.
561 */
562 private static function prepare_statics($class) {
563 // _cache_statics_prepared setting must come first to prevent infinite loops when we call
564 // get_static below
565 self::$_cache_statics_prepared[$class] = true;
566
567 // load statics now for DataObject classes
568 if(is_subclass_of($class, 'DataObject')) {
569 $extensions = Object::uninherited_static($class, 'extensions');
570 if($extensions) foreach($extensions as $extension) {
571 DataObjectDecorator::load_extra_statics($class, $extension);
572 }
573 }
574 }
575
576
577 /**
578 * Remove an extension from a class.
579 * Keep in mind that this won't revert any datamodel additions
580 * of the extension at runtime, unless its used before the
581 * schema building kicks in (in your _config.php).
582 * Doesn't remove the extension from any {@link Object}
583 * instances which are already created, but will have an
584 * effect on new extensions.
585 * Clears any previously created singletons through {@link singleton()}
586 * to avoid side-effects from stale extension information.
587 *
588 * @todo Add support for removing extensions with parameters
589 *
590 * @param string $class
591 * @param string $extension Classname of an {@link Extension} subclass, without parameters
592 */
593 public static function remove_extension($class, $extension) {
594 if(self::has_extension($class, $extension)) {
595 self::set_static(
596 $class,
597 'extensions',
598 array_diff(self::uninherited_static($class, 'extensions'), array($extension))
599 );
600 }
601
602 // unset singletons to avoid side-effects
603 global $_SINGLETONS;
604 $_SINGLETONS = array();
605
606 // unset some caches
607 self::$cached_statics[$class]['extensions'] = null;
608 $subclasses = ClassInfo::subclassesFor($class);
609 $subclasses[] = $class;
610 if($subclasses) foreach($subclasses as $subclass) {
611 unset(self::$classes_constructed[$subclass]);
612 unset(self::$extra_methods[$subclass]);
613 }
614
615 }
616
617 /**
618 * @param string $class
619 * @param bool $includeArgumentString Include the argument string in the return array,
620 * FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
621 * @return array Numeric array of either {@link DataObjectDecorator} classnames,
622 * or eval'ed classname strings with constructor arguments.
623 */
624 function get_extensions($class, $includeArgumentString = false) {
625 $extensions = self::get_static($class, 'extensions');
626 if($includeArgumentString) {
627 return $extensions;
628 } else {
629 $extensionClassnames = array();
630 if($extensions) foreach($extensions as $extension) {
631 $extensionClassnames[] = Extension::get_classname_without_arguments($extension);
632 }
633 return $extensionClassnames;
634 }
635 }
636
637 // -----------------------------------------------------------------------------------------------------------------
638
639 public function __construct() {
640 $this->class = get_class($this);
641
642 // Don't bother checking some classes that should never be extended
643 static $notExtendable = array('Object', 'ViewableData', 'RequestHandler');
644
645 if($extensionClasses = ClassInfo::ancestry($this->class)) foreach($extensionClasses as $class) {
646 if(in_array($class, $notExtendable)) continue;
647
648 if($extensions = self::uninherited_static($class, 'extensions')) {
649 foreach($extensions as $extension) {
650 $instance = self::create_from_string($extension);
651 $instance->setOwner(null, $class);
652 $this->extension_instances[$instance->class] = $instance;
653 }
654 }
655 }
656
657 if(!isset(self::$classes_constructed[$this->class])) {
658 $this->defineMethods();
659 self::$classes_constructed[$this->class] = true;
660 }
661 }
662
663 function __wakeup() {
664 if(!isset(self::$classes_constructed[$this->class])) {
665 $this->defineMethods();
666 self::$classes_constructed[$this->class] = true;
667 }
668 }
669
670 /**
671 * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
672 *
673 * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
674 * {@link Object::addWrapperMethod()}
675 *
676 * @param string $method
677 * @param array $arguments
678 * @return mixed
679 */
680 public function __call($method, $arguments) {
681 // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
682 // call, then we should rebuild it.
683 if(empty(self::$cached_statics[get_class($this)])) {
684 $this->defineMethods();
685 }
686
687 $method = strtolower($method);
688
689 if(isset(self::$extra_methods[$this->class][$method])) {
690 $config = self::$extra_methods[$this->class][$method];
691
692 switch(true) {
693 case isset($config['property']) :
694 $obj = $config['index'] !== null ?
695 $this->{$config['property']}[$config['index']] :
696 $this->{$config['property']};
697
698 if($obj) {
699 if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
700 $retVal = call_user_func_array(array($obj, $method), $arguments);
701 if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
702 return $retVal;
703 }
704
705 if($this->destroyed) {
706 throw new Exception (
707 "Object->__call(): attempt to call $method on a destroyed $this->class object"
708 );
709 } else {
710 throw new Exception (
711 "Object->__call(): $this->class cannot pass control to $config[property]($config[index])." .
712 ' Perhaps this object was mistakenly destroyed?'
713 );
714 }
715
716 case isset($config['wrap']) :
717 array_unshift($arguments, $config['method']);
718 return call_user_func_array(array($this, $config['wrap']), $arguments);
719
720 case isset($config['function']) :
721 return $config['function']($this, $arguments);
722
723 default :
724 throw new Exception (
725 "Object->__call(): extra method $method is invalid on $this->class:" . var_export($config, true)
726 );
727 }
728 } else {
729 // Please do not change the exception code number below.
730
731 throw new Exception("Object->__call(): the method '$method' does not exist on '$this->class'", 2175);
732 }
733 }
734
735 // -----------------------------------------------------------------------------------------------------------------
736
737 /**
738 * Return TRUE if a method exists on this object
739 *
740 * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
741 * extensions
742 *
743 * @param string $method
744 * @return bool
745 */
746 public function hasMethod($method) {
747 return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
748 }
749
750 /**
751 * Return the names of all the methods available on this object
752 *
753 * @param bool $custom include methods added dynamically at runtime
754 * @return array
755 */
756 public function allMethodNames($custom = false) {
757 if(!isset(self::$built_in_methods[$this->class])) {
758 self::$built_in_methods[$this->class] = array_map('strtolower', get_class_methods($this));
759 }
760
761 if($custom && isset(self::$extra_methods[$this->class])) {
762 return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
763 } else {
764 return self::$built_in_methods[$this->class];
765 }
766 }
767
768 /**
769 * Adds any methods from {@link Extension} instances attached to this object.
770 * All these methods can then be called directly on the instance (transparently
771 * mapped through {@link __call()}), or called explicitly through {@link extend()}.
772 *
773 * @uses addMethodsFrom()
774 */
775 protected function defineMethods() {
776 if($this->extension_instances) foreach(array_keys($this->extension_instances) as $key) {
777 $this->addMethodsFrom('extension_instances', $key);
778 }
779
780 if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
781 Debug::require_developer_login();
782
783 echo '<h2>Methods defined on ' . $this->class . '</h2><ul>';
784 foreach(self::$built_in_methods[$this->class] as $method) {
785 echo "<li>$method</li>";
786 }
787 echo '</ul>';
788 }
789 }
790
791 /**
792 * Add all the methods from an object property (which is an {@link Extension}) to this object.
793 *
794 * @param string $property the property name
795 * @param string|int $index an index to use if the property is an array
796 */
797 protected function addMethodsFrom($property, $index = null) {
798 $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
799
800 if(!$extension) {
801 throw new InvalidArgumentException (
802 "Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
803 );
804 }
805
806 if(method_exists($extension, 'allMethodNames')) {
807 $methods = $extension->allMethodNames(true);
808
809 } else {
810 if(!isset(self::$built_in_methods[$extension->class])) {
811 self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
812 }
813 $methods = self::$built_in_methods[$extension->class];
814 }
815
816 if($methods) {
817 $methodInfo = array(
818 'property' => $property,
819 'index' => $index,
820 'callSetOwnerFirst' => $extension instanceof Extension,
821 );
822
823 $newMethods = array_fill_keys($methods, $methodInfo);
824
825 if(isset(self::$extra_methods[$this->class])) {
826 self::$extra_methods[$this->class] =
827 array_merge(self::$extra_methods[$this->class], $newMethods);
828 } else {
829 self::$extra_methods[$this->class] = $newMethods;
830 }
831 }
832 }
833
834 /**
835 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
836 * can be wrapped to generateThumbnail(x)
837 *
838 * @param string $method the method name to wrap
839 * @param string $wrap the method name to wrap to
840 */
841 protected function addWrapperMethod($method, $wrap) {
842 self::$extra_methods[$this->class][strtolower($method)] = array (
843 'wrap' => $wrap,
844 'method' => $method
845 );
846 }
847
848 /**
849 * Add an extra method using raw PHP code passed as a string
850 *
851 * @param string $method the method name
852 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
853 * by using $obj. Note that you cannot call protected methods, as the method is actually an external function
854 */
855 protected function createMethod($method, $code) {
856 self::$extra_methods[$this->class][strtolower($method)] = array (
857 'function' => create_function('$obj, $args', $code)
858 );
859 }
860
861 // -----------------------------------------------------------------------------------------------------------------
862
863 /**
864 * @see Object::get_static()
865 */
866 public function stat($name, $uncached = false) {
867 return self::get_static(($this->class ? $this->class : get_class($this)), $name, $uncached);
868 }
869
870 /**
871 * @see Object::set_static()
872 */
873 public function set_stat($name, $value) {
874 self::set_static(($this->class ? $this->class : get_class($this)), $name, $value);
875 }
876
877 /**
878 * @see Object::uninherited_static()
879 */
880 public function uninherited($name) {
881 return self::uninherited_static(($this->class ? $this->class : get_class($this)), $name);
882 }
883
884 /**
885 * @deprecated
886 */
887 public function set_uninherited() {
888 user_error (
889 'Object->set_uninherited() is deprecated, please use a custom static on your object', E_USER_WARNING
890 );
891 }
892
893 // -----------------------------------------------------------------------------------------------------------------
894
895 /**
896 * Return true if this object "exists" i.e. has a sensible value
897 *
898 * This method should be overriden in subclasses to provide more context about the classes state. For example, a
899 * {@link DataObject} class could return false when it is deleted from the database
900 *
901 * @return bool
902 */
903 public function exists() {
904 return true;
905 }
906
907 /**
908 * @return string this classes parent class
909 */
910 public function parentClass() {
911 return get_parent_class($this);
912 }
913
914 /**
915 * Check if this class is an instance of a specific class, or has that class as one of its parents
916 *
917 * @param string $class
918 * @return bool
919 */
920 public function is_a($class) {
921 return $this instanceof $class;
922 }
923
924 /**
925 * @return string the class name
926 */
927 public function __toString() {
928 return $this->class;
929 }
930
931 // -----------------------------------------------------------------------------------------------------------------
932
933 /**
934 * Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
935 * all results into an array
936 *
937 * @param string $method the method name to call
938 * @param mixed $argument a single argument to pass
939 * @return mixed
940 * @todo integrate inheritance rules
941 */
942 public function invokeWithExtensions($method, $argument = null) {
943 $result = method_exists($this, $method) ? array($this->$method($argument)) : array();
944 $extras = $this->extend($method, $argument);
945
946 return $extras ? array_merge($result, $extras) : $result;
947 }
948
949 /**
950 * Run the given function on all of this object's extensions. Note that this method originally returned void, so if
951 * you wanted to return results, you're hosed
952 *
953 * Currently returns an array, with an index resulting every time the function is called. Only adds returns if
954 * they're not NULL, to avoid bogus results from methods just defined on the parent decorator. This is important for
955 * permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
956 * do type checking, an included NULL return would fail the permission checks.
957 *
958 * The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
959 *
960 * @param string $method the name of the method to call on each extension
961 * @param mixed $a1,... up to 7 arguments to be passed to the method
962 * @return array
963 */
964 public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
965 $values = array();
966
967 if($this->extension_instances) foreach($this->extension_instances as $instance) {
968 if(method_exists($instance, $method)) {
969 $instance->setOwner($this);
970 $value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
971 if($value !== null) $values[] = $value;
972 $instance->clearOwner();
973 }
974 }
975
976 return $values;
977 }
978
979 /**
980 * Get an extension instance attached to this object by name.
981 *
982 * @uses hasExtension()
983 *
984 * @param string $extension
985 * @return Extension
986 */
987 public function getExtensionInstance($extension) {
988 if($this->hasExtension($extension)) return $this->extension_instances[$extension];
989 }
990
991 /**
992 * Returns TRUE if this object instance has a specific extension applied
993 * in {@link $extension_instances}. Extension instances are initialized
994 * at constructor time, meaning if you use {@link add_extension()}
995 * afterwards, the added extension will just be added to new instances
996 * of the decorated class. Use the static method {@link has_extension()}
997 * to check if a class (not an instance) has a specific extension.
998 * Caution: Don't use singleton(<class>)->hasExtension() as it will
999 * give you inconsistent results based on when the singleton was first
1000 * accessed.
1001 *
1002 * @param string $extension Classname of an {@link Extension} subclass without parameters
1003 * @return bool
1004 */
1005 public function hasExtension($extension) {
1006 return isset($this->extension_instances[$extension]);
1007 }
1008
1009 /**
1010 * Get all extension instances for this specific object instance.
1011 * See {@link get_extensions()} to get all applied extension classes
1012 * for this class (not the instance).
1013 *
1014 * @return array Map of {@link DataObjectDecorator} instances, keyed by classname.
1015 */
1016 public function getExtensionInstances() {
1017 return $this->extension_instances;
1018 }
1019
1020 // -----------------------------------------------------------------------------------------------------------------
1021
1022 /**
1023 * Cache the results of an instance method in this object to a file, or if it is already cache return the cached
1024 * results
1025 *
1026 * @param string $method the method name to cache
1027 * @param int $lifetime the cache lifetime in seconds
1028 * @param string $ID custom cache ID to use
1029 * @param array $arguments an optional array of arguments
1030 * @return mixed the cached data
1031 */
1032 public function cacheToFile($method, $lifetime = 3600, $ID = false, $arguments = array()) {
1033 if(!$this->hasMethod($method)) {
1034 throw new InvalidArgumentException("Object->cacheToFile(): the method $method does not exist to cache");
1035 }
1036
1037 $cacheName = $this->class . '_' . $method;
1038
1039 if(!is_array($arguments)) $arguments = array($arguments);
1040
1041 if($ID) $cacheName .= '_' . $ID;
1042 if(count($arguments)) $cacheName .= '_' . implode('_', $arguments);
1043
1044 if($data = $this->loadCache($cacheName, $lifetime)) {
1045 return $data;
1046 }
1047
1048 $data = call_user_func_array(array($this, $method), $arguments);
1049 $this->saveCache($cacheName, $data);
1050
1051 return $data;
1052 }
1053
1054 /**
1055 * Clears the cache for the given cacheToFile call
1056 */
1057 public function clearCache($method, $ID = false, $arguments = array()) {
1058 $cacheName = $this->class . '_' . $method;
1059 if(!is_array($arguments)) $arguments = array($arguments);
1060 if($ID) $cacheName .= '_' . $ID;
1061 if(count($arguments)) $cacheName .= '_' . implode('_', $arguments);
1062
1063 $file = TEMP_FOLDER . '/' . $this->sanitiseCachename($cacheName);
1064 if(file_exists($file)) unlink($file);
1065 }
1066
1067 /**
1068 * @deprecated
1069 */
1070 public function cacheToFileWithArgs($callback, $arguments = array(), $lifetime = 3600, $ID = false) {
1071 user_error (
1072 'Object->cacheToFileWithArgs() is deprecated, please use Object->cacheToFile() with the $arguments param',
1073 E_USER_NOTICE
1074 );
1075
1076 return $this->cacheToFile($callback, $lifetime, $ID, $arguments);
1077 }
1078
1079 /**
1080 * Loads a cache from the filesystem if a valid on is present and within the specified lifetime
1081 *
1082 * @param string $cache the cache name
1083 * @param int $lifetime the lifetime (in seconds) of the cache before it is invalid
1084 * @return mixed
1085 */
1086 protected function loadCache($cache, $lifetime = 3600) {
1087 $path = TEMP_FOLDER . '/' . $this->sanitiseCachename($cache);
1088
1089 if(!isset($_REQUEST['flush']) && file_exists($path) && (filemtime($path) + $lifetime) > time()) {
1090 return unserialize(file_get_contents($path));
1091 }
1092
1093 return false;
1094 }
1095
1096 /**
1097 * Save a piece of cached data to the file system
1098 *
1099 * @param string $cache the cache name
1100 * @param mixed $data data to save (must be serializable)
1101 */
1102 protected function saveCache($cache, $data) {
1103 file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
1104 }
1105
1106 /**
1107 * Strip a file name of special characters so it is suitable for use as a cache file name
1108 *
1109 * @param string $name
1110 * @return string the name with all special cahracters replaced with underscores
1111 */
1112 protected function sanitiseCachename($name) {
1113 return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
1114 }
1115
1116 /**
1117 * @deprecated 2.4 Use getExtensionInstance
1118 */
1119 public function extInstance($extension) {
1120 user_error('Object::extInstance() is deprecated. Please use Object::getExtensionInstance() instead.', E_USER_NOTICE);
1121 return $this->getExtensionInstance($extension);
1122 }
1123
1124 }
1125