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..d5e318f5d858 100644 --- a/tests/Support/SupportMacroableTest.php +++ b/tests/Support/SupportMacroableTest.php @@ -39,6 +39,59 @@ 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;