From a8012b4a49a07fb7a6857b2a575b16c220a919bf Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 8 Sep 2022 00:11:06 -0400 Subject: [PATCH 1/5] first draft to make extend() usable with behaviors --- src/Database/Model.php | 4 ++-- src/Extension/Extendable.php | 4 ++-- src/Extension/ExtendableTrait.php | 36 +++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Database/Model.php b/src/Database/Model.php index 210590eb8..60f2e8b19 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -147,9 +147,9 @@ public function reloadRelations($relationName = null) /** * Extend this object properties upon construction. */ - public static function extend(Closure $callback) + public static function extend(Closure $callback, $after = false) { - self::extendableExtendCallback($callback); + self::extendableExtendCallback($callback, $after); } /** diff --git a/src/Extension/Extendable.php b/src/Extension/Extendable.php index 7d890e327..ca25cf4db 100644 --- a/src/Extension/Extendable.php +++ b/src/Extension/Extendable.php @@ -50,8 +50,8 @@ public static function __callStatic($name, $params) return self::extendableCallStatic($name, $params); } - public static function extend(callable $callback) + public static function extend(callable $callback, $after = false) { - self::extendableExtendCallback($callback); + self::extendableExtendCallback($callback, $after); } } diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index d8dbf998f..95200dbc7 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -33,10 +33,17 @@ trait ExtendableTrait * @var array Used to extend the constructor of an extendable class. Eg: * * Class::extend(function($obj) { }) - * */ protected static $extendableCallbacks = []; + /** + * @var array Used to extend the constructor of an extendable class + * AFTER the behaviors are loaded. Eg: + * + * Class::extend(function($obj) { }, after: true) + */ + protected static $extendableAfterConstructCallbacks = []; + /** * @var array Collection of static methods used by behaviors. */ @@ -101,6 +108,17 @@ public function extendableConstruct() $this->extendClassWith($useClass); } + + /* + * Apply init callbacks after Behaviors have been loaded. + */ + foreach ($classes as $class) { + if (isset(self::$extendableAfterConstructCallbacks[$class]) && is_array(self::$extendableAfterConstructCallbacks[$class])) { + foreach (self::$extendableAfterConstructCallbacks[$class] as $callback) { + call_user_func(Serialization::unwrapClosure($callback), $this); + } + } + } } /** @@ -108,16 +126,22 @@ public function extendableConstruct() * @param callable $callback * @return void */ - public static function extendableExtendCallback($callback) + public static function extendableExtendCallback($callback, $after = false) { + $var = 'extendableCallbacks'; $class = get_called_class(); + + if ($after) { + $var = 'extendableAfterConstructCallbacks'; + } + if ( - !isset(self::$extendableCallbacks[$class]) || - !is_array(self::$extendableCallbacks[$class]) + !isset(self::${$var}[$class]) || + !is_array(self::${$var}[$class]) ) { - self::$extendableCallbacks[$class] = []; + self::${$var}[$class] = []; } - self::$extendableCallbacks[$class][] = Serialization::wrapClosure($callback); + self::${$var}[$class][] = Serialization::wrapClosure($callback); } /** From 8cb2f247b077e08693a2a16c245be6ffb7f01b73 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 8 Sep 2022 14:40:32 +0800 Subject: [PATCH 2/5] Clean up extendable trait, tweak property names --- src/Extension/Extendable.php | 2 +- src/Extension/ExtendableTrait.php | 206 ++++++++++++------------------ 2 files changed, 81 insertions(+), 127 deletions(-) diff --git a/src/Extension/Extendable.php b/src/Extension/Extendable.php index ca25cf4db..361e53a1f 100644 --- a/src/Extension/Extendable.php +++ b/src/Extension/Extendable.php @@ -50,7 +50,7 @@ public static function __callStatic($name, $params) return self::extendableCallStatic($name, $params); } - public static function extend(callable $callback, $after = false) + public static function extend(callable $callback, bool $after = false) { self::extendableExtendCallback($callback, $after); } diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index 95200dbc7..1844891fb 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -9,10 +9,12 @@ use Illuminate\Support\Facades\App; /** - * This extension trait is used when access to the underlying base class - * is not available, such as classes that belong to the foundation - * framework (Laravel). It is currently used by the Controller and - * Model classes. + * Extendable trait. + * + * Provides dynamic class extension functionality, allowing for classes to have additional methods, properties and + * functionality defined at runtime. + * + * This trait can be used when a class is unable to extend the `Extendable` class. * * @author Alexey Bobkov, Samuel Georges */ @@ -20,9 +22,9 @@ trait ExtendableTrait { /** - * @var array Class reflection information, including behaviors. + * Class reflection information, including behaviors. */ - protected $extensionData = [ + protected array $extensionData = [ 'extensions' => [], 'methods' => [], 'dynamicMethods' => [], @@ -30,47 +32,43 @@ trait ExtendableTrait ]; /** - * @var array Used to extend the constructor of an extendable class. Eg: - * - * Class::extend(function($obj) { }) + * Registered extension callbacks. These are run prior to loading implemented behaviors. */ - protected static $extendableCallbacks = []; + protected static array $preBehaviorCallbacks = []; /** - * @var array Used to extend the constructor of an extendable class - * AFTER the behaviors are loaded. Eg: - * - * Class::extend(function($obj) { }, after: true) + * Registered extension callbacks. These are run after loading implemented behaviors. */ - protected static $extendableAfterConstructCallbacks = []; + protected static array $postBehaviorCallbacks = []; /** - * @var array Collection of static methods used by behaviors. + * Collection of static methods used by behaviors. */ - protected static $extendableStaticMethods = []; + protected static array $extendableStaticMethods = []; /** - * @var bool Indicates if dynamic properties can be created. + * Indicates if dynamic properties can be created. */ - protected static $extendableGuardProperties = true; + protected static bool $extendableGuardProperties = true; /** - * @var ClassLoader|null Class loader instance. + * Class loader instance. */ - protected static $extendableClassLoader = null; + protected static ?ClassLoader $extendableClassLoader = null; /** * This method should be called as part of the constructor. */ - public function extendableConstruct() + public function extendableConstruct(): void { /* * Apply init callbacks */ $classes = array_merge([get_class($this)], class_parents($this)); + foreach ($classes as $class) { - if (isset(self::$extendableCallbacks[$class]) && is_array(self::$extendableCallbacks[$class])) { - foreach (self::$extendableCallbacks[$class] as $callback) { + if (isset(self::$preBehaviorCallbacks[$class]) && is_array(self::$preBehaviorCallbacks[$class])) { + foreach (self::$preBehaviorCallbacks[$class] as $callback) { call_user_func(Serialization::unwrapClosure($callback), $this); } } @@ -85,11 +83,9 @@ public function extendableConstruct() if (is_string($this->implement)) { $uses = explode(',', $this->implement); - } - elseif (is_array($this->implement)) { + } elseif (is_array($this->implement)) { $uses = $this->implement; - } - else { + } else { throw new Exception(sprintf('Class %s contains an invalid $implement value', get_class($this))); } @@ -113,8 +109,8 @@ public function extendableConstruct() * Apply init callbacks after Behaviors have been loaded. */ foreach ($classes as $class) { - if (isset(self::$extendableAfterConstructCallbacks[$class]) && is_array(self::$extendableAfterConstructCallbacks[$class])) { - foreach (self::$extendableAfterConstructCallbacks[$class] as $callback) { + if (isset(self::$postBehaviorCallbacks[$class]) && is_array(self::$postBehaviorCallbacks[$class])) { + foreach (self::$postBehaviorCallbacks[$class] as $callback) { call_user_func(Serialization::unwrapClosure($callback), $this); } } @@ -122,42 +118,37 @@ public function extendableConstruct() } /** - * Helper method for `::extend()` static method - * @param callable $callback - * @return void + * Registers an extension callback. + * + * If `$after` is set to `true`, the callback will be run after the behaviors have been + * initialised. */ - public static function extendableExtendCallback($callback, $after = false) + public static function extendableExtendCallback(callable $callback, bool $after = false): void { - $var = 'extendableCallbacks'; $class = get_called_class(); - - if ($after) { - $var = 'extendableAfterConstructCallbacks'; - } + $property = (($after) ? 'post' : 'pre') . 'BehaviorCallbacks'; if ( - !isset(self::${$var}[$class]) || - !is_array(self::${$var}[$class]) + !isset(self::${$property}[$class]) || + !is_array(self::${$property}[$class]) ) { - self::${$var}[$class] = []; + self::${$property}[$class] = []; } - self::${$var}[$class][] = Serialization::wrapClosure($callback); + + self::${$property}[$class][] = Serialization::wrapClosure($callback); } /** * Clear the list of extended classes so they will be re-extended. - * @return void */ - public static function clearExtendedClasses() + public static function clearExtendedClasses(): void { - self::$extendableCallbacks = []; + self::$preBehaviorCallbacks = []; + self::$postBehaviorCallbacks = []; } /** - * Normalizes the provided extension name allowing for the ClassLoader to inject aliased classes - * - * @param string $name - * @return string + * Normalizes the provided extension name allowing for the ClassLoader to inject aliased classes. */ protected function extensionNormalizeClassName(string $name): string { @@ -169,13 +160,9 @@ protected function extensionNormalizeClassName(string $name): string } /** - * Extracts the available methods from a behavior and adds it to the - * list of callable methods. - * @param string $extensionName - * @param object $extensionObject - * @return void + * Extracts the available methods from a behavior and adds it to the list of callable methods. */ - protected function extensionExtractMethods($extensionName, $extensionObject) + protected function extensionExtractMethods(string $extensionName, object $extensionObject): void { if (!method_exists($extensionObject, 'extensionIsHiddenMethod')) { throw new Exception(sprintf( @@ -198,12 +185,9 @@ protected function extensionExtractMethods($extensionName, $extensionObject) } /** - * Programmatically adds a method to the extendable class - * @param string $dynamicName - * @param callable $method - * @param string $extension + * Programmatically adds a method to the extendable class. */ - public function addDynamicMethod($dynamicName, $method, $extension = null) + public function addDynamicMethod(string $name, callable $method, ?string $extension = null): void { if ( is_string($method) && @@ -212,38 +196,32 @@ public function addDynamicMethod($dynamicName, $method, $extension = null) ) { $method = [$extensionObj, $method]; } - $this->extensionData['dynamicMethods'][$dynamicName] = Serialization::wrapClosure($method); + $this->extensionData['dynamicMethods'][$name] = Serialization::wrapClosure($method); } /** * Programmatically adds a property to the extendable class - * - * @param string $dynamicName The name of the property to add - * @param mixed $value The value of the property - * @return void */ - public function addDynamicProperty($dynamicName, $value = null) + public function addDynamicProperty(string $name, $value = null): void { - if (array_key_exists($dynamicName, $this->getDynamicProperties())) { + if (array_key_exists($name, $this->getDynamicProperties())) { return; } self::$extendableGuardProperties = false; - if (!property_exists($this, $dynamicName)) { - $this->{$dynamicName} = $value; + if (!property_exists($this, $name)) { + $this->{$name} = $value; } - $this->extensionData['dynamicProperties'][] = $dynamicName; + $this->extensionData['dynamicProperties'][] = $name; self::$extendableGuardProperties = true; } /** - * Dynamically extend a class with a specified behavior - * @param string $extensionName - * @return void + * Dynamically extend a class with a specified behavior. */ - public function extendClassWith($extensionName) + public function extendClassWith(string $extensionName): void { if (empty($extensionName)) { throw new Exception(sprintf( @@ -268,24 +246,19 @@ public function extendClassWith($extensionName) } /** - * Check if extendable class is extended with a behavior object - * @param string $name Fully qualified behavior name - * @return boolean + * Check if extendable class is extended with a behavior object. */ - public function isClassExtendedWith($name) + public function isClassExtendedWith(string $name): bool { return isset($this->extensionData['extensions'][$this->extensionNormalizeClassName($name)]); } /** - * Returns a behavior object from an extendable class, example: - * - * $this->getClassExtension('Backend.Behaviors.FormController') + * Returns a behavior object from an extendable class. * - * @param string $name Fully qualified behavior name - * @return mixed + * If this behavior has not been implemented in this class, this method will return `null`. */ - public function getClassExtension($name) + public function getClassExtension(string $name): ?object { return $this->extensionData['extensions'][$this->extensionNormalizeClassName($name)] ?? null; } @@ -295,11 +268,8 @@ public function getClassExtension($name) * extension name, example: * * $this->asExtension('FormController') - * - * @param string $shortName - * @return mixed */ - public function asExtension($shortName) + public function asExtension(string $shortName): ?object { $hints = []; foreach ($this->extensionData['extensions'] as $class => $obj) { @@ -315,11 +285,11 @@ public function asExtension($shortName) } /** - * Checks if a method exists, extension equivalent of method_exists() - * @param string $name - * @return boolean + * Checks if a method exists. + * + * Functionally similar to the `method_exists` PHP function, but also checks for dynamic methods. */ - public function methodExists($name) + public function methodExists(string $name): bool { return ( method_exists($this, $name) || @@ -329,10 +299,11 @@ public function methodExists($name) } /** - * Get a list of class methods, extension equivalent of get_class_methods() - * @return array + * Get a list of class methods. + * + * Functionally similar to the `get_class_methods` PHP function, but also returns dynamic methods. */ - public function getClassMethods() + public function getClassMethods(): array { return array_values(array_unique(array_merge( get_class_methods($this), @@ -342,10 +313,9 @@ public function getClassMethods() } /** - * Returns all dynamic properties and their values - * @return array ['property' => 'value'] + * Returns all dynamic properties and their values. */ - public function getDynamicProperties() + public function getDynamicProperties(): array { $result = []; $propertyNames = $this->extensionData['dynamicProperties']; @@ -356,11 +326,11 @@ public function getDynamicProperties() } /** - * Checks if a property exists, extension equivalent of `property_exists()` - * @param string $name - * @return boolean + * Checks if a property exists. + * + * Functionally similar to the `property_exists` PHP function, but also returns dynamic properties. */ - public function propertyExists($name) + public function propertyExists(string $name): bool { if (property_exists($this, $name)) { return true; @@ -379,12 +349,9 @@ public function propertyExists($name) } /** - * Checks if a property is accessible, property equivalent of `is_callable()` - * @param mixed $class - * @param string $propertyName - * @return boolean + * Checks if a property is accessible (public). */ - protected function extendableIsAccessible($class, $propertyName) + protected function extendableIsAccessible(object $class, string $propertyName): bool { $reflector = new ReflectionClass($class); $property = $reflector->getProperty($propertyName); @@ -393,10 +360,8 @@ protected function extendableIsAccessible($class, $propertyName) /** * Magic method for `__get()` - * @param string $name - * @return mixed|null */ - public function extendableGet($name) + public function extendableGet(string $name): mixed { foreach ($this->extensionData['extensions'] as $extensionObject) { if ( @@ -417,11 +382,8 @@ public function extendableGet($name) /** * Magic method for `__set()` - * @param string $name - * @param mixed $value - * @return void */ - public function extendableSet($name, $value) + public function extendableSet(string $name, $value = null): void { foreach ($this->extensionData['extensions'] as $extensionObject) { if (!property_exists($extensionObject, $name)) { @@ -449,11 +411,8 @@ public function extendableSet($name, $value) /** * Magic method for `__call()` - * @param string $name - * @param array $params - * @return mixed */ - public function extendableCall($name, $params = null) + public function extendableCall(string $name, array $params = []) { if (isset($this->extensionData['methods'][$name])) { $extension = $this->extensionData['methods'][$name]; @@ -486,11 +445,8 @@ public function extendableCall($name, $params = null) /** * Magic method for `__callStatic()` - * @param string $name - * @param array $params - * @return mixed */ - public static function extendableCallStatic($name, $params = null) + public static function extendableCallStatic(string $name, array $params = []) { $className = get_called_class(); @@ -549,9 +505,7 @@ public static function extendableCallStatic($name, $params = null) } /** - * Gets the class loader - * - * @return ClassLoader|null + * Gets the class loader instance. */ protected function extensionGetClassLoader(): ?ClassLoader { From 0d9d3a8fd62bd8a0acd57ea82dab5013c52e04f2 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 8 Sep 2022 14:44:18 +0800 Subject: [PATCH 3/5] Allow string to be provided for dynamic method callback --- src/Extension/ExtendableTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index 1844891fb..f4db9a115 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -187,7 +187,7 @@ protected function extensionExtractMethods(string $extensionName, object $extens /** * Programmatically adds a method to the extendable class. */ - public function addDynamicMethod(string $name, callable $method, ?string $extension = null): void + public function addDynamicMethod(string $name, callable|string $method, ?string $extension = null): void { if ( is_string($method) && From f3702202f680aa24d94f8e66753405404cccbde3 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 8 Sep 2022 14:49:12 +0800 Subject: [PATCH 4/5] Tweak API of model extension --- src/Database/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Model.php b/src/Database/Model.php index 60f2e8b19..68044bc6e 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -147,7 +147,7 @@ public function reloadRelations($relationName = null) /** * Extend this object properties upon construction. */ - public static function extend(Closure $callback, $after = false) + public static function extend(callable $callback, bool $after = false) { self::extendableExtendCallback($callback, $after); } From 97112bef20a33b5b021f4f1e450ae2ca474ad3a2 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 7 Oct 2022 11:04:22 -0400 Subject: [PATCH 5/5] add unit tests for extend() method with $after argument --- tests/Extension/ExtendableTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Extension/ExtendableTest.php b/tests/Extension/ExtendableTest.php index 4e7a6efb7..5191ce3a9 100644 --- a/tests/Extension/ExtendableTest.php +++ b/tests/Extension/ExtendableTest.php @@ -39,6 +39,26 @@ public function testExtendingExtendableClass() $this->assertEquals('bar', $subject->classAttribute); } + public function testExtendingExtendableClassAfterBehaviors() + { + // test default behavior + ExtendableTestExampleExtendableClass::extend(function ($extension) { + $this->assertFalse($extension->methodExists('getFoo')); + }); + + // test with after explicitly set to false + ExtendableTestExampleExtendableClass::extend(function ($extension) { + $this->assertFalse($extension->methodExists('getFoo')); + }, after: false); + + // test with after set to true + ExtendableTestExampleExtendableClass::extend(function ($extension) { + $this->assertTrue($extension->methodExists('getFoo')); + }, after: true); + + $instance = new ExtendableTestExampleExtendableClass; + } + public function testSettingDeclaredPropertyOnClass() { $subject = $this->mockClassLoader(ExtendableTestExampleExtendableClass::class);