diff --git a/README.md b/README.md index 94dc21a..568a8cb 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,53 @@ class TappableClass $name = TappableClass::make()->tap(function ($tappable) { $tappable->setName('MyName'); })->getName(); + +// Or, even though setName does not return this you can now just chain from it! +$name = TappableClass::make()->tap()->setName('MyName')->getName() +``` + +### Support\Traits\Macroable + +Macros allow you to add methods to classes dynamically (without having to modify their code). + +Let's say you are tired of having to do this: + +```php +$app->get('/', function ($request, $response) { + $response = new Response; + $response->getBody()->write('Hello'); + + return $response; +}) +``` + +Instead you just want to call a write method directly from the `$response` instance. First, we need to extend the Response class so we can use the `Macroable` trait, but still have all of our base Response methods. + +```php +use GuzzleHttp\Psr7\Response; +use SlimFacades\Support\Traits\Macroable; + +class MacroableResponse extends Response +{ + use Macroable; +} +``` + +Then we need to add `MacroableResponse` to our container, so we are always dealing with the same instance (not all instances will have the "macroed" methods). + +```php +use SlimFacades\Facades\Container; +// ... above code here + +Container::set('response', function () { + return new MacroableResponse(); +}); +``` + +Then we can get our `MacroableResponse` instance from the container however you want, and just call `write`! + +```php +App::get('/', function () { + return Container::get('response')->write('Macro!'); +}); ``` diff --git a/composer.json b/composer.json index 4b10e17..781d919 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "affinity4/slimphp-facades", - "description": "Add Laravel style facades and helper functions to any SlimPHP app", + "description": "Add Laravel style facades, traits and helper functions to any SlimPHP app", "type": "library", "license": "MIT", "authors": [ diff --git a/src/Support/Traits/Macroable.php b/src/Support/Traits/Macroable.php new file mode 100644 index 0000000..3ba788e --- /dev/null +++ b/src/Support/Traits/Macroable.php @@ -0,0 +1,125 @@ +getMethods( + ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED + ); + + foreach ($methods as $method) { + if ($replace || ! static::hasMacro($method->name)) { + static::macro($method->name, $method->invoke($mixin)); + } + } + } + + /** + * Checks if macro is registered. + * + * @param string $name + * @return bool + */ + public static function hasMacro($name) + { + return isset(static::$macros[$name]); + } + + /** + * Flush the existing macros. + * + * @return void + */ + public static function flushMacros() + { + static::$macros = []; + } + + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + public static function __callStatic($method, $parameters) + { + if (! static::hasMacro($method)) { + throw new BadMethodCallException(sprintf( + 'Method %s::%s does not exist.', static::class, $method + )); + } + + $macro = static::$macros[$method]; + + if ($macro instanceof Closure) { + $macro = $macro->bindTo(null, static::class); + } + + return $macro(...$parameters); + } + + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + if (! static::hasMacro($method)) { + throw new BadMethodCallException(sprintf( + 'Method %s::%s does not exist.', static::class, $method + )); + } + + $macro = static::$macros[$method]; + + if ($macro instanceof Closure) { + $macro = $macro->bindTo($this, static::class); + } + + return $macro(...$parameters); + } +} diff --git a/tests/Support/Traits/MacroableTest.php b/tests/Support/Traits/MacroableTest.php new file mode 100644 index 0000000..d2481ca --- /dev/null +++ b/tests/Support/Traits/MacroableTest.php @@ -0,0 +1,201 @@ +macroable = $this->createObjectForTrait(); + } + + private function createObjectForTrait() + { + return new EmptyMacroable; + } + + public function testRegisterMacro() + { + $macroable = $this->macroable; + $macroable::macro(__CLASS__, function () { + return 'Taylor'; + }); + $this->assertSame('Taylor', $macroable::{__CLASS__}()); + } + + public function testHasMacro() + { + $macroable = $this->macroable; + $macroable::macro('foo', function () { + return 'Taylor'; + }); + $this->assertTrue($macroable::hasMacro('foo')); + $this->assertFalse($macroable::hasMacro('bar')); + } + + public function testRegisterMacroAndCallWithoutStatic() + { + $macroable = $this->macroable; + $macroable::macro(__CLASS__, function () { + return 'Taylor'; + }); + $this->assertSame('Taylor', $macroable->{__CLASS__}()); + } + + public function testWhenCallingMacroClosureIsBoundToObject() + { + TestMacroable::macro('tryInstance', function () { + return $this->protectedVariable; + }); + TestMacroable::macro('tryStatic', function () { + return static::getProtectedStatic(); + }); + $instance = new TestMacroable; + + $result = $instance->tryInstance(); + $this->assertSame('instance', $result); + + $result = TestMacroable::tryStatic(); + $this->assertSame('static', $result); + } + + public function testClassBasedMacros() + { + TestMacroable::mixin(new TestMixin); + $instance = new TestMacroable; + $this->assertSame('instance-Adam', $instance->methodOne('Adam')); + } + + public function testClassBasedMacrosNoReplace() + { + TestMacroable::macro('methodThree', function () { + return 'bar'; + }); + TestMacroable::mixin(new TestMixin, false); + $instance = new TestMacroable; + $this->assertSame('bar', $instance->methodThree()); + + TestMacroable::mixin(new TestMixin); + $this->assertSame('foo', $instance->methodThree()); + } + + public function testFlushMacros() + { + TestMacroable::macro('flushMethod', function () { + return 'flushMethod'; + }); + + $instance = new TestMacroable; + + $this->assertSame('flushMethod', $instance->flushMethod()); + + TestMacroable::flushMacros(); + + $this->expectException(BadMethodCallException::class); + + $instance->flushMethod(); + } + + public function testFlushMacrosStatic() + { + TestMacroable::macro('flushMethod', function () { + return 'flushMethod'; + }); + + $instance = new TestMacroable; + + $this->assertSame('flushMethod', $instance::flushMethod()); + + TestMacroable::flushMacros(); + + $this->expectException(BadMethodCallException::class); + + $instance::flushMethod(); + } + + public function testMacroWithArguments() + { + $this->macroable::macro('concatenate', function ($arg1, $arg2) { + return $arg1.' '.$arg2; + }); + + $result = $this->macroable::concatenate('Hello', 'World'); + $this->assertSame('Hello World', $result); + } + + public function testMacroWithDefaultArguments() + { + $this->macroable::macro('greet', function ($name = 'Guest') { + return 'Hello, '.$name; + }); + + $this->assertSame('Hello, Guest', $this->macroable::greet()); + $this->assertSame('Hello, Saleh', $this->macroable::greet('Saleh')); + } + + public function testCallingUndefinedMacroThrowsException() + { + $this->expectException(BadMethodCallException::class); + + $this->macroable::nonExistentMacro(); + } + + public function testMethodConflictDoesNotThrowException() + { + $this->macroable::macro('existingMethod', function () { + return 'oldMethod'; + }); + + // Replacing existing macro. + $this->macroable::macro('existingMethod', function () { + return 'newMethod'; + }); + + $this->assertSame('newMethod', $this->macroable::existingMethod()); + } +} + +class EmptyMacroable +{ + use Macroable; +} + +class TestMacroable +{ + use Macroable; + + protected $protectedVariable = 'instance'; + + protected static function getProtectedStatic() + { + return 'static'; + } +} + +class TestMixin +{ + public function methodOne() + { + return function ($value) { + return $this->methodTwo($value); + }; + } + + protected function methodTwo() + { + return function ($value) { + return $this->protectedVariable.'-'.$value; + }; + } + + protected function methodThree() + { + return function () { + return 'foo'; + }; + } +}