2016-01-27 13:46:43 +13:00
|
|
|
<?php
|
|
|
|
|
2016-08-19 10:51:35 +12:00
|
|
|
namespace SilverStripe\Core;
|
2016-01-27 13:46:43 +13:00
|
|
|
|
|
|
|
use BadMethodCallException;
|
|
|
|
use InvalidArgumentException;
|
2018-01-12 14:53:25 +13:00
|
|
|
use ReflectionClass;
|
|
|
|
use ReflectionMethod;
|
2016-01-27 13:46:43 +13:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Allows an object to declare a set of custom methods
|
|
|
|
*/
|
2016-11-29 12:31:16 +13:00
|
|
|
trait CustomMethods
|
|
|
|
{
|
2016-01-27 13:46:43 +13:00
|
|
|
/**
|
|
|
|
* Custom method sources
|
|
|
|
*
|
2018-01-12 14:53:25 +13:00
|
|
|
* @var array Array of class names (lowercase) to list of methods.
|
|
|
|
* The list of methods will have lowercase keys. Each value in this array
|
|
|
|
* can be a callable, array, or string callback
|
2016-01-27 13:46:43 +13:00
|
|
|
*/
|
2017-08-22 11:22:08 +12:00
|
|
|
protected static $extra_methods = [];
|
2016-01-27 13:46:43 +13:00
|
|
|
|
2016-11-29 12:31:16 +13:00
|
|
|
/**
|
|
|
|
* Name of methods to invoke by defineMethods for this instance
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2020-04-20 18:58:09 +01:00
|
|
|
protected $extra_method_registers = [];
|
2016-01-27 13:46:43 +13:00
|
|
|
|
|
|
|
/**
|
2018-01-12 14:53:25 +13:00
|
|
|
* Non-custom public methods.
|
2016-01-27 13:46:43 +13:00
|
|
|
*
|
2018-01-12 14:53:25 +13:00
|
|
|
* @var array Array of class names (lowercase) to list of methods.
|
|
|
|
* The list of methods will have lowercase keys and correct-case values.
|
2016-01-27 13:46:43 +13:00
|
|
|
*/
|
2020-04-20 18:58:09 +01:00
|
|
|
protected static $built_in_methods = [];
|
2016-01-27 13:46:43 +13:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
|
|
|
|
*
|
|
|
|
* You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
|
|
|
|
* {@link Object::addWrapperMethod()}
|
|
|
|
*
|
|
|
|
* @param string $method
|
|
|
|
* @param array $arguments
|
|
|
|
* @return mixed
|
|
|
|
* @throws BadMethodCallException
|
|
|
|
*/
|
2016-11-29 12:31:16 +13:00
|
|
|
public function __call($method, $arguments)
|
|
|
|
{
|
|
|
|
// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
|
|
|
|
// call, then we should rebuild it.
|
2017-05-17 17:40:13 +12:00
|
|
|
$class = static::class;
|
2016-11-29 12:31:16 +13:00
|
|
|
$config = $this->getExtraMethodConfig($method);
|
|
|
|
if (empty($config)) {
|
|
|
|
throw new BadMethodCallException(
|
|
|
|
"Object->__call(): the method '$method' does not exist on '$class'"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (true) {
|
2017-08-22 11:22:08 +12:00
|
|
|
case isset($config['callback']): {
|
|
|
|
return $config['callback']($this, $arguments);
|
|
|
|
}
|
2016-11-29 12:31:16 +13:00
|
|
|
case isset($config['property']) : {
|
2017-09-04 09:23:07 +12:00
|
|
|
$property = $config['property'];
|
|
|
|
$index = $config['index'];
|
|
|
|
$obj = $index !== null ?
|
|
|
|
$this->{$property}[$index] :
|
|
|
|
$this->{$property};
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2017-10-05 17:23:02 +13:00
|
|
|
if (!$obj) {
|
|
|
|
throw new BadMethodCallException(
|
|
|
|
"Object->__call(): {$class} cannot pass control to {$property}({$index})."
|
|
|
|
. ' Perhaps this object was mistakenly destroyed?'
|
|
|
|
);
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
// Call on object
|
2017-10-05 17:23:02 +13:00
|
|
|
try {
|
2018-01-12 14:53:25 +13:00
|
|
|
if ($obj instanceof Extension) {
|
|
|
|
$obj->setOwner($this);
|
|
|
|
}
|
2017-10-05 17:23:02 +13:00
|
|
|
return $obj->$method(...$arguments);
|
|
|
|
} finally {
|
2018-01-12 14:53:25 +13:00
|
|
|
if ($obj instanceof Extension) {
|
|
|
|
$obj->clearOwner();
|
|
|
|
}
|
2017-10-05 17:23:02 +13:00
|
|
|
}
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
2017-08-22 11:22:08 +12:00
|
|
|
case isset($config['wrap']): {
|
2016-11-29 12:31:16 +13:00
|
|
|
array_unshift($arguments, $config['method']);
|
2017-10-05 17:23:02 +13:00
|
|
|
$wrapped = $config['wrap'];
|
|
|
|
return $this->$wrapped(...$arguments);
|
2017-08-22 11:22:08 +12:00
|
|
|
}
|
|
|
|
case isset($config['function']): {
|
2016-11-29 12:31:16 +13:00
|
|
|
return $config['function']($this, $arguments);
|
2017-08-22 11:22:08 +12:00
|
|
|
}
|
|
|
|
default: {
|
2016-11-29 12:31:16 +13:00
|
|
|
throw new BadMethodCallException(
|
|
|
|
"Object->__call(): extra method $method is invalid on $class:"
|
2017-08-22 11:22:08 +12:00
|
|
|
. var_export($config, true)
|
2016-11-29 12:31:16 +13:00
|
|
|
);
|
2017-08-22 11:22:08 +12:00
|
|
|
}
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds any methods from {@link Extension} instances attached to this object.
|
|
|
|
* All these methods can then be called directly on the instance (transparently
|
|
|
|
* mapped through {@link __call()}), or called explicitly through {@link extend()}.
|
|
|
|
*
|
|
|
|
* @uses addMethodsFrom()
|
|
|
|
*/
|
|
|
|
protected function defineMethods()
|
|
|
|
{
|
|
|
|
// Define from all registered callbacks
|
|
|
|
foreach ($this->extra_method_registers as $callback) {
|
|
|
|
call_user_func($callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an callback to invoke that defines extra methods
|
|
|
|
*
|
|
|
|
* @param string $name
|
|
|
|
* @param callable $callback
|
|
|
|
*/
|
|
|
|
protected function registerExtraMethodCallback($name, $callback)
|
|
|
|
{
|
|
|
|
if (!isset($this->extra_method_registers[$name])) {
|
|
|
|
$this->extra_method_registers[$name] = $callback;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return TRUE if a method exists on this object
|
|
|
|
*
|
|
|
|
* This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
|
|
|
|
* extensions
|
|
|
|
*
|
|
|
|
* @param string $method
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function hasMethod($method)
|
|
|
|
{
|
2023-02-01 16:05:54 +13:00
|
|
|
return method_exists($this, $method ?? '') || $this->hasCustomMethod($method);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a custom method with this name is defined.
|
|
|
|
*/
|
|
|
|
protected function hasCustomMethod($method): bool
|
|
|
|
{
|
|
|
|
return $this->getExtraMethodConfig($method) !== null;
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get meta-data details on a named method
|
|
|
|
*
|
2017-05-17 17:40:13 +12:00
|
|
|
* @param string $method
|
2016-11-29 12:31:16 +13:00
|
|
|
* @return array List of custom method details, if defined for this method
|
|
|
|
*/
|
|
|
|
protected function getExtraMethodConfig($method)
|
|
|
|
{
|
2022-07-21 15:49:48 +12:00
|
|
|
if (empty($method)) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-08-22 11:22:08 +12:00
|
|
|
// Lazy define methods
|
2018-01-12 14:53:25 +13:00
|
|
|
$lowerClass = strtolower(static::class);
|
|
|
|
if (!isset(self::$extra_methods[$lowerClass])) {
|
2017-08-22 11:22:08 +12:00
|
|
|
$this->defineMethods();
|
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
2016-01-27 13:46:43 +13:00
|
|
|
|
|
|
|
/**
|
2016-11-29 12:31:16 +13:00
|
|
|
* Return the names of all the methods available on this object
|
|
|
|
*
|
|
|
|
* @param bool $custom include methods added dynamically at runtime
|
2018-01-12 14:53:25 +13:00
|
|
|
* @return array Map of method names with lowercase keys
|
2016-11-29 12:31:16 +13:00
|
|
|
*/
|
|
|
|
public function allMethodNames($custom = false)
|
|
|
|
{
|
2018-01-12 14:53:25 +13:00
|
|
|
$methods = static::findBuiltInMethods();
|
|
|
|
|
|
|
|
// Query extra methods
|
|
|
|
$lowerClass = strtolower(static::class);
|
|
|
|
if ($custom && isset(self::$extra_methods[$lowerClass])) {
|
|
|
|
$methods = array_merge(self::$extra_methods[$lowerClass], $methods);
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
return $methods;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all public built in methods for this class
|
|
|
|
*
|
|
|
|
* @param string|object $class Class or instance to query methods from (defaults to static::class)
|
|
|
|
* @return array Map of methods with lowercase key name
|
|
|
|
*/
|
|
|
|
protected static function findBuiltInMethods($class = null)
|
|
|
|
{
|
|
|
|
$class = is_object($class) ? get_class($class) : ($class ?: static::class);
|
|
|
|
$lowerClass = strtolower($class);
|
|
|
|
if (isset(self::$built_in_methods[$lowerClass])) {
|
|
|
|
return self::$built_in_methods[$lowerClass];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build new list
|
|
|
|
$reflection = new ReflectionClass($class);
|
|
|
|
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
|
|
|
|
self::$built_in_methods[$lowerClass] = [];
|
|
|
|
foreach ($methods as $method) {
|
|
|
|
$name = $method->getName();
|
|
|
|
self::$built_in_methods[$lowerClass][strtolower($name)] = $name;
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
2018-01-12 14:53:25 +13:00
|
|
|
return self::$built_in_methods[$lowerClass];
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-12 14:53:25 +13:00
|
|
|
* Find all methods on the given object.
|
|
|
|
*
|
|
|
|
* @param object $object
|
2016-11-29 12:31:16 +13:00
|
|
|
* @return array
|
|
|
|
*/
|
2018-01-12 14:53:25 +13:00
|
|
|
protected function findMethodsFrom($object)
|
2016-11-29 12:31:16 +13:00
|
|
|
{
|
2018-01-12 14:53:25 +13:00
|
|
|
// Respect "allMethodNames"
|
|
|
|
if (method_exists($object, 'allMethodNames')) {
|
|
|
|
if ($object instanceof Extension) {
|
2017-10-05 17:23:02 +13:00
|
|
|
try {
|
2018-01-12 14:53:25 +13:00
|
|
|
$object->setOwner($this);
|
|
|
|
$methods = $object->allMethodNames(true);
|
2017-10-05 17:23:02 +13:00
|
|
|
} finally {
|
2018-01-12 14:53:25 +13:00
|
|
|
$object->clearOwner();
|
2017-10-05 17:23:02 +13:00
|
|
|
}
|
|
|
|
} else {
|
2018-01-12 14:53:25 +13:00
|
|
|
$methods = $object->allMethodNames(true);
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
2018-01-12 14:53:25 +13:00
|
|
|
return $methods;
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
// Get methods
|
|
|
|
return static::findBuiltInMethods($object);
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-12 14:53:25 +13:00
|
|
|
* Add all the methods from an object property.
|
2016-11-29 12:31:16 +13:00
|
|
|
*
|
|
|
|
* @param string $property the property name
|
|
|
|
* @param string|int $index an index to use if the property is an array
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
*/
|
|
|
|
protected function addMethodsFrom($property, $index = null)
|
|
|
|
{
|
2017-05-17 17:40:13 +12:00
|
|
|
$class = static::class;
|
2018-01-12 14:53:25 +13:00
|
|
|
$object = ($index !== null) ? $this->{$property}[$index] : $this->$property;
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
if (!$object) {
|
2016-11-29 12:31:16 +13:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
$methods = $this->findMethodsFrom($object);
|
|
|
|
if (!$methods) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$methodInfo = [
|
|
|
|
'property' => $property,
|
|
|
|
'index' => $index,
|
|
|
|
];
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
$newMethods = array_fill_keys(array_keys($methods), $methodInfo);
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
// Merge with extra_methods
|
|
|
|
$lowerClass = strtolower($class);
|
|
|
|
if (isset(self::$extra_methods[$lowerClass])) {
|
|
|
|
self::$extra_methods[$lowerClass] = array_merge(self::$extra_methods[$lowerClass], $newMethods);
|
|
|
|
} else {
|
|
|
|
self::$extra_methods[$lowerClass] = $newMethods;
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add all the methods from an object property (which is an {@link Extension}) to this object.
|
|
|
|
*
|
|
|
|
* @param string $property the property name
|
|
|
|
* @param string|int $index an index to use if the property is an array
|
|
|
|
*/
|
|
|
|
protected function removeMethodsFrom($property, $index = null)
|
|
|
|
{
|
|
|
|
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
|
2017-05-17 17:40:13 +12:00
|
|
|
$class = static::class;
|
2016-11-29 12:31:16 +13:00
|
|
|
|
|
|
|
if (!$extension) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"Object->removeMethodsFrom(): could not remove methods from {$class}->{$property}[$index]"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
$lowerClass = strtolower($class);
|
|
|
|
if (!isset(self::$extra_methods[$lowerClass])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$methods = $this->findMethodsFrom($extension);
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
// Unset by key
|
|
|
|
self::$extra_methods[$lowerClass] = array_diff_key(self::$extra_methods[$lowerClass], $methods);
|
2016-11-29 12:31:16 +13:00
|
|
|
|
2018-01-12 14:53:25 +13:00
|
|
|
// Clear empty list
|
|
|
|
if (empty(self::$extra_methods[$lowerClass])) {
|
|
|
|
unset(self::$extra_methods[$lowerClass]);
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
|
|
|
|
* can be wrapped to generateThumbnail(x)
|
|
|
|
*
|
|
|
|
* @param string $method the method name to wrap
|
|
|
|
* @param string $wrap the method name to wrap to
|
|
|
|
*/
|
|
|
|
protected function addWrapperMethod($method, $wrap)
|
|
|
|
{
|
2018-01-12 14:53:25 +13:00
|
|
|
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
|
2017-08-22 11:22:08 +12:00
|
|
|
'wrap' => $wrap,
|
2016-11-29 12:31:16 +13:00
|
|
|
'method' => $method
|
2020-04-20 18:58:09 +01:00
|
|
|
];
|
2016-11-29 12:31:16 +13:00
|
|
|
}
|
2017-08-22 11:22:08 +12:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add callback as a method.
|
|
|
|
*
|
|
|
|
* @param string $method Name of method
|
|
|
|
* @param callable $callback Callback to invoke.
|
|
|
|
* Note: $this is passed as first parameter to this callback and then $args as array
|
|
|
|
*/
|
|
|
|
protected function addCallbackMethod($method, $callback)
|
|
|
|
{
|
2018-01-12 14:53:25 +13:00
|
|
|
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
|
2017-08-22 11:22:08 +12:00
|
|
|
'callback' => $callback,
|
|
|
|
];
|
|
|
|
}
|
2016-01-27 13:46:43 +13:00
|
|
|
}
|