PageRenderTime 43ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

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