PageRenderTime 58ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/core/CustomMethods.php

http://github.com/silverstripe/sapphire
PHP | 292 lines | 159 code | 34 blank | 99 comment | 28 complexity | 6e0aefc376996fccf8b480896e5ab45c MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. namespace SilverStripe\Framework\Core;
  3. use BadMethodCallException;
  4. use InvalidArgumentException;
  5. /**
  6. * Allows an object to declare a set of custom methods
  7. */
  8. trait CustomMethods {
  9. /**
  10. * Custom method sources
  11. *
  12. * @var array
  13. */
  14. protected static $extra_methods = array();
  15. /**
  16. * Name of methods to invoke by defineMethods for this instance
  17. *
  18. * @var array
  19. */
  20. protected $extra_method_registers = array();
  21. /**
  22. * Non-custom methods
  23. *
  24. * @var array
  25. */
  26. protected static $built_in_methods = array();
  27. /**
  28. * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
  29. *
  30. * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
  31. * {@link Object::addWrapperMethod()}
  32. *
  33. * @param string $method
  34. * @param array $arguments
  35. * @return mixed
  36. * @throws BadMethodCallException
  37. */
  38. public function __call($method, $arguments) {
  39. // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
  40. // call, then we should rebuild it.
  41. $class = get_class($this);
  42. if(!array_key_exists($class, self::$extra_methods)) {
  43. $this->defineMethods();
  44. }
  45. $config = $this->getExtraMethodConfig($method);
  46. if(empty($config)) {
  47. throw new BadMethodCallException(
  48. "Object->__call(): the method '$method' does not exist on '$class'"
  49. );
  50. }
  51. switch(true) {
  52. case isset($config['property']) : {
  53. $obj = $config['index'] !== null ?
  54. $this->{$config['property']}[$config['index']] :
  55. $this->{$config['property']};
  56. if ($obj) {
  57. if (!empty($config['callSetOwnerFirst'])) {
  58. $obj->setOwner($this);
  59. }
  60. $retVal = call_user_func_array(array($obj, $method), $arguments);
  61. if (!empty($config['callSetOwnerFirst'])) {
  62. $obj->clearOwner();
  63. }
  64. return $retVal;
  65. }
  66. if (!empty($this->destroyed)) {
  67. throw new BadMethodCallException(
  68. "Object->__call(): attempt to call $method on a destroyed $class object"
  69. );
  70. } else {
  71. throw new BadMethodCallException(
  72. "Object->__call(): $class cannot pass control to $config[property]($config[index])."
  73. . ' Perhaps this object was mistakenly destroyed?'
  74. );
  75. }
  76. }
  77. case isset($config['wrap']) :
  78. array_unshift($arguments, $config['method']);
  79. return call_user_func_array(array($this, $config['wrap']), $arguments);
  80. case isset($config['function']) :
  81. return $config['function']($this, $arguments);
  82. default :
  83. throw new BadMethodCallException(
  84. "Object->__call(): extra method $method is invalid on $class:"
  85. . var_export($config, true)
  86. );
  87. }
  88. }
  89. /**
  90. * Adds any methods from {@link Extension} instances attached to this object.
  91. * All these methods can then be called directly on the instance (transparently
  92. * mapped through {@link __call()}), or called explicitly through {@link extend()}.
  93. *
  94. * @uses addMethodsFrom()
  95. */
  96. protected function defineMethods() {
  97. // Define from all registered callbacks
  98. foreach($this->extra_method_registers as $callback) {
  99. call_user_func($callback);
  100. }
  101. }
  102. /**
  103. * Register an callback to invoke that defines extra methods
  104. *
  105. * @param string $name
  106. * @param callable $callback
  107. */
  108. protected function registerExtraMethodCallback($name, $callback) {
  109. if(!isset($this->extra_method_registers[$name])) {
  110. $this->extra_method_registers[$name] = $callback;
  111. }
  112. }
  113. // --------------------------------------------------------------------------------------------------------------
  114. /**
  115. * Return TRUE if a method exists on this object
  116. *
  117. * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
  118. * extensions
  119. *
  120. * @param string $method
  121. * @return bool
  122. */
  123. public function hasMethod($method) {
  124. return method_exists($this, $method) || $this->getExtraMethodConfig($method);
  125. }
  126. /**
  127. * Get meta-data details on a named method
  128. *
  129. * @param array $method
  130. * @return array List of custom method details, if defined for this method
  131. */
  132. protected function getExtraMethodConfig($method) {
  133. $class = get_class($this);
  134. if(isset(self::$extra_methods[$class][strtolower($method)])) {
  135. return self::$extra_methods[$class][strtolower($method)];
  136. }
  137. return null;
  138. }
  139. /**
  140. * Return the names of all the methods available on this object
  141. *
  142. * @param bool $custom include methods added dynamically at runtime
  143. * @return array
  144. */
  145. public function allMethodNames($custom = false) {
  146. $class = get_class($this);
  147. if(!isset(self::$built_in_methods[$class])) {
  148. self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
  149. }
  150. if($custom && isset(self::$extra_methods[$class])) {
  151. return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
  152. } else {
  153. return self::$built_in_methods[$class];
  154. }
  155. }
  156. /**
  157. * @param object $extension
  158. * @return array
  159. */
  160. protected function findMethodsFromExtension($extension) {
  161. if (method_exists($extension, 'allMethodNames')) {
  162. if ($extension instanceof \Extension) $extension->setOwner($this);
  163. $methods = $extension->allMethodNames(true);
  164. if ($extension instanceof \Extension) $extension->clearOwner();
  165. } else {
  166. if (!isset(self::$built_in_methods[$extension->class])) {
  167. self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
  168. }
  169. $methods = self::$built_in_methods[$extension->class];
  170. }
  171. return $methods;
  172. }
  173. /**
  174. * Add all the methods from an object property (which is an {@link Extension}) to this object.
  175. *
  176. * @param string $property the property name
  177. * @param string|int $index an index to use if the property is an array
  178. * @throws InvalidArgumentException
  179. */
  180. protected function addMethodsFrom($property, $index = null) {
  181. $class = get_class($this);
  182. $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
  183. if (!$extension) {
  184. throw new InvalidArgumentException(
  185. "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
  186. );
  187. }
  188. $methods = $this->findMethodsFromExtension($extension);
  189. if ($methods) {
  190. $methodInfo = array(
  191. 'property' => $property,
  192. 'index' => $index,
  193. 'callSetOwnerFirst' => $extension instanceof \Extension,
  194. );
  195. $newMethods = array_fill_keys($methods, $methodInfo);
  196. if(isset(self::$extra_methods[$class])) {
  197. self::$extra_methods[$class] =
  198. array_merge(self::$extra_methods[$class], $newMethods);
  199. } else {
  200. self::$extra_methods[$class] = $newMethods;
  201. }
  202. }
  203. }
  204. /**
  205. * Add all the methods from an object property (which is an {@link Extension}) to this object.
  206. *
  207. * @param string $property the property name
  208. * @param string|int $index an index to use if the property is an array
  209. */
  210. protected function removeMethodsFrom($property, $index = null) {
  211. $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
  212. if (!$extension) {
  213. throw new InvalidArgumentException(
  214. "Object->removeMethodsFrom(): could not remove methods from {$this->class}->{$property}[$index]"
  215. );
  216. }
  217. $methods = $this->findMethodsFromExtension($extension);
  218. if ($methods) {
  219. foreach ($methods as $method) {
  220. $methodInfo = self::$extra_methods[$this->class][$method];
  221. if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
  222. unset(self::$extra_methods[$this->class][$method]);
  223. }
  224. }
  225. if (empty(self::$extra_methods[$this->class])) {
  226. unset(self::$extra_methods[$this->class]);
  227. }
  228. }
  229. }
  230. /**
  231. * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
  232. * can be wrapped to generateThumbnail(x)
  233. *
  234. * @param string $method the method name to wrap
  235. * @param string $wrap the method name to wrap to
  236. */
  237. protected function addWrapperMethod($method, $wrap) {
  238. $class = get_class($this);
  239. self::$extra_methods[$class][strtolower($method)] = array (
  240. 'wrap' => $wrap,
  241. 'method' => $method
  242. );
  243. }
  244. /**
  245. * Add an extra method using raw PHP code passed as a string
  246. *
  247. * @param string $method the method name
  248. * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
  249. * by using $obj. Note that you cannot call protected methods, as the method is actually an external
  250. * function
  251. */
  252. protected function createMethod($method, $code) {
  253. $class = get_class($this);
  254. self::$extra_methods[$class][strtolower($method)] = array (
  255. 'function' => create_function('$obj, $args', $code)
  256. );
  257. }
  258. }