silverstripe-framework/core/CustomMethods.php
Damian Mooyman 5c9044a007 API Enforce default_cast for all field usages
API Introduce HTMLFragment as casting helper for HTMLText with shortcodes disabled
API Introduce DBField::CDATA for XML file value encoding
API RSSFeed now casts from the underlying model rather than by override
API Introduce CustomMethods::getExtraMethodConfig() to allow metadata to be queried
BUG Remove _call hack from VirtualPage
API Remove FormField::$dontEscape
API Introduce HTMLReadonlyField for non-editable readonly HTML
API FormField::Field() now returns string in many cases rather than DBField instance.
API Remove redundant *_val methods from ViewableData
API ViewableData::obj() no longer has a $forceReturnObject parameter as it always returns an object
BUG  Fix issue with ViewableData caching incorrect field values after being modified.
API Remove deprecated DB class methods
API Enforce plain text left/right formfield titles
2016-07-13 17:15:45 +12:00

293 lines
8.5 KiB
PHP

<?php
namespace SilverStripe\Framework\Core;
use BadMethodCallException;
use InvalidArgumentException;
/**
* Allows an object to declare a set of custom methods
*/
trait CustomMethods {
/**
* Custom method sources
*
* @var array
*/
protected static $extra_methods = array();
/**
* Name of methods to invoke by defineMethods for this instance
*
* @var array
*/
protected $extra_method_registers = array();
/**
* Non-custom methods
*
* @var array
*/
protected static $built_in_methods = array();
/**
* 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
*/
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.
$class = get_class($this);
if(!array_key_exists($class, self::$extra_methods)) {
$this->defineMethods();
}
$config = $this->getExtraMethodConfig($method);
if(empty($config)) {
throw new BadMethodCallException(
"Object->__call(): the method '$method' does not exist on '$class'"
);
}
switch(true) {
case isset($config['property']) : {
$obj = $config['index'] !== null ?
$this->{$config['property']}[$config['index']] :
$this->{$config['property']};
if ($obj) {
if (!empty($config['callSetOwnerFirst'])) {
$obj->setOwner($this);
}
$retVal = call_user_func_array(array($obj, $method), $arguments);
if (!empty($config['callSetOwnerFirst'])) {
$obj->clearOwner();
}
return $retVal;
}
if (!empty($this->destroyed)) {
throw new BadMethodCallException(
"Object->__call(): attempt to call $method on a destroyed $class object"
);
} else {
throw new BadMethodCallException(
"Object->__call(): $class cannot pass control to $config[property]($config[index])."
. ' Perhaps this object was mistakenly destroyed?'
);
}
}
case isset($config['wrap']) :
array_unshift($arguments, $config['method']);
return call_user_func_array(array($this, $config['wrap']), $arguments);
case isset($config['function']) :
return $config['function']($this, $arguments);
default :
throw new BadMethodCallException(
"Object->__call(): extra method $method is invalid on $class:"
. var_export($config, true)
);
}
}
/**
* 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) {
return method_exists($this, $method) || $this->getExtraMethodConfig($method);
}
/**
* Get meta-data details on a named method
*
* @param array $method
* @return array List of custom method details, if defined for this method
*/
protected function getExtraMethodConfig($method) {
$class = get_class($this);
if(isset(self::$extra_methods[$class][strtolower($method)])) {
return self::$extra_methods[$class][strtolower($method)];
}
return null;
}
/**
* Return the names of all the methods available on this object
*
* @param bool $custom include methods added dynamically at runtime
* @return array
*/
public function allMethodNames($custom = false) {
$class = get_class($this);
if(!isset(self::$built_in_methods[$class])) {
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
}
if($custom && isset(self::$extra_methods[$class])) {
return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
} else {
return self::$built_in_methods[$class];
}
}
/**
* @param object $extension
* @return array
*/
protected function findMethodsFromExtension($extension) {
if (method_exists($extension, 'allMethodNames')) {
if ($extension instanceof \Extension) $extension->setOwner($this);
$methods = $extension->allMethodNames(true);
if ($extension instanceof \Extension) $extension->clearOwner();
} else {
if (!isset(self::$built_in_methods[$extension->class])) {
self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
}
$methods = self::$built_in_methods[$extension->class];
}
return $methods;
}
/**
* 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
* @throws InvalidArgumentException
*/
protected function addMethodsFrom($property, $index = null) {
$class = get_class($this);
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
if (!$extension) {
throw new InvalidArgumentException(
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
);
}
$methods = $this->findMethodsFromExtension($extension);
if ($methods) {
$methodInfo = array(
'property' => $property,
'index' => $index,
'callSetOwnerFirst' => $extension instanceof \Extension,
);
$newMethods = array_fill_keys($methods, $methodInfo);
if(isset(self::$extra_methods[$class])) {
self::$extra_methods[$class] =
array_merge(self::$extra_methods[$class], $newMethods);
} else {
self::$extra_methods[$class] = $newMethods;
}
}
}
/**
* 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;
if (!$extension) {
throw new InvalidArgumentException(
"Object->removeMethodsFrom(): could not remove methods from {$this->class}->{$property}[$index]"
);
}
$methods = $this->findMethodsFromExtension($extension);
if ($methods) {
foreach ($methods as $method) {
$methodInfo = self::$extra_methods[$this->class][$method];
if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
unset(self::$extra_methods[$this->class][$method]);
}
}
if (empty(self::$extra_methods[$this->class])) {
unset(self::$extra_methods[$this->class]);
}
}
}
/**
* 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) {
$class = get_class($this);
self::$extra_methods[$class][strtolower($method)] = array (
'wrap' => $wrap,
'method' => $method
);
}
/**
* Add an extra method using raw PHP code passed as a string
*
* @param string $method the method name
* @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
* by using $obj. Note that you cannot call protected methods, as the method is actually an external
* function
*/
protected function createMethod($method, $code) {
$class = get_class($this);
self::$extra_methods[$class][strtolower($method)] = array (
'function' => create_function('$obj, $args', $code)
);
}
}