From 3e515af23919a1972d2b5f9e1564b44720da4e9d Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 4 Jul 2025 18:49:15 +0100 Subject: [PATCH 1/3] feat/scoped-macros * adding scoped macros to Macroable --- src/Illuminate/Macroable/Traits/Macroable.php | 55 +++++++++++++++++-- tests/Support/SupportMacroableTest.php | 54 ++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Macroable/Traits/Macroable.php b/src/Illuminate/Macroable/Traits/Macroable.php index 5490f1ea2b13..7cc1d14b9545 100644 --- a/src/Illuminate/Macroable/Traits/Macroable.php +++ b/src/Illuminate/Macroable/Traits/Macroable.php @@ -16,6 +16,13 @@ trait Macroable */ protected static $macros = []; + /** + * The registered scoped macros. + * + * @var array + */ + protected static $scopedMacros = []; + /** * Register a custom macro. * @@ -31,6 +38,19 @@ public static function macro($name, $macro) static::$macros[$name] = $macro; } + /** + * Register a custom scoped macro, scoped macros are only + * available to the class in which they are registered. + * + * @param string $name + * @param object|callable $macro + * @return void + */ + public static function scopedMacro($name, $macro) + { + static::$scopedMacros[static::class][$name] = $macro; + } + /** * Mix another object into the class. * @@ -60,6 +80,22 @@ public static function mixin($mixin, $replace = true) * @return bool */ public static function hasMacro($name) + { + return static::hasGlobalMacro($name) || static::hasScopedMacro($name); + } + + /** + * Checks if scoped macro is registered. + * + * @param string $name + * @return bool + */ + public static function hasScopedMacro($name): bool + { + return isset(static::$scopedMacros[static::class][$name]); + } + + public static function hasGlobalMacro($name): bool { return isset(static::$macros[$name]); } @@ -72,10 +108,12 @@ public static function hasMacro($name) public static function flushMacros() { static::$macros = []; + static::$scopedMacros = []; } /** - * Dynamically handle calls to the class. + * Dynamically handle static method calls to the class. + * Scoped macros take priority over global macros. * * @param string $method * @param array $parameters @@ -91,7 +129,11 @@ public static function __callStatic($method, $parameters) )); } - $macro = static::$macros[$method]; + if (static::hasScopedMacro($method)) { + $macro = static::$scopedMacros[static::class][$method]; + } else { + $macro = static::$macros[$method]; + } if ($macro instanceof Closure) { $macro = $macro->bindTo(null, static::class); @@ -101,7 +143,8 @@ public static function __callStatic($method, $parameters) } /** - * Dynamically handle calls to the class. + * Dynamically handle method calls to the class instance. + * Scoped macros take priority over global macros. * * @param string $method * @param array $parameters @@ -117,7 +160,11 @@ public function __call($method, $parameters) )); } - $macro = static::$macros[$method]; + if (static::hasScopedMacro($method)) { + $macro = static::$scopedMacros[static::class][$method]; + } else { + $macro = static::$macros[$method]; + } if ($macro instanceof Closure) { $macro = $macro->bindTo($this, static::class); diff --git a/tests/Support/SupportMacroableTest.php b/tests/Support/SupportMacroableTest.php index 78864c76b57c..19177403c058 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -39,6 +39,60 @@ public function testHasMacro() $this->assertFalse($macroable::hasMacro('bar')); } + public function testRegisterScopedMacro() + { + $macroable = $this->macroable; + $macroable::scopedMacro(__METHOD__, function () { + return 'Scoped Macro'; + }); + + $this->assertSame('Scoped Macro', $macroable::{__METHOD__}()); + } + + public function testHasScopedMacro() + { + $macroable = $this->macroable; + + $macroable::scopedMacro(__METHOD__, function () { + return 'Scoped Macro'; + }); + + $this->assertTrue($macroable::hasScopedMacro(__METHOD__)); + $this->assertFalse($macroable::hasGlobalMacro(__METHOD__)); + $this->assertFalse($macroable::hasScopedMacro(__METHOD__.'_NoneExistent')); + } + + public function testExtendedClassDoesNotHaveScopedMacro() + { + $macroable = $this->macroable; + $otherMacroable = new class extends EmptyMacroable + { + }; + $macroable::scopedMacro(__METHOD__, function () { + return 'Scoped Macro'; + }); + + $this->assertSame('Scoped Macro', $macroable::{__METHOD__}()); + $this->assertFalse($otherMacroable::hasScopedMacro(__METHOD__)); + $this->expectException(BadMethodCallException::class); + $otherMacroable::{__METHOD__}(); + } + public function testParentClassDoesNotHaveScopedMacro() + { + $macroable = $this->macroable; + $otherMacroable = new class extends EmptyMacroable + { + }; + $otherMacroable::scopedMacro(__METHOD__, function () { + return 'Scoped Macro'; + }); + + $this->assertSame('Scoped Macro', $otherMacroable::{__METHOD__}()); + $this->assertFalse($macroable::hasScopedMacro(__METHOD__)); + $this->expectException(BadMethodCallException::class); + $macroable::{__METHOD__}(); + } + public function testRegisterMacroAndCallWithoutStatic() { $macroable = $this->macroable; From 5e0e3873909eb2a6a96ba66d2ca3d15f33c44b72 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 7 Jul 2025 09:13:35 +0100 Subject: [PATCH 2/3] * style fix --- tests/Support/SupportMacroableTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Support/SupportMacroableTest.php b/tests/Support/SupportMacroableTest.php index 19177403c058..9e7457aa93e5 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -65,8 +65,7 @@ public function testHasScopedMacro() public function testExtendedClassDoesNotHaveScopedMacro() { $macroable = $this->macroable; - $otherMacroable = new class extends EmptyMacroable - { + $otherMacroable = new class extends EmptyMacroable { }; $macroable::scopedMacro(__METHOD__, function () { return 'Scoped Macro'; @@ -80,8 +79,7 @@ public function testExtendedClassDoesNotHaveScopedMacro() public function testParentClassDoesNotHaveScopedMacro() { $macroable = $this->macroable; - $otherMacroable = new class extends EmptyMacroable - { + $otherMacroable = new class extends EmptyMacroable { }; $otherMacroable::scopedMacro(__METHOD__, function () { return 'Scoped Macro'; From 9346c5573f4ee03a15d6f6383b29443d7b162816 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 7 Jul 2025 09:31:42 +0100 Subject: [PATCH 3/3] * style fix # 2 --- tests/Support/SupportMacroableTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Support/SupportMacroableTest.php b/tests/Support/SupportMacroableTest.php index 9e7457aa93e5..d5e318f5d858 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -76,6 +76,7 @@ public function testExtendedClassDoesNotHaveScopedMacro() $this->expectException(BadMethodCallException::class); $otherMacroable::{__METHOD__}(); } + public function testParentClassDoesNotHaveScopedMacro() { $macroable = $this->macroable;