mirror of
https://github.com/marcostoll/FF.git
synced 2025-03-21 15:40:00 +01:00
[FEATURE] add templating interface and twig
This commit is contained in:
parent
ed0d8c5854
commit
f247fb692e
@ -15,7 +15,12 @@
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"fastforward/data-structures": "^1.0.0",
|
||||
"fastforward/factories": "^1.2.0"
|
||||
"fastforward/factories": "^1.2.0",
|
||||
"symfony/config": "~4.3",
|
||||
"symfony/http-foundation": "~4.3",
|
||||
"symfony/routing": "~4.3",
|
||||
"symfony/yaml": "~4.3",
|
||||
"twig/twig": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8"
|
||||
|
43
src/Events/Templating/PostRender.php
Normal file
43
src/Events/Templating/PostRender.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PostRender
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Templating;
|
||||
|
||||
use FF\Events\AbstractEvent;
|
||||
use FF\Services\Templating\RenderedDocument;
|
||||
|
||||
/**
|
||||
* Class PostRender
|
||||
*
|
||||
* @package FF\Events\Templating
|
||||
*/
|
||||
class PostRender extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var RenderedDocument
|
||||
*/
|
||||
protected $doc;
|
||||
|
||||
/**
|
||||
* @param RenderedDocument $doc
|
||||
*/
|
||||
public function __construct(RenderedDocument $doc)
|
||||
{
|
||||
$this->doc = $doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RenderedDocument
|
||||
*/
|
||||
public function getDoc(): RenderedDocument
|
||||
{
|
||||
return $this->doc;
|
||||
}
|
||||
}
|
58
src/Events/Templating/PreRender.php
Normal file
58
src/Events/Templating/PreRender.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PreRender
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Templating;
|
||||
|
||||
use FF\DataStructures\Record;
|
||||
use FF\Events\AbstractEvent;
|
||||
|
||||
/**
|
||||
* Class PreForward
|
||||
*
|
||||
* @package FF\Events\Templating
|
||||
*/
|
||||
class PreRender extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* @var Record
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @param Record $data
|
||||
*/
|
||||
public function __construct(string $template, Record $data)
|
||||
{
|
||||
$this->template = $template;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Record
|
||||
*/
|
||||
public function getData(): Record
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
21
src/Services/Templating/Exceptions/RenderingException.php
Normal file
21
src/Services/Templating/Exceptions/RenderingException.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of RenderingException
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Templating\Exceptions;
|
||||
|
||||
/**
|
||||
* Class RenderingException
|
||||
*
|
||||
* @package FF\Services\Templating\Exceptions
|
||||
*/
|
||||
class RenderingException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
50
src/Services/Templating/RenderedDocument.php
Normal file
50
src/Services/Templating/RenderedDocument.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of RenderedDocument
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Templating;
|
||||
|
||||
/**
|
||||
* Class RenderedDocument
|
||||
*
|
||||
* @package FF\Services\Templating
|
||||
*/
|
||||
class RenderedDocument
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $contents;
|
||||
|
||||
/**
|
||||
* @param string $contents
|
||||
*/
|
||||
public function __construct(string $contents)
|
||||
{
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContents(): string
|
||||
{
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $contents
|
||||
* @return $this
|
||||
*/
|
||||
public function setContents(string $contents)
|
||||
{
|
||||
$this->contents = $contents;
|
||||
return $this;
|
||||
}
|
||||
}
|
33
src/Services/Templating/TemplateRendererInterface.php
Normal file
33
src/Services/Templating/TemplateRendererInterface.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of TemplateRendererInterface
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Templating;
|
||||
|
||||
use FF\Services\Templating\Exceptions\RenderingException;
|
||||
|
||||
/**
|
||||
* Interface TemplateRendererInterface
|
||||
*
|
||||
* @package FF\Services\Templating
|
||||
*/
|
||||
interface TemplateRendererInterface
|
||||
{
|
||||
/**
|
||||
* Renders a template using the given data
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return string
|
||||
* @throws RenderingException
|
||||
* @fires Templating\PreRender
|
||||
* @fires Templating\PostRender
|
||||
*/
|
||||
public function render(string $template, array $data): string;
|
||||
}
|
186
src/Services/Templating/TwigRenderer.php
Normal file
186
src/Services/Templating/TwigRenderer.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of TwigRenderer
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Templating;
|
||||
|
||||
use FF\DataStructures\Record;
|
||||
use FF\Services\AbstractService;
|
||||
use FF\Services\Templating\Exceptions\RenderingException;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\Error;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
use Twig\RuntimeLoader\RuntimeLoaderInterface;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Class TwigRenderer
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - twig-options : array (default: []) - options to configure twig environment
|
||||
* - template-dir : string - path to templates folder
|
||||
* - fire-events : bool (default: false) - whether to fire rendering events
|
||||
*
|
||||
* @method void addRuntimeLoader(RuntimeLoaderInterface $loader)
|
||||
* @method void addExtension(ExtensionInterface $extension)
|
||||
* @method void addTokenParser(TokenParserInterface $parser)
|
||||
* @method void addNodeVisitor(NodeVisitorInterface $visitor)
|
||||
* @method void addFilter(TwigFilter $filter)
|
||||
* @method void addTest(TwigTest $test)
|
||||
* @method void addFunction(TwigFunction $function)
|
||||
* @method void addGlobal(string $name, mixed $value)
|
||||
*
|
||||
* @package FF\Services\Templating
|
||||
*
|
||||
* @link https://twig.symfony.com/doc/2.x/api.html#environment-options Twig Environment options
|
||||
* @link https://twig.symfony.com/doc/2.x/advanced.html Extending Twig
|
||||
*/
|
||||
class TwigRenderer extends AbstractService implements TemplateRendererInterface
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $fireEvents;
|
||||
|
||||
/**
|
||||
* @return Environment
|
||||
*/
|
||||
public function getTwig(): Environment
|
||||
{
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Environment $twig
|
||||
* @return $this
|
||||
*/
|
||||
public function setTwig(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getFireEvents(): bool
|
||||
{
|
||||
return $this->fireEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $fireEvents
|
||||
* @return $this
|
||||
*/
|
||||
public function setFireEvents(bool $fireEvents)
|
||||
{
|
||||
$this->fireEvents = $fireEvents;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a template using the given data
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return string
|
||||
* @throws RenderingException
|
||||
* @fires Templating\PreRender
|
||||
* @fires Templating\PostRender
|
||||
*/
|
||||
public function render(string $template, array $data): string
|
||||
{
|
||||
if ($this->fireEvents) {
|
||||
// wrap data in object to support data manipulation via event listener
|
||||
$record = new Record($data);
|
||||
$this->fire('Templating\PreRender', $template, $record);
|
||||
$data = $record->getDataAsArray();
|
||||
}
|
||||
|
||||
try {
|
||||
$contents = $this->twig->render($template, $data);
|
||||
} catch (Error $e) {
|
||||
throw new RenderingException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
if ($this->fireEvents) {
|
||||
// wrap data in object to support data manipulation via event listener
|
||||
$doc = new RenderedDocument($contents);
|
||||
$this->fire('Templating\PostRender', $doc);
|
||||
$contents = $doc->getContents();
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic proxy for the public api of Twig\Environment
|
||||
*
|
||||
* Routes the method call to the twig environment instance encapsulated
|
||||
* within this service.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
* @throws \BadMethodCallException Method is not defined or accessible on Twig\Environment
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
$callable = [$this->twig, $name];
|
||||
if (!is_callable($callable)) {
|
||||
// trigger fatal error: unsupported method call
|
||||
// mimic standard php error message
|
||||
// Fatal error: Call to undefined method {class}::{method}() in {file} on line {line}
|
||||
$backTrace = debug_backtrace();
|
||||
$errorMsg = 'Call to undefined method ' . __CLASS__ . '::' . $name . '() '
|
||||
. 'in ' . $backTrace[0]['file'] . ' on line ' . $backTrace[0]['line'];
|
||||
trigger_error($errorMsg, E_USER_ERROR);
|
||||
}
|
||||
|
||||
return call_user_func_array($callable, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function initialize(array $options)
|
||||
{
|
||||
parent::initialize($options);
|
||||
|
||||
$this->fireEvents = $this->getOption('fire-events', false);
|
||||
|
||||
$loader = new FilesystemLoader($this->getOption('template-dir'));
|
||||
$this->twig = new Environment($loader, $this->getOption('twig-options', []));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function validateOptions(array $options, array &$errors): bool
|
||||
{
|
||||
if (!isset($options['template-dir']) || empty($options['template-dir'])) {
|
||||
$errors[] = 'missing or empty mandatory option [template-dir]';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
181
tests/Services/Templating/TwigRendererTest.php
Normal file
181
tests/Services/Templating/TwigRendererTest.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of TwigRendererTest
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Tests\Services\Templating;
|
||||
|
||||
use FF\Events\AbstractEvent;
|
||||
use FF\Events\Templating\PostRender;
|
||||
use FF\Events\Templating\PreRender;
|
||||
use FF\Factories\ServicesFactory;
|
||||
use FF\Factories\SF;
|
||||
use FF\Services\Events\EventBroker;
|
||||
use FF\Services\Exceptions\ConfigurationException;
|
||||
use FF\Services\Templating\Exceptions\RenderingException;
|
||||
use FF\Services\Templating\TwigRenderer;
|
||||
use PHPUnit\Framework\Error\Error;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
|
||||
/**
|
||||
* Test TwigRendererTest
|
||||
*
|
||||
* @package FF\Tests
|
||||
*/
|
||||
class TwigRendererTest extends TestCase
|
||||
{
|
||||
const DEFAULT_OPTIONS = [
|
||||
'template-dir' => __DIR__ . '/templates'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var TwigRenderer
|
||||
*/
|
||||
protected $uut;
|
||||
|
||||
/**
|
||||
* @var AbstractEvent[]
|
||||
*/
|
||||
protected static $lastEvents;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
SF::setInstance(new ServicesFactory());
|
||||
|
||||
/** @var EventBroker $eventBroker */
|
||||
$eventBroker = SF::i()->get('Events\EventBroker');
|
||||
|
||||
// register test listener
|
||||
$eventBroker
|
||||
->subscribe([__CLASS__, 'listener'], 'Templating\PreRender')
|
||||
->subscribe([__CLASS__, 'listener'], 'Templating\PostRender');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->uut = new TwigRenderer(self::DEFAULT_OPTIONS);
|
||||
|
||||
self::$lastEvents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy event listener
|
||||
*
|
||||
* @param AbstractEvent $event
|
||||
*/
|
||||
public static function listener(AbstractEvent $event)
|
||||
{
|
||||
self::$lastEvents[get_class($event)] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testInitializeErrorMandatoryOptions()
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
|
||||
new TwigRenderer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testSetGetTwig()
|
||||
{
|
||||
$value = new Environment(new ArrayLoader());
|
||||
$same = $this->uut->setTwig($value);
|
||||
$this->assertSame($this->uut, $same);
|
||||
$this->assertEquals($value, $this->uut->getTwig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testSetGetFireEvents()
|
||||
{
|
||||
$value = false;
|
||||
$same = $this->uut->setFireEvents($value);
|
||||
$this->assertSame($this->uut, $same);
|
||||
$this->assertEquals($value, $this->uut->getFireEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testRenderWithoutEvents()
|
||||
{
|
||||
$doc = $this->uut->setFireEvents(false)
|
||||
->render('basic.html.twig', ['foo' => 'bar']);
|
||||
$this->assertEquals('foo: bar', $doc);
|
||||
|
||||
$this->assertEmpty(self::$lastEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testRenderWithEvents()
|
||||
{
|
||||
$doc = $this->uut->setFireEvents(true)
|
||||
->render('basic.html.twig', ['foo' => 'bar']);
|
||||
$this->assertEquals('foo: bar', $doc);
|
||||
|
||||
$this->assertArrayHasKey(PreRender::class, self::$lastEvents);
|
||||
$this->assertArrayHasKey(PostRender::class, self::$lastEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testRenderErrorLoader()
|
||||
{
|
||||
$this->expectException(RenderingException::class);
|
||||
|
||||
$this->uut->render('missing.html.twig', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testRenderErrorSyntax()
|
||||
{
|
||||
$this->expectException(RenderingException::class);
|
||||
|
||||
$this->uut->render('invalid.html.twig', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testMagicCall()
|
||||
{
|
||||
$this->uut->addGlobal('foo', 'bar');
|
||||
|
||||
$doc = $this->uut->render('basic.html.twig', []);
|
||||
$this->assertEquals('foo: bar', $doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testMagicCallUnknown()
|
||||
{
|
||||
$this->expectException(Error::class);
|
||||
|
||||
$this->uut->foo();
|
||||
}
|
||||
}
|
1
tests/Services/Templating/templates/basic.html.twig
Normal file
1
tests/Services/Templating/templates/basic.html.twig
Normal file
@ -0,0 +1 @@
|
||||
foo: {{ foo }}
|
2
tests/Services/Templating/templates/invalid.html.twig
Normal file
2
tests/Services/Templating/templates/invalid.html.twig
Normal file
@ -0,0 +1,2 @@
|
||||
{% for %}
|
||||
{% endif %}
|
Loading…
x
Reference in New Issue
Block a user