/src/Core/CustomMethods.php

https://gitlab.com/djpmedia/silverstripe-framework · PHP · 323 lines · 189 code · 33 blank · 101 comment · 27 complexity · c128248ba7cc3df41f9903545091e227 MD5 · raw file

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