Skip to content

Commit

Permalink
Add Controller and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dmjohnsson23 committed Apr 11, 2024
1 parent 924ed1b commit e202d34
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 7 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ Contemplate

This is an extended fork of [Plates](https://github.com/thephpleague/plates) that adds support for additional functionality, such as:

* Loading controllers (or, any arbitrary function or object) using the same loader used to load templates
* Loading static resources (but *not* public web assets...for now) using the same loader used to load templates
* Name-based associations between templates, controllers, and resources
* Loading controllers (or, any arbitrary function or object) using the same loader used to load templates.
* Loading static resources (but *not* public web assets...for now) using the same loader used to load templates.
* Name-based associations between templates, controllers, and resources.

Plates is a very handy little project, but doesn't appear to be receiving new features or responding to pull requests. Contemplate is a drop-in replacement for Plates; you should be able to simply change the import, and everything should "just work". You can then add additional features over time using Contemplate's extended functionality.
Plates is a very handy little project, but doesn't appear to be receiving new features or responding to pull requests. Contemplate is a drop-in replacement for Plates; you should be able to simply change the import, and everything should "just work" so long as you don't have any custom template functions whose names interfere with new methods added by Contemplate. You can then add additional features over time using Contemplate's extended functionality.

Loading controllers and resources via the template loader system has a few advantages:

* Organization: it's nice to have all the code for a request live close together in your project structure.
* Extensibility and modularity: Using Themes, you can override the functionality of certain controllers or resources for a specific theme, but fall back to the base theme if an override does not exist.

## Documentation

Expand Down
68 changes: 68 additions & 0 deletions src/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DMJohnson\Contemplate;

use DMJohnson\Contemplate\Extension\ExtensionInterface;
use DMJohnson\Contemplate\Template\Controller;
use DMJohnson\Contemplate\Template\Data;
use DMJohnson\Contemplate\Template\Directory;
use DMJohnson\Contemplate\Template\FileExtension;
Expand Down Expand Up @@ -352,4 +353,71 @@ public function import($name, array $data=[], $type = null)
{
return $this->resolve($name, $type)->import($data);
}

/**
* Load one of the controller logic files that accompany the named template,
* and return its value.
*
* @param string $name The name of the controller to locate
* @param string|null $type An optional value specifying the type of object to resolve. This
* is used to allow multiple types of `Resolvable`s to exist under the same name (e.g. a
* template, multiple controllers, static resources, etc...).
* @return Controller
*/
public function makeController($name, $type=null)
{
return new Controller($this, $name, $type);
}

/**
* Load one of the controller logic files that accompany the named template, call the function,
* and return its value.
*
* @param string $name The name of the controller to locate
* @param string|null $type An optional value specifying the type of object to resolve. This
* is used to allow multiple types of `Resolvable`s to exist under the same name (e.g. a
* template, multiple controllers, static resources, etc...).
* @param array $params Function parameters to pass to the controller function
* @return mixed The return value of the controller function
*/
public function callController($name, $type=null, array $params=[])
{
return $this->makeController($name, $type)->call($params);
}

/**
* Automated method to call the given controller, assuming your controllers have been named
* with HTTP verb suffixes. Echoes the controller's return value (which is presumed to be a
* rendered template).
*
* @param string $name The name of the controller to locate
* @param array $params Function parameters to pass to the controller function
* @return mixed The return value of the controller function
*/
public function autoCallHttpController($name, array $params=[])
{
$req_method = \strtoupper($_SERVER['REQUEST_METHOD']);
if ($req_method === 'GET') {
$type = Resolvable::TYPE_CONTROLLER_GET;
}
elseif ($req_method === 'POST') {
$type = Resolvable::TYPE_CONTROLLER_POST;
}
elseif ($req_method === 'PUT') {
$type = Resolvable::TYPE_CONTROLLER_PUT;
}
elseif ($req_method === 'DELETE') {
$type = Resolvable::TYPE_CONTROLLER_DELETE;
}
elseif ($req_method === 'PATCH') {
$type = Resolvable::TYPE_CONTROLLER_PATCH;
}
elseif ($req_method === 'HEAD') {
$type = Resolvable::TYPE_CONTROLLER_HEAD;
}
else {
$type = $req_method;
}
echo $this->makeController($name, $type)->call($params);
}
}
52 changes: 52 additions & 0 deletions src/Template/Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
namespace DMJohnson\Contemplate\Template;

/**
* Wrapper for controller objects.
*
* The controller should be a function or other callable which can be imported via the resolver.
*/
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);
}

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

/** Shortcut for `$this->engine->addData()` */
public function addData(array $data=[], $templates=null){
return $this->engine->addData($data, $templates);
}

/** Add data for the template with the same name as this controller */
public function addDataAssociated(array $data=[]){
return $this->engine->addData($data, $this->name->getName());
}

/**
* Delegate this action, or part of this action, to a different controller
* (e.g. a form handler for a specific form on a page)
*
* @param string $name The controller to delegate to
* @param string|null $type The controller type to use
*/
public function delegate($name, array $params = [], $type = Resolvable::TYPE_CONTROLLER_DELEGATE){
return $this->engine->callController($name, $type, $params);
}

/**
* Delegate this action, or part of this action, to the controller with the same name, but
* of a different type (e.g. call a GET or DELETE controller from the POST controller)
*
* @param string $type The controller type to use.
*/
public function delegateAssociated(array $params = [], $type = Resolvable::TYPE_CONTROLLER_DELEGATE){
return $this->engine->callController($this->name->getName(), $type, $params);
}
}
35 changes: 32 additions & 3 deletions src/Template/Resolvable.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,38 @@
use Throwable;

/**
* Container which holds template data and provides access to template functions.
* Generic base container for all kinds of resolvable objects (Templates, Controllers, resources, etc...).
*/
class Resolvable
{
/**
* Instance of the template engine.
* Instance of the Contemplate engine.
* @var Engine
*/
protected $engine;

/**
* The name of the template.
* The name of the resolvable.
* @var Name
*/
protected $name;

/** A normal template file */
const TYPE_TEMPLATE = '__TEMPLATE__';
/** A controller which should return a string as a response to an HTTP GET request */
const TYPE_CONTROLLER_GET = '__HTTP_GET__';
/** A controller which should run as a response to an HTTP GET request */
const TYPE_CONTROLLER_HEAD = '__HTTP_HEAD__';
/** A controller which should return a string as a response to an HTTP POST request */
const TYPE_CONTROLLER_POST = '__HTTP_POST__';
/** A controller which should return a string as a response to an HTTP PUT request */
const TYPE_CONTROLLER_PUT = '__HTTP_PUT__';
/** A controller which should return a string as a response to an HTTP DELETE request */
const TYPE_CONTROLLER_DELETE = '__HTTP_DELETE__';
/** A controller which should return a string as a response to an HTTP PATCH request */
const TYPE_CONTROLLER_PATCH = '__HTTP_PATCH__';
/** A controller which is intended to be called by other controllers */
const TYPE_CONTROLLER_DELEGATE = '__DELEGATE__';

/**
* Create new Resolvable instance.
Expand All @@ -43,6 +56,22 @@ public function __construct(Engine $engine, $name, $type=null)
$this->name = new Name($engine, $name, $type);
}

/**
* @return Engine
*/
public function getEngine()
{
return $this->engine;
}

/**
* @return Name
*/
public function getName()
{
return $this->name;
}

/**
* Magic method used to call extension functions.
* @param string $name
Expand Down
11 changes: 11 additions & 0 deletions tests/EngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,15 @@ public function testRenderTemplate()

$this->assertSame('Hello!', $this->engine->render('template'));
}

public function testCallController()
{
vfsStream::create(
array(
'controller.php' => '<?php return function(){return "Hello!";};',
)
);

$this->assertSame('Hello!', $this->engine->callController('controller'));
}
}
152 changes: 152 additions & 0 deletions tests/Template/ControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace DMJohnson\Contemplate\Tests\Template;

use DMJohnson\Contemplate\Engine;
use DMJohnson\Contemplate\Template\Controller;
use DMJohnson\Contemplate\Template\Resolvable;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;

class ControllerTest extends TestCase
{
private $controller;

protected function setUp(): void
{
vfsStream::setup('templates');

$engine = new Engine(vfsStream::url('templates'));
$engine->setFileExtension('delegate.php', Resolvable::TYPE_CONTROLLER_DELEGATE);
$engine->setFileExtension('get.php', Resolvable::TYPE_CONTROLLER_GET);
$engine->setFileExtension('post.php', Resolvable::TYPE_CONTROLLER_POST);

$this->controller = new Controller($engine, 'controller');
}

public function testCanCreateInstance()
{
$this->assertInstanceOf(Controller::class, $this->controller);
}

public function testCall()
{
vfsStream::create(
array(
'controller.php' => '<?php return function(){return "Hello World";};',
)
);

$this->assertSame('Hello World', $this->controller->call());
}

public function testCallWithParameters()
{
vfsStream::create(
array(
'controller.php' => '<?php return function($string){return $string;};',
)
);

$this->assertSame('Hello World', $this->controller->call(['Hello World']));
}

public function testCallWithParametersViaInvoke()
{
vfsStream::create(
array(
'controller.php' => '<?php return function($string){return $string;};',
)
);

$this->assertSame('Hello World', ($this->controller)('Hello World'));
}

public function testCallDoesNotExist()
{
// The template "controller" could not be found at "vfs://templates/controller.php".
$this->expectException(\LogicException::class);
var_dump($this->controller->call());
}

public function testCallException()
{
// error
$this->expectException('Exception');
vfsStream::create(
array(
'controller.php' => '<?php return function(){throw new Exception("error");}; ?>',
)
);
var_dump($this->controller->call());
}

public function testCallDoesNotLeakVariables()
{
vfsStream::create(
array(
'controller.php' => '<?php $defined = get_defined_vars(); return function() use ($defined){return $defined;};',
)
);

$this->assertSame([], $this->controller->call());
}

public function testDelegate()
{
vfsStream::create(
array(
'other.delegate.php' => '<?php return function(){return "Delegate to the delegate";};',
)
);

$this->assertSame('Delegate to the delegate', $this->controller->delegate('other'));
}

public function testDelegateAssociated()
{
vfsStream::create(
array(
'controller.delegate.php' => '<?php return function(){return "Delegate to the delegate";};',
)
);

$this->assertSame('Delegate to the delegate', $this->controller->delegateAssociated());
}

public function testDelegateAssociatedWithType()
{
vfsStream::create(
array(
'controller.get.php' => '<?php return function(){return "Delegate to the delegate";};',
)
);

$this->assertSame('Delegate to the delegate', $this->controller->delegateAssociated(type:Resolvable::TYPE_CONTROLLER_GET));
}

public function testDelegateWithParams()
{
vfsStream::create(
array(
'other.delegate.php' => '<?php return function($string){return $string;};',
)
);

$this->assertSame('Delegate to the delegate', $this->controller->delegate('other', ['Delegate to the delegate']));
}

public function testDelegateAssociatedWithParams()
{
vfsStream::create(
array(
'controller.delegate.php' => '<?php return function($string){return $string;};',
)
);

$this->assertSame('Delegate to the delegate', $this->controller->delegateAssociated(['Delegate to the delegate']));
}

}

0 comments on commit e202d34

Please sign in to comment.