Skip to content

Commit

Permalink
Add a decorator class that can be extended to add additional function…
Browse files Browse the repository at this point in the history
…ality to controllers
  • Loading branch information
Dominick Johnson committed Sep 6, 2024
1 parent 4537e69 commit 86e1a13
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
}
],
"require" : {
"php": "^7.0|^8.0"
"php": "^8.0"
},
"suggest": {
"twig/twig": "For the ContemplateTwig bridge extension"
Expand Down
4 changes: 2 additions & 2 deletions src/Template/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ class Controller extends Resolvable{
* Execute the controller code and return its value
*/
public function call(array $args = []){
return call_user_func_array($this->import(), $args);
return ControllerDecorator::callDecorated($this->import(), $args);
}

/** Alias for call() */
public function __invoke(...$args){
return call_user_func_array($this->import(), $args);
return ControllerDecorator::callDecorated($this->import(), $args);
}

/** Shortcut for `$this->engine->addData()` */
Expand Down
58 changes: 58 additions & 0 deletions src/Template/ControllerDecorator.php
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);
}
}
115 changes: 115 additions & 0 deletions tests/Template/ControllerDecoratorTest.php
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
// );
// }
}

0 comments on commit 86e1a13

Please sign in to comment.