-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
376 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<?php | ||
|
||
namespace SlimFacades\Support\Traits; | ||
|
||
use BadMethodCallException; | ||
use Closure; | ||
use ReflectionClass; | ||
use ReflectionMethod; | ||
|
||
trait Macroable | ||
{ | ||
/** | ||
* The registered string macros. | ||
* | ||
* @var array | ||
*/ | ||
protected static $macros = []; | ||
|
||
/** | ||
* Register a custom macro. | ||
* | ||
* @param string $name | ||
* @param object|callable $macro | ||
* @return void | ||
*/ | ||
public static function macro($name, $macro) | ||
{ | ||
static::$macros[$name] = $macro; | ||
} | ||
|
||
/** | ||
* Mix another object into the class. | ||
* | ||
* @param object $mixin | ||
* @param bool $replace | ||
* @return void | ||
* | ||
* @throws \ReflectionException | ||
*/ | ||
public static function mixin($mixin, $replace = true) | ||
{ | ||
$methods = (new ReflectionClass($mixin))->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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
<?php declare(strict_types=1); | ||
|
||
use BadMethodCallException; | ||
use SlimFacades\Support\Traits\Macroable; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class SupportMacroableTest extends TestCase | ||
{ | ||
private $macroable; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->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'; | ||
}; | ||
} | ||
} |