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 InvalidArgumentException;
use SilverStripe\Dev\Deprecation;
use ReflectionClass;
use ReflectionMethod;
/**
* Allows an object to declare a set of custom methods
*/
trait CustomMethods
{
/**
* 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 = [];
@ -27,9 +29,10 @@ trait CustomMethods
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 = [];
@ -74,17 +77,16 @@ trait CustomMethods
);
}
// Call without setOwner
if (empty($config['callSetOwnerFirst'])) {
return $obj->$method(...$arguments);
}
/** @var Extension $obj */
// Call on object
try {
$obj->setOwner($this);
if ($obj instanceof Extension) {
$obj->setOwner($this);
}
return $obj->$method(...$arguments);
} finally {
$obj->clearOwner();
if ($obj instanceof Extension) {
$obj->clearOwner();
}
}
}
case isset($config['wrap']): {
@ -160,66 +162,87 @@ trait CustomMethods
return null;
}
// Lazy define methods
if (!isset(self::$extra_methods[static::class])) {
$lowerClass = strtolower(static::class);
if (!isset(self::$extra_methods[$lowerClass])) {
$this->defineMethods();
}
if (isset(self::$extra_methods[static::class][strtolower($method)])) {
return self::$extra_methods[static::class][strtolower($method)];
}
return null;
return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
}
/**
* Return the names of all the methods available on this object
*
* @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)
{
$class = static::class;
if (!isset(self::$built_in_methods[$class])) {
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this ?? ''));
}
$methods = static::findBuiltInMethods();
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) {
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];
// Query extra methods
$lowerClass = strtolower(static::class);
if ($custom && isset(self::$extra_methods[$lowerClass])) {
$methods = array_merge(self::$extra_methods[$lowerClass], $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|int $index an index to use if the property is an array
@ -228,37 +251,31 @@ trait CustomMethods
protected function addMethodsFrom($property, $index = null)
{
$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(
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
);
}
$methods = $this->findMethodsFromExtension($extension);
if ($methods) {
if ($extension instanceof Extension) {
Deprecation::notice(
'5.0',
'Register custom methods from extensions with addCallbackMethod.'
. ' callSetOwnerFirst will be removed in 5.0'
);
}
$methodInfo = [
'property' => $property,
'index' => $index,
'callSetOwnerFirst' => $extension instanceof Extension,
];
$methods = $this->findMethodsFrom($object);
if (!$methods) {
return;
}
$methodInfo = [
'property' => $property,
'index' => $index,
];
$newMethods = array_fill_keys($methods ?? [], $methodInfo);
$newMethods = array_fill_keys(array_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;
}
// 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;
}
}
@ -279,26 +296,18 @@ trait CustomMethods
);
}
$methods = $this->findMethodsFromExtension($extension);
if ($methods) {
foreach ($methods as $method) {
if (!isset(self::$extra_methods[$class][$method])) {
continue;
}
$lowerClass = strtolower($class);
if (!isset(self::$extra_methods[$lowerClass])) {
return;
}
$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
// check for index only if provided
if ((isset($methodInfo['property']) && $methodInfo['property'] === $property) &&
(!$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]);
}
// Clear empty list
if (empty(self::$extra_methods[$lowerClass])) {
unset(self::$extra_methods[$lowerClass]);
}
}
@ -311,7 +320,7 @@ trait CustomMethods
*/
protected function addWrapperMethod($method, $wrap)
{
self::$extra_methods[static::class][strtolower($method)] = [
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'wrap' => $wrap,
'method' => $method
];
@ -326,7 +335,7 @@ trait CustomMethods
*/
protected function addCallbackMethod($method, $callback)
{
self::$extra_methods[static::class][strtolower($method)] = [
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'callback' => $callback,
];
}

View File

@ -104,14 +104,6 @@ trait Extensible
$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()
{
$this->defineMethodsCustom();
@ -131,7 +123,7 @@ trait Extensible
{
$extensions = $this->getExtensionInstances();
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) {
/** @var Extensible $inst */
$extension = $inst->getExtensionInstance($extensionClass);
@ -199,11 +191,8 @@ trait Extensible
// unset some caches
$subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]);
}
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[strtolower($subclass)]);
}
Config::modify()
@ -261,10 +250,8 @@ trait Extensible
// unset some caches
$subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]);
}
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[strtolower($subclass)]);
}
}
@ -403,25 +390,19 @@ trait Extensible
* all results into an array
*
* @param string $method the method name to call
* @param mixed $a1
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @param mixed ...$arguments List of arguments
* @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 = [];
if (method_exists($this, $method ?? '')) {
$thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
$thisResult = $this->$method(...$arguments);
if ($thisResult !== null) {
$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;
}
@ -438,22 +419,16 @@ trait Extensible
* 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 mixed $a1
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @param mixed &...$arguments
* @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 = [];
if (!empty($this->beforeExtendCallbacks[$method])) {
foreach (array_reverse($this->beforeExtendCallbacks[$method] ?? []) as $callback) {
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]);
foreach (array_reverse($this->beforeExtendCallbacks[$method ?? '']) as $callback) {
$value = call_user_func_array($callback, $arguments);
if ($value !== null) {
$values[] = $value;
}
@ -462,22 +437,16 @@ trait Extensible
}
foreach ($this->getExtensionInstances() as $instance) {
if (method_exists($instance, $method ?? '')) {
try {
$instance->setOwner($this);
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
} finally {
$instance->clearOwner();
}
if ($value !== null) {
$values[] = $value;
}
// Prefer `extend` prefixed methods
$value = $instance->invokeExtension($this, $method, ...$arguments);
if ($value !== null) {
$values[] = $value;
}
}
if (!empty($this->afterExtendCallbacks[$method])) {
foreach (array_reverse($this->afterExtendCallbacks[$method] ?? []) as $callback) {
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]);
foreach (array_reverse($this->afterExtendCallbacks[$method ?? '']) as $callback) {
$value = call_user_func_array($callback, $arguments);
if ($value !== null) {
$values[] = $value;
}

View File

@ -115,4 +115,31 @@ abstract class Extension
// Split out both args and service name
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();
}
}
}