Merge pull request #10457 from creative-commoners/pulls/5/rescue-master-extensions-expose-public

API Rescue Master Branch PR: Only expose public extension methods
This commit is contained in:
Steve Boyd 2022-08-29 19:09:00 +12:00 committed by GitHub
commit 250a75b233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 152 additions and 147 deletions

View File

@ -4,18 +4,20 @@ namespace SilverStripe\Core;
use BadMethodCallException; use BadMethodCallException;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Dev\Deprecation; use ReflectionClass;
use ReflectionMethod;
/** /**
* Allows an object to declare a set of custom methods * Allows an object to declare a set of custom methods
*/ */
trait CustomMethods trait CustomMethods
{ {
/** /**
* Custom method sources * Custom method sources
* *
* @var array * @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
*/ */
protected static $extra_methods = []; protected static $extra_methods = [];
@ -27,9 +29,10 @@ trait CustomMethods
protected $extra_method_registers = []; protected $extra_method_registers = [];
/** /**
* Non-custom methods * Non-custom public methods.
* *
* @var array * @var array Array of class names (lowercase) to list of methods.
* The list of methods will have lowercase keys and correct-case values.
*/ */
protected static $built_in_methods = []; protected static $built_in_methods = [];
@ -74,19 +77,18 @@ trait CustomMethods
); );
} }
// Call without setOwner // Call on object
if (empty($config['callSetOwnerFirst'])) {
return $obj->$method(...$arguments);
}
/** @var Extension $obj */
try { try {
if ($obj instanceof Extension) {
$obj->setOwner($this); $obj->setOwner($this);
}
return $obj->$method(...$arguments); return $obj->$method(...$arguments);
} finally { } finally {
if ($obj instanceof Extension) {
$obj->clearOwner(); $obj->clearOwner();
} }
} }
}
case isset($config['wrap']): { case isset($config['wrap']): {
array_unshift($arguments, $config['method']); array_unshift($arguments, $config['method']);
$wrapped = $config['wrap']; $wrapped = $config['wrap'];
@ -160,66 +162,87 @@ trait CustomMethods
return null; return null;
} }
// Lazy define methods // Lazy define methods
if (!isset(self::$extra_methods[static::class])) { $lowerClass = strtolower(static::class);
if (!isset(self::$extra_methods[$lowerClass])) {
$this->defineMethods(); $this->defineMethods();
} }
if (isset(self::$extra_methods[static::class][strtolower($method)])) { return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
return self::$extra_methods[static::class][strtolower($method)];
}
return null;
} }
/** /**
* Return the names of all the methods available on this object * Return the names of all the methods available on this object
* *
* @param bool $custom include methods added dynamically at runtime * @param bool $custom include methods added dynamically at runtime
* @return array * @return array Map of method names with lowercase keys
*/ */
public function allMethodNames($custom = false) public function allMethodNames($custom = false)
{ {
$class = static::class; $methods = static::findBuiltInMethods();
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])) { // Query extra methods
return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class] ?? [])); $lowerClass = strtolower(static::class);
} else { if ($custom && isset(self::$extra_methods[$lowerClass])) {
return self::$built_in_methods[$class]; $methods = array_merge(self::$extra_methods[$lowerClass], $methods);
}
}
/**
* @param object $extension
* @return array
*/
protected function findMethodsFromExtension($extension)
{
if (method_exists($extension, 'allMethodNames')) {
if ($extension instanceof Extension) {
try {
$extension->setOwner($this);
$methods = $extension->allMethodNames(true);
} finally {
$extension->clearOwner();
}
} else {
$methods = $extension->allMethodNames(true);
}
} else {
$class = get_class($extension);
if (!isset(self::$built_in_methods[$class])) {
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($extension ?? ''));
}
$methods = self::$built_in_methods[$class];
} }
return $methods; return $methods;
} }
/** /**
* Add all the methods from an object property (which is an {@link Extension}) to this object. * 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;
}
return self::$built_in_methods[$lowerClass];
}
/**
* Find all methods on the given object.
*
* @param object $object
* @return array
*/
protected function findMethodsFrom($object)
{
// Respect "allMethodNames"
if (method_exists($object, 'allMethodNames')) {
if ($object instanceof Extension) {
try {
$object->setOwner($this);
$methods = $object->allMethodNames(true);
} finally {
$object->clearOwner();
}
} else {
$methods = $object->allMethodNames(true);
}
return $methods;
}
// Get methods
return static::findBuiltInMethods($object);
}
/**
* Add all the methods from an object property.
* *
* @param string $property the property name * @param string $property the property name
* @param string|int $index an index to use if the property is an array * @param string|int $index an index to use if the property is an array
@ -228,37 +251,31 @@ trait CustomMethods
protected function addMethodsFrom($property, $index = null) protected function addMethodsFrom($property, $index = null)
{ {
$class = static::class; $class = static::class;
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property; $object = ($index !== null) ? $this->{$property}[$index] : $this->$property;
if (!$extension) { if (!$object) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]" "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
); );
} }
$methods = $this->findMethodsFromExtension($extension); $methods = $this->findMethodsFrom($object);
if ($methods) { if (!$methods) {
if ($extension instanceof Extension) { return;
Deprecation::notice(
'5.0',
'Register custom methods from extensions with addCallbackMethod.'
. ' callSetOwnerFirst will be removed in 5.0'
);
} }
$methodInfo = [ $methodInfo = [
'property' => $property, 'property' => $property,
'index' => $index, 'index' => $index,
'callSetOwnerFirst' => $extension instanceof Extension,
]; ];
$newMethods = array_fill_keys($methods ?? [], $methodInfo); $newMethods = array_fill_keys(array_keys($methods), $methodInfo);
if (isset(self::$extra_methods[$class])) { // Merge with extra_methods
self::$extra_methods[$class] = $lowerClass = strtolower($class);
array_merge(self::$extra_methods[$class], $newMethods); if (isset(self::$extra_methods[$lowerClass])) {
self::$extra_methods[$lowerClass] = array_merge(self::$extra_methods[$lowerClass], $newMethods);
} else { } else {
self::$extra_methods[$class] = $newMethods; self::$extra_methods[$lowerClass] = $newMethods;
}
} }
} }
@ -279,26 +296,18 @@ trait CustomMethods
); );
} }
$methods = $this->findMethodsFromExtension($extension); $lowerClass = strtolower($class);
if ($methods) { if (!isset(self::$extra_methods[$lowerClass])) {
foreach ($methods as $method) { return;
if (!isset(self::$extra_methods[$class][$method])) {
continue;
} }
$methods = $this->findMethodsFrom($extension);
$methodInfo = self::$extra_methods[$class][$method]; // Unset by key
self::$extra_methods[$lowerClass] = array_diff_key(self::$extra_methods[$lowerClass], $methods);
// always check for property, AND // Clear empty list
// check for index only if provided if (empty(self::$extra_methods[$lowerClass])) {
if ((isset($methodInfo['property']) && $methodInfo['property'] === $property) && unset(self::$extra_methods[$lowerClass]);
(!$index || ($index && isset($methodInfo['index']) && $methodInfo['index'] === $index))) {
unset(self::$extra_methods[$class][$method]);
}
}
if (empty(self::$extra_methods[$class])) {
unset(self::$extra_methods[$class]);
}
} }
} }
@ -311,7 +320,7 @@ trait CustomMethods
*/ */
protected function addWrapperMethod($method, $wrap) protected function addWrapperMethod($method, $wrap)
{ {
self::$extra_methods[static::class][strtolower($method)] = [ self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'wrap' => $wrap, 'wrap' => $wrap,
'method' => $method 'method' => $method
]; ];
@ -326,7 +335,7 @@ trait CustomMethods
*/ */
protected function addCallbackMethod($method, $callback) protected function addCallbackMethod($method, $callback)
{ {
self::$extra_methods[static::class][strtolower($method)] = [ self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'callback' => $callback, 'callback' => $callback,
]; ];
} }

View File

@ -104,14 +104,6 @@ trait Extensible
$this->afterExtendCallbacks[$method][] = $callback; $this->afterExtendCallbacks[$method][] = $callback;
} }
/**
* @deprecated 4.0.0:5.0.0 Extensions and methods are now lazy-loaded
*/
protected function constructExtensions()
{
Deprecation::notice('5.0', 'constructExtensions does not need to be invoked and will be removed in 5.0');
}
protected function defineMethods() protected function defineMethods()
{ {
$this->defineMethodsCustom(); $this->defineMethodsCustom();
@ -131,7 +123,7 @@ trait Extensible
{ {
$extensions = $this->getExtensionInstances(); $extensions = $this->getExtensionInstances();
foreach ($extensions as $extensionClass => $extensionInstance) { foreach ($extensions as $extensionClass => $extensionInstance) {
foreach ($this->findMethodsFromExtension($extensionInstance) as $method) { foreach ($this->findMethodsFrom($extensionInstance) as $method) {
$this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) { $this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) {
/** @var Extensible $inst */ /** @var Extensible $inst */
$extension = $inst->getExtensionInstance($extensionClass); $extension = $inst->getExtensionInstance($extensionClass);
@ -199,11 +191,8 @@ trait Extensible
// unset some caches // unset some caches
$subclasses = ClassInfo::subclassesFor($class); $subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class; $subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) { foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]); unset(self::$extra_methods[strtolower($subclass)]);
}
} }
Config::modify() Config::modify()
@ -261,10 +250,8 @@ trait Extensible
// unset some caches // unset some caches
$subclasses = ClassInfo::subclassesFor($class); $subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class; $subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) { foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]); unset(self::$extra_methods[strtolower($subclass)]);
}
} }
} }
@ -403,25 +390,19 @@ trait Extensible
* all results into an array * all results into an array
* *
* @param string $method the method name to call * @param string $method the method name to call
* @param mixed $a1 * @param mixed ...$arguments List of arguments
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @return array List of results with nulls filtered out * @return array List of results with nulls filtered out
*/ */
public function invokeWithExtensions($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null) public function invokeWithExtensions($method, &...$arguments)
{ {
$result = []; $result = [];
if (method_exists($this, $method ?? '')) { if (method_exists($this, $method ?? '')) {
$thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7); $thisResult = $this->$method(...$arguments);
if ($thisResult !== null) { if ($thisResult !== null) {
$result[] = $thisResult; $result[] = $thisResult;
} }
} }
$extras = $this->extend($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7); $extras = $this->extend($method, ...$arguments);
return $extras ? array_merge($result, $extras) : $result; return $extras ? array_merge($result, $extras) : $result;
} }
@ -438,22 +419,16 @@ trait Extensible
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}. * The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
* *
* @param string $method the name of the method to call on each extension * @param string $method the name of the method to call on each extension
* @param mixed $a1 * @param mixed &...$arguments
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @return array * @return array
*/ */
public function extend($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null) public function extend($method, &...$arguments)
{ {
$values = []; $values = [];
if (!empty($this->beforeExtendCallbacks[$method])) { if (!empty($this->beforeExtendCallbacks[$method])) {
foreach (array_reverse($this->beforeExtendCallbacks[$method] ?? []) as $callback) { foreach (array_reverse($this->beforeExtendCallbacks[$method ?? '']) as $callback) {
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]); $value = call_user_func_array($callback, $arguments);
if ($value !== null) { if ($value !== null) {
$values[] = $value; $values[] = $value;
} }
@ -462,22 +437,16 @@ trait Extensible
} }
foreach ($this->getExtensionInstances() as $instance) { foreach ($this->getExtensionInstances() as $instance) {
if (method_exists($instance, $method ?? '')) { // Prefer `extend` prefixed methods
try { $value = $instance->invokeExtension($this, $method, ...$arguments);
$instance->setOwner($this);
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
} finally {
$instance->clearOwner();
}
if ($value !== null) { if ($value !== null) {
$values[] = $value; $values[] = $value;
} }
} }
}
if (!empty($this->afterExtendCallbacks[$method])) { if (!empty($this->afterExtendCallbacks[$method])) {
foreach (array_reverse($this->afterExtendCallbacks[$method] ?? []) as $callback) { foreach (array_reverse($this->afterExtendCallbacks[$method ?? '']) as $callback) {
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]); $value = call_user_func_array($callback, $arguments);
if ($value !== null) { if ($value !== null) {
$values[] = $value; $values[] = $value;
} }

View File

@ -115,4 +115,31 @@ abstract class Extension
// Split out both args and service name // Split out both args and service name
return strtok(strtok($extensionStr ?? '', '(') ?? '', '.'); return strtok(strtok($extensionStr ?? '', '(') ?? '', '.');
} }
/**
* Invoke extension point. This will prefer explicit `extend` prefixed
* methods.
*
* @param object $owner
* @param string $method
* @param array &...$arguments
* @return mixed
*/
public function invokeExtension($owner, $method, &...$arguments)
{
// Prefer `extend` prefixed methods
$instanceMethod = method_exists($this, "extend{$method}")
? "extend{$method}"
: (method_exists($this, $method) ? $method : null);
if (!$instanceMethod) {
return null;
}
try {
$this->setOwner($owner);
return $this->$instanceMethod(...$arguments);
} finally {
$this->clearOwner();
}
}
} }