PageRenderTime 37ms CodeModel.GetById 13ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/12_extending/sapphire/core/Object.php

https://github.com/chillu/silverstripe-book
PHP | 629 lines | 368 code | 55 blank | 206 comment | 54 complexity | 2eb2fca8affb5d10036a905b0fedb83f MD5 | raw file
  1<?php
  2/**
  3 * Base object that all others should inherit from.
  4 * This object provides a number of helper methods that patch over PHP's deficiencies.
  5 * 
  6 * @package sapphire
  7 * @subpackage core
  8 */
  9class Object {
 10	
 11	/**
 12	 * @var string $class
 13	 */
 14	public $class;
 15	
 16	/**
 17	 * @var array $statics
 18	 */
 19	protected static $statics = array();
 20	
 21	/**
 22	 * @var array $static_cached
 23	 */
 24	protected static $static_cached = array();
 25	
 26	/**
 27	 * This DataObjects extensions, eg Versioned.
 28	 * @var array
 29	 */
 30	protected $extension_instances = array();
 31
 32	/**
 33	 * Extensions to be used on this object. An array of extension names
 34	 * and parameters eg:
 35	 * 
 36	 * 	static $extensions = array(
 37	 * 		"Hierarchy",
 38	 * 		"Versioned('Stage', 'Live')",
 39	 * 	);
 40	 * 
 41	 * @var array
 42	 */
 43	public static $extensions = null;
 44
 45	/**
 46	 * @var array $extraStatics
 47	 */
 48	protected static $extraStatics = array();
 49	
 50	/**
 51	 * @var array $classConstructed
 52	 */
 53	protected static $classConstructed = array();
 54
 55	/**
 56	 * @var array $extraMethods
 57	 */
 58	protected static $extraMethods = array();
 59	
 60	/**
 61	 * @var array $builtInMethods
 62	 */
 63	protected static $builtInMethods = array();
 64
 65    /**
 66    * @var array $custom_classes Use the class in the value instead of the class in the key
 67    */
 68    private static $custom_classes = array();
 69
 70	/**
 71	 * @var array $strong_classes
 72	 */
 73	private static $strong_classes = array();
 74	
 75	/**
 76	 * @var array $uninherited_statics
 77	 */
 78	private static $uninherited_statics = array();
 79
 80	function __construct() {
 81		$this->class = get_class($this);	
 82
 83		// Set up the extensions
 84		if($extensions = $this->stat('extensions')) {
 85			foreach($extensions as $extension) {
 86				$instance = eval("return new $extension;");
 87				$instance->setOwner($this);
 88				$this->extension_instances[$instance->class] = $instance;
 89			}
 90		}
 91
 92		if(!isset(Object::$classConstructed[$this->class])) {
 93			$this->defineMethods();
 94			Object::$classConstructed[$this->class] = true;
 95		}
 96	}
 97	
 98	/**
 99	 * Calls a method.
100	 * Extra methods can be hooked to a class using
101	 */
102	public function __call($methodName, $args) {
103		$lowerMethodName = strtolower($methodName);
104		if(isset(Object::$extraMethods[$this->class][$lowerMethodName])) {
105			$config = Object::$extraMethods[$this->class][$lowerMethodName];
106			if(isset($config['parameterName'])) {
107				if(isset($config['arrayIndex'])) $obj = $this->{$config['parameterName']}[$config['arrayIndex']];
108				else $obj = $this->{$config['parameterName']};
109
110				if($obj) {
111					return call_user_func_array(array(&$obj, $methodName), $args);
112				} else {
113					if($this->destroyed) user_error("Attempted to call $methodName on destroyed '$this->class' object", E_USER_ERROR);
114					else user_error("'$this->class' object doesn't have a parameter $config[parameterName]($config[arrayIndex]) to pass control to.  Perhaps this object has been mistakenly destroyed?", E_USER_WARNING);
115				}
116
117			} else if(isset($config['wrap'])) {
118				array_unshift($args, $config['methodName']);
119				return call_user_func_array(array(&$this, $config['wrap']), $args);
120
121			} else if(isset($config['function'])) {
122				$function = $config['function'];
123				return $function($this, $args);
124
125			} else if($config['function_str']) {
126				$function = Object::$extraMethods[$this->class][strtolower($methodName)]['function'] = create_function('$obj, $args', $config['function_str']);
127				return $function($this, $args);
128
129			} else {
130				user_error("Object::__call() Method '$methodName' in class '$this->class' an invalid format: " . var_export(Object::$extraMethods[$this->class][$methodName],true), E_USER_ERROR);
131			}
132		} else {
133			user_error("Object::__call() Method '$methodName' not found in class '$this->class'", E_USER_ERROR);
134		}
135	}
136
137	/**
138	 * This function allows you to overload class creation methods, so certain classes are
139	 * always created correctly over your system.
140	 *
141	 * @param oldClass = the old classname you want to replace with.
142	 * @param customClass = the new Classname you wish to replace the old class with.
143	 * @param strong - If you want to force a replacement of a class then we use a different array
144	 * e.g for use in singleton classes.
145	 */
146    public static function useCustomClass( $oldClass, $customClass,$strong = false ) {
147        if($strong){
148        	self::$strong_classes[$oldClass] = $customClass;
149		}else{
150			self::$custom_classes[$oldClass] = $customClass;
151		}
152    }
153
154    public static function getCustomClass( $oldClass ) {
155    	if( array_key_exists($oldClass, self::$custom_classes) )
156    		return self::$custom_classes[$oldClass];
157    	else{
158     		return $oldClass;
159		}
160    }
161
162	/**
163	 * Create allows us to override the standard classes of sapphire with our own custom classes.
164	 * create will load strong classes firstly for singleton level and database interaction, otherwise will
165	 * use the fallback custom classes.
166	 * To set a strong custom class to overide an object at for say singleton use, use the syntax
167	 *  Object::useCustomClass('Datetime','SSDatetime',true);
168	 * @param className - The classname you want to create
169	 * @param args -  Up to 9 arguments you wish to pass on to the new class
170	 */
171    public static function create( $className, $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null, $arg6 = null, $arg7 = null, $arg8 = null ) {
172
173		$useStrongClassName =  isset(self::$strong_classes[$className]);
174		$useClassName = isset(self::$custom_classes[$className]);
175
176		if($useStrongClassName){
177			$classToCreate = self::$strong_classes[$className];
178		}elseif($useClassName){
179			$classToCreate = self::$custom_classes[$className];
180		}
181
182		$hasStrong = isset(self::$strong_classes[$className]) && class_exists(self::$strong_classes[$className]);
183		$hasNormal = isset(self::$custom_classes[$className]) && class_exists(self::$custom_classes[$className]);
184
185		if( !isset($classToCreate) || (!$hasStrong && !$hasNormal)){
186		  	 $classToCreate = $className;
187		}
188        return new $classToCreate( $arg0, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8 );
189    }
190
191
192	/**
193	 * Strong_create is a function to enforce a certain class replacement
194	 * e.g Php5.2's latest introduction of a namespace conflict means we have to replace
195	 * all instances of Datetime with SSdatetime.
196	 * this allows us to seperate those, and sapphires classes
197	 * @param className -  The class you wish to create.
198	 * @param args - pass up to 8 arguments to the created class.
199	 */
200	public static function strong_create( $className, $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null, $arg6 = null, $arg7 = null, $arg8 = null ) {
201		$useStrongClassName =  isset(self::$strong_classes[$className]);
202		if($useStrongClassName){
203			$classToCreate = self::$strong_classes[$className];
204		}
205		if( !isset($classToCreate) || !class_exists( self::$strong_classes[$className])){
206		  	 $classToCreate = $className;
207		}
208        return new $classToCreate( $arg0, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8 );
209	}
210
211	/**
212	 * Returns true if the given method exists.
213	 */
214	public function hasMethod($methodName) {
215		if(method_exists($this, $methodName)) return true;
216
217		$methodName = strtolower($methodName);
218		if(!isset($this->class)) $this->class = get_class($this);
219		/*
220		if(!isset(Object::$builtInMethods['_set'][$this->class])) $this->buildMethodList();
221
222		if(isset(Object::$builtInMethods[$this->class][$methodName])) return true;
223		*/
224		if(isset(Object::$extraMethods[$this->class][$methodName])) return true;
225		return false;
226	}
227
228	/**
229	 * Add the all methods from a given parameter to this object.
230	 * This is used for extensions.
231	 * @param parameterName The name of the parameter.  This parameter must be instanciated with an item of the correct class.
232	 * @param arrayIndex If parameterName is an array, this can be an index.  If null, we'll assume the value is all that is needed.
233	 */
234	protected function addMethodsFrom($parameterName, $arrayIndex = null) {
235		$obj = isset($arrayIndex) ? $this->{$parameterName}[$arrayIndex] : $this->$parameterName;
236		if(!$obj) user_error("Object::addMethodsFrom: $parameterName/$arrayIndex", E_USER_ERROR);
237		// Hack to fix Fatal error: Call to undefined method stdClass::allMethodNames()
238		if(method_exists($obj, 'allMethodNames')) {
239			$methodNames = $obj->allMethodNames(true);
240			foreach($methodNames as $methodName) {
241				Object::$extraMethods[$this->class][strtolower($methodName)] = array("parameterName" => $parameterName, "arrayIndex" => $arrayIndex);
242			}
243		}
244	}
245
246	/**
247	 * Add a 'wrapper method'.
248	 * For example, Thumbnail($arg, $arg) can be defined to call generateImage("Thumbnail", $arg, $arg)
249	 */
250	protected function addWrapperMethod($methodName, $wrapperMethod) {
251		Object::$extraMethods[$this->class][strtolower($methodName)] = array("wrap" => $wrapperMethod, "methodName" => $methodName);
252	}
253
254	/**
255	 * Create a new method
256	 * @param methodName The name of the method
257	 * @param methodCode The PHP code of the method, in a string.  Arguments will be contained
258	 * in an array called $args.  The object will be $obj, not $this.  You won't be able to access
259	 * any protected methods; the method is actually contained in an external function.
260	 */
261	protected function createMethod($methodName, $methodCode) {
262		Object::$extraMethods[$this->class][strtolower($methodName)] = array("function_str" => $methodCode);
263	}
264
265	/**
266	 * Return the names of all the methods on this object.
267	 * param includeCustom If set to true, then return custom methods too.
268	 */
269	function allMethodNames($includeCustom = false) {
270		if(!$this->class) $this->class = get_class($this);
271
272		if(!isset(Object::$builtInMethods['_set'][$this->class])) $this->buildMethodList();
273
274		if($includeCustom && isset(Object::$extraMethods[$this->class])) {
275			return array_merge(Object::$builtInMethods[$this->class], array_keys(Object::$extraMethods[$this->class]));
276		} else {
277			return Object::$builtInMethods[$this->class];
278		}
279	}
280
281	function buildMethodList() {
282		if(!$this->class) $this->class = get_class($this);
283		$reflection = new ReflectionClass($this->class);
284
285		$methods = $reflection->getMethods();
286		foreach($methods as $method) {
287			$name = $method->getName();
288			$methodNames[strtolower($name)] = $name;
289		}
290		Object::$builtInMethods[$this->class] = $methodNames;
291		Object::$builtInMethods['_set'][$this->class] = true;
292	}
293
294	/**
295	 * This constructor will be called the first time an object of this class is created.
296	 * You can overload it with methods for setting up the class - for example, extra methods.
297	 */
298	protected function defineMethods() {
299		if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
300			$this->addMethodsFrom('extension_instances', $i);
301		}
302
303		if(isset($_REQUEST['debugmethods']) && isset(Object::$builtInMethods[$this->class])) {
304			Debug::require_developer_login();
305			echo "<h2>Methods defined for $this->class</h2>";
306			foreach(Object::$builtInMethods[$this->class] as $name => $info) {
307				echo "<li>$name";
308			}
309		}
310	}
311
312	/**
313	 * This method lets us extend a built-in class by adding pseudo-static variables to it.
314	 * 
315	 * @param string $class Classname
316	 * @param array $statics Statics to add, with keys being static property names on the class
317	 * @param boolean $replace Replace the whole variable instead of merging arrays 
318	 */
319	static function addStaticVars($class, $statics, $replace = false) {
320		if(empty(Object::$extraStatics[$class])) {
321			Object::$extraStatics[$class] = (array)$statics;
322		} elseif($replace) {
323			Object::$extraStatics[$class] = $statics;
324		} else {
325			$ar1 = (array)Object::$extraStatics[$class];
326			$ar2 = (array)$statics;
327			Object::$extraStatics[$class] = array_merge_recursive($ar1, $ar2);
328		}
329	}
330
331	/**
332	 * Set an uninherited static variable
333	 */
334	function set_uninherited($name, $val) {
335		return Object::$uninherited_statics[$this->class][$name] = $val;
336	}
337	
338	/**
339	 * Get an uninherited static variable
340	 */
341	function uninherited($name, $builtIn = false) {
342		// Copy a built-in value into our own array cache.  PHP's static variable support is shit.
343		if($builtIn) {
344			$val = $this->stat($name);
345			$val2 = null;
346
347			// isset() can handle the case where a variable isn't defined; more reliable than reflection
348			$propertyName = get_parent_class($this) . "::\$$name";
349			$val2 = eval("return isset($propertyName) ? $propertyName : null;");
350
351			return ($val != $val2) ? $val : null;
352		}
353
354		return isset(Object::$uninherited_statics[$this->class][$name]) ? Object::$uninherited_statics[$this->class][$name] : null;
355	}
356
357	/**
358	 * Get a static variable.
359	 * 
360	 * @param string $name
361	 * @param boolean $uncached
362	 * @return mixed
363	 */
364	function stat($name, $uncached = false) {
365		if(!$this->class) $this->class = get_class($this);
366
367		if(!isset(Object::$static_cached[$this->class][$name]) || $uncached) {
368			$classes = ClassInfo::ancestry($this->class);
369			foreach($classes as $class) {
370				if(isset(Object::$extraStatics[$class][$name])) {
371					$extra = Object::$extraStatics[$class][$name];
372					if(!is_array($extra)) return $extra;
373					break;
374				}
375			}
376			$stat = eval("return {$this->class}::\$$name;");
377			Object::$statics[$this->class][$name] = isset($extra) ? array_merge($extra, (array)$stat) : $stat;
378			Object::$static_cached[$this->class][$name] = true;
379		}
380
381		return Object::$statics[$this->class][$name];
382	}
383	/**
384	 * Set a static variable
385	 */
386	function set_stat($name, $val) {
387		Object::$statics[$this->class][$name] = $val;
388		Object::$static_cached[$this->class][$name] = true;
389	}
390	
391	/**
392	 * Returns true if this object "exists", i.e., has a sensible value.
393	 * Overload this in subclasses.
394	 * For example, an empty DataObject record could return false.
395	 *
396	 * @return boolean
397	 */
398	public function exists() {
399		return true;
400	}
401
402	function parentClass() {
403		return get_parent_class($this);
404	}
405	
406	/**
407	 * Wrapper for PHP's is_a()
408	 * 
409	 * @param string $class
410	 * @return boolean
411	 */
412	function is_a($class) {
413		return is_a($this, $class);
414	}
415
416	public function __toString() {
417		return $this->class;
418	}
419	
420	
421	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
422	// EXTENSION METHODS
423	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
424
425	/**
426	 * Invokes a method on the object itself, or proxied through a decorator.
427	 * 
428	 * This method breaks the normal rules of inheritance, and aggregates everything
429	 * in the returned result. If the invoked methods return void, they are still recorded as
430	 * empty array keys.
431	 * 
432	 * @todo find a better way of integrating inheritance rules
433	 *
434	 * @param unknown_type $funcName
435	 * @param unknown_type $arg
436	 */
437	public function invokeWithExtensions($funcName, $arg=null) {
438		$results = array();
439		if (method_exists($this, $funcName)) {
440			$results[] = $this->$funcName($arg);
441		}
442		$extras = $this->extend($funcName, $arg);
443		if ($extras) {
444			return array_merge($results, $extras);
445		} else {
446			return $results;	
447		}
448	}
449	
450	/**
451	 * Run the given function on all of this object's extensions. Note that this method
452	 * originally returned void, so if you wanted to return results, you're hosed.
453	 * 
454	 * Currently returns an array, with an index resulting every time the function is called.
455	 * Only adds returns if they're not NULL, to avoid bogus results from methods just
456	 * defined on the parent decorator. This is important for permission-checks through
457	 * extend, as they use min() to determine if any of the returns is FALSE.
458	 * As min() doesn't do type checking, an included NULL return would fail the permission checks.
459	 * 
460	 * @param string $funcName The name of the function.
461	 * @param mixed $arg An Argument to be passed to each of the extension functions.
462	 */
463	public function extend($funcName, &$arg1=null, &$arg2=null, &$arg3=null, &$arg4=null, &$arg5=null, &$arg6=null, &$arg7=null) {
464		$arguments = func_get_args();
465		array_shift($arguments);
466		
467		if($this->extension_instances) {
468			$returnArr = array();
469			foreach($this->extension_instances as $extension) {
470				if($extension->hasMethod($funcName)) {
471					$return = $extension->$funcName($arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7);
472					if($return !== NULL) $returnArr[] = $return;
473				}
474			}
475			return $returnArr;
476		}
477	}
478	
479	/**
480	 * Get an extension on this DataObject
481	 * 
482	 * @param string $name Classname of the Extension (e.g. 'Versioned')
483	 * 
484	 * @return DataObjectDecorator The instance of the extension
485	 */
486	public function extInstance($name) {
487		if(isset($this->extension_instances[$name])) {
488			return $this->extension_instances[$name];
489		}
490	}
491	
492	/**
493	 * Returns true if the given extension class is attached to this object
494	 * 
495	 * @param string $requiredExtension Classname of the extension
496	 * 
497	 * @return boolean True if the given extension class is attached to this object
498	 */
499	public function hasExtension($requiredExtension) {
500		return isset($this->extension_instances[$requiredExtension]) ? true : false;
501	}
502
503	/**
504	 * Add an extension to the given object.
505	 * This can be used to add extensions to built-in objects, such as role decorators on Member 
506	 */
507	public static function add_extension($className, $extensionName) {
508		Object::addStaticVars($className, array(
509			'extensions' => array(
510				$extensionName,
511			),
512		));
513	}
514
515	public static function remove_extension($className, $extensionName) {
516		Object::$extraStatics[$className]['extensions'] = array_diff(Object::$extraStatics[$className]['extensions'], array($extensionName));
517	}
518	
519	/**
520	 * Loads a current cache from the filesystem, if it can.
521	 *
522	 * @param string $cachename The name of the cache to load
523	 * @param int $expire The lifetime of the cache in seconds
524	 * @return mixed The data from the cache, or false if the cache wasn't loaded
525	 */
526	protected function loadCache($cachename, $expire = 3600) {
527		$cache_dir = TEMP_FOLDER;
528		$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
529		if((!isset($_GET['flush']) || $_GET['flush']!=1) && (@file_exists($cache_path) && ((@filemtime($cache_path) + $expire) > (time())))) {
530			return @unserialize(file_get_contents($cache_path));
531		}
532		return false;
533	}
534	
535	/**
536	 * Saves a cache to the file system
537	 *
538	 * @param string $cachename The name of the cache to save
539	 * @param mixed $data The data to cache
540	 */
541	protected function saveCache($cachename, $data) {
542		$cache_dir = TEMP_FOLDER;
543		$cache_path = $cache_dir . "/" . $this->sanitiseCachename($cachename);
544		$fp = @fopen($cache_path, "w+");
545		if(!$fp) {
546			return; // Throw an error?
547		}
548		@fwrite($fp, @serialize($data));
549		@fclose($fp);
550	}
551	
552	/**
553	 * Makes a cache name safe to use in a file system
554	 *
555	 * @param string $cachename The cache name to sanitise
556	 * @return string the sanitised cache name
557	 */
558	protected function sanitiseCachename($cachename) {
559		// Replace illegal characters with underscores
560		$cachename = str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $cachename);
561		return $cachename;
562	}
563	
564	/**
565	 * Caches the return value of a method.
566	 *
567	 * @param callback $callback The method to cache
568	 * @param int $expire The lifetime of the cache
569	 * @param string|int $id An id for the cache
570	 * @return mixed The cached return of the method
571	 */
572	public function cacheToFile($callback, $expire = 3600, $id = false) {
573		if(!$this->class) {
574			$this->class = get_class($this);
575		}
576		if(!method_exists($this->class, $callback)) {
577			user_error("Class {$this->class} doesn't have the method $callback.", E_USER_ERROR);
578		}
579		$cachename = $this->class . "_" . $callback;
580		if($id) {
581			$cachename .= "_" . (string)$id;
582		}
583		if(($data = $this->loadCache($cachename, $expire)) !== false) {
584			return $data;
585		}
586		// No cache to use
587		$data = $this->$callback();
588		if($data === false) {
589			// Some problem with function. Didn't give anything to cache. So don't cache it.
590			return false;
591		}
592		$this->saveCache($cachename, $data);
593		return $data;
594	}
595	
596	/**
597	 * Caches the return value of a method. Passes args to the method as well.
598	 *
599	 * @param callback $callback The method to cache
600	 * @param array $args The arguments to pass to the method
601	 * @param int $expire The lifetime of the cache
602	 * @param string|int $id An id for the cache
603	 * @return mixed The cached return of the method
604	 */
605	public function cacheToFileWithArgs($callback, $args = array(), $expire = 3600, $id = false) {
606		if(!$this->class) {
607			$this->class = get_class($this);
608		}
609		if(!method_exists($this->class, $callback)) {
610			user_error("Class {$this->class} doesn't have the method $callback.", E_USER_ERROR);
611		}
612		$cachename = $this->class . "_" . $callback;
613		if($id) {
614			$cachename .= "_" . (string)$id;
615		}
616		if(($data = $this->loadCache($cachename, $expire)) !== false) {
617			return $data;
618		}
619		// No cache to use
620		$data = call_user_func_array(array($this, $callback), $args);
621		if($data === false) {
622			// Some problem with function. Didn't give anything to cache. So don't cache it.
623			return false;
624		}
625		$this->saveCache($cachename, $data);
626		return $data;
627	}
628}
629?>