forked from thephpleague/plates
-
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.
Add a decorator class that can be extended to add additional function…
…ality to controllers
- Loading branch information
Dominick Johnson
committed
Sep 6, 2024
1 parent
4537e69
commit 86e1a13
Showing
4 changed files
with
176 additions
and
3 deletions.
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,58 @@ | ||
<?php | ||
|
||
namespace DMJohnson\Contemplate\Template; | ||
|
||
use Attribute; | ||
use Closure; | ||
use ReflectionAttribute; | ||
use ReflectionFunction; | ||
use ReflectionObject; | ||
|
||
/** | ||
* An attribute used to add additional functionality to a controller. | ||
* | ||
* Extend this class and overwrite the `__invoke` method. | ||
*/ | ||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] | ||
abstract class ControllerDecorator{ | ||
/** | ||
* Called to wrap the controller | ||
* | ||
* @param object|callable The function or class being decorated | ||
* @param callable $next A callable that you should call to invoke the next decorator in the | ||
* chain or, if this is the last decorator, the target controller itself. You should pass | ||
* `$args` or a modified version of `$args` to this callable when you call it. | ||
* @param array $args The arguments being passed to the controller, either from the main | ||
* caller or the previous decorator in the chain. | ||
* @return mixed The return value. Usually this will be the value returned by `$next($args)`, | ||
* but this is not required. | ||
*/ | ||
public function __invoke($target, $next, $args){ | ||
return $next($args); | ||
} | ||
|
||
/** | ||
* Execute a function or callable class along with all its decorators | ||
*/ | ||
public static function callDecorated($target, $args){ | ||
if (is_object($target) && !$target instanceof Closure){ | ||
$reflectionObject = new ReflectionObject($target); | ||
$attributes = $reflectionObject->getAttributes(ControllerDecorator::class, ReflectionAttribute::IS_INSTANCEOF); | ||
} | ||
elseif ((is_string($target) && is_callable($target)) || $target instanceof Closure){ | ||
$reflectionFunction = new ReflectionFunction($target); | ||
$attributes = $reflectionFunction->getAttributes(ControllerDecorator::class, ReflectionAttribute::IS_INSTANCEOF); | ||
} | ||
else{ | ||
$attributes = []; | ||
} | ||
$next = function(array $args) use ($target){return $target(...$args);}; | ||
foreach (array_reverse($attributes) as $attr){ | ||
$decorator = $attr->newInstance(); | ||
$next = function(array $args) use ($decorator, $target, $next){ | ||
return $decorator($target, $next, $args); | ||
}; | ||
} | ||
return $next($args); | ||
} | ||
} |
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,115 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace DMJohnson\Contemplate\Tests\Template; | ||
|
||
use Attribute; | ||
use DMJohnson\Contemplate\Template\ControllerDecorator; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
|
||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] | ||
class Dec1 extends ControllerDecorator | ||
{ | ||
public function __invoke($target, $next, $args) | ||
{ | ||
$args[0][] = 'enter 1'; | ||
$result = $next($args); | ||
$result[] = 'exit 1'; | ||
return $result; | ||
} | ||
} | ||
|
||
|
||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] | ||
class Dec2 extends ControllerDecorator | ||
{ | ||
public function __invoke($target, $next, $args) | ||
{ | ||
$args[0][] = 'enter 2'; | ||
$result = $next($args); | ||
$result[] = 'exit 2'; | ||
return $result; | ||
} | ||
} | ||
|
||
|
||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] | ||
class Psych extends ControllerDecorator | ||
{ | ||
public function __invoke($target, $next, $args) | ||
{ | ||
return 'You thought we had to call the real function? Psych!'; | ||
} | ||
} | ||
|
||
|
||
class ControllerDecoratorTest extends TestCase | ||
{ | ||
public function testCallOrder() | ||
{ | ||
$stack = ControllerDecorator::callDecorated( | ||
#[Dec1] | ||
#[Dec2] | ||
function($stack){ | ||
$stack[] = 'target'; | ||
return $stack; | ||
}, | ||
[['initial']] | ||
); | ||
$this->assertSame( | ||
['initial', 'enter 1', 'enter 2', 'target', 'exit 2', 'exit 1'], | ||
$stack | ||
); | ||
} | ||
|
||
public function testShortCircuit() | ||
{ | ||
$result = ControllerDecorator::callDecorated( | ||
#[Psych] | ||
function(){ | ||
return 'I will never be called'; | ||
}, | ||
[] | ||
); | ||
$this->assertSame( | ||
'You thought we had to call the real function? Psych!', | ||
$result | ||
); | ||
} | ||
|
||
public function testUndecoratedFunction() | ||
{ | ||
$result = ControllerDecorator::callDecorated( | ||
function(){ | ||
return 'I am totally normal'; | ||
}, | ||
[] | ||
); | ||
$this->assertSame( | ||
'I am totally normal', | ||
$result | ||
); | ||
} | ||
|
||
// TODO this syntax doesn't work--seems like PHP doesn't allow attributes on anonymous classes? | ||
// public function testDecoratedClass() | ||
// { | ||
// $stack = ControllerDecorator::callDecorated( | ||
// #[Dec1] | ||
// #[Dec2] | ||
// new class{ | ||
// public function __invoke($stack){ | ||
// $stack[] = 'target'; | ||
// return $stack; | ||
// } | ||
// }, | ||
// [['initial']] | ||
// ); | ||
// $this->assertSame( | ||
// ['initial', 'enter 1', 'enter 2', 'target', 'exit 2', 'exit 1'], | ||
// $stack | ||
// ); | ||
// } | ||
} |