PageRenderTime 53ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/mppda_open/sapphire/core/Object.php

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