PageRenderTime 60ms CodeModel.GetById 22ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/core/Object.php

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