PageRenderTime 66ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

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