Added Twig template renderer, closes #338

This commit is contained in:
Igor Scheller 2018-08-26 02:54:52 +02:00
parent a1bc763a16
commit bb3d16d273
20 changed files with 351 additions and 77 deletions

View File

@ -29,6 +29,7 @@
"symfony/http-foundation": "^3.3", "symfony/http-foundation": "^3.3",
"symfony/psr-http-message-bridge": "^1.0", "symfony/psr-http-message-bridge": "^1.0",
"twbs/bootstrap": "^3.3", "twbs/bootstrap": "^3.3",
"twig/twig": "^2.5",
"zendframework/zend-diactoros": "^1.7" "zendframework/zend-diactoros": "^1.7"
}, },
"require-dev": { "require-dev": {

View File

@ -10,6 +10,7 @@ return [
\Engelsystem\Config\ConfigServiceProvider::class, \Engelsystem\Config\ConfigServiceProvider::class,
\Engelsystem\Http\UrlGeneratorServiceProvider::class, \Engelsystem\Http\UrlGeneratorServiceProvider::class,
\Engelsystem\Renderer\RendererServiceProvider::class, \Engelsystem\Renderer\RendererServiceProvider::class,
\Engelsystem\Renderer\TwigServiceProvider::class,
\Engelsystem\Database\DatabaseServiceProvider::class, \Engelsystem\Database\DatabaseServiceProvider::class,
\Engelsystem\Http\RequestServiceProvider::class, \Engelsystem\Http\RequestServiceProvider::class,
\Engelsystem\Http\SessionServiceProvider::class, \Engelsystem\Http\SessionServiceProvider::class,

View File

@ -16,7 +16,7 @@ require __DIR__ . '/includes.php';
* Check for maintenance * Check for maintenance
*/ */
if ($app->get('config')->get('maintenance')) { if ($app->get('config')->get('maintenance')) {
echo file_get_contents(__DIR__ . '/../templates/maintenance.html'); echo file_get_contents(__DIR__ . '/../templates/layouts/maintenance.html');
die(); die();
} }

View File

@ -13,5 +13,5 @@ function credits_title()
*/ */
function guest_credits() function guest_credits()
{ {
return view(__DIR__ . '/../../templates/guest_credits.html'); return view(__DIR__ . '/../../templates/pages/credits.html');
} }

View File

@ -224,7 +224,7 @@ function view_user_shifts()
return page([ return page([
div('col-md-12', [ div('col-md-12', [
msg(), msg(),
view(__DIR__ . '/../../templates/user_shifts.html', [ view(__DIR__ . '/../../templates/pages/user-shifts.html', [
'title' => shifts_title(), 'title' => shifts_title(),
'room_select' => make_select($rooms, $shiftsFilter->getRooms(), 'rooms', _('Rooms')), 'room_select' => make_select($rooms, $shiftsFilter->getRooms(), 'rooms', _('Rooms')),
'start_select' => html_select_key( 'start_select' => html_select_key(

View File

@ -107,6 +107,7 @@ class Application extends Container
$this->instance('path', $appPath); $this->instance('path', $appPath);
$this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config'); $this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config');
$this->instance('path.lang', $appPath . DIRECTORY_SEPARATOR . 'locale'); $this->instance('path.lang', $appPath . DIRECTORY_SEPARATOR . 'locale');
$this->instance('path.views', $appPath . DIRECTORY_SEPARATOR . 'templates');
} }
/** /**

View File

@ -283,7 +283,7 @@ class LegacyMiddleware implements MiddlewareInterface
$content = info($content, true); $content = info($content, true);
} }
return response(view(__DIR__ . '/../../templates/layout.html', [ return response(view('layouts/app', [
'theme' => isset($user) ? $user['color'] : config('theme'), 'theme' => isset($user) ? $user['color'] : config('theme'),
'title' => $title, 'title' => $title,
'atom_link' => ($page == 'news' || $page == 'user_meetings') 'atom_link' => ($page == 'news' || $page == 'user_meetings')

View File

@ -0,0 +1,41 @@
<?php
namespace Engelsystem\Renderer;
use Twig_Environment as Twig;
use Twig_Error_Loader as LoaderError;
use Twig_Error_Runtime as RuntimeError;
use Twig_Error_Syntax as SyntaxError;
class TwigEngine implements EngineInterface
{
/** @var Twig */
protected $twig;
public function __construct(Twig $twig)
{
$this->twig = $twig;
}
/**
* Render a twig template
*
* @param string $path
* @param array $data
* @return string
* @throws LoaderError|RuntimeError|SyntaxError
*/
public function get($path, $data = [])
{
return $this->twig->render($path, $data);
}
/**
* @param string $path
* @return bool
*/
public function canRender($path)
{
return $this->twig->getLoader()->exists($path);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Engelsystem\Renderer;
use Twig_Error_Loader;
use Twig_Loader_Filesystem as FilesystemLoader;
class TwigLoader extends FilesystemLoader
{
/**
* @param string $name
* @param bool $throw
* @return false|string
* @throws Twig_Error_Loader
*/
public function findTemplate($name, $throw = true)
{
$extension = '.twig';
$extensionLength = strlen($extension);
if (substr($name, -$extensionLength, $extensionLength) !== $extension) {
$name .= $extension;
}
return parent::findTemplate($name, $throw);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Renderer;
use Engelsystem\Container\ServiceProvider;
use Twig_Environment as Twig;
use Twig_LoaderInterface as TwigLoaderInterface;
class TwigServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerTwigEngine();
}
protected function registerTwigEngine()
{
$viewsPath = $this->app->get('path.views');
$twigLoader = $this->app->make(TwigLoader::class, ['paths' => $viewsPath]);
$this->app->instance(TwigLoader::class, $twigLoader);
$this->app->instance(TwigLoaderInterface::class, $twigLoader);
$twig = $this->app->make(Twig::class);
$this->app->instance(Twig::class, $twig);
$twigEngine = $this->app->make(TwigEngine::class);
$this->app->instance('renderer.twigEngine', $twigEngine);
$this->app->tag('renderer.twigEngine', ['renderer.engine']);
}
}

View File

@ -5,8 +5,8 @@ use Engelsystem\Application;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Renderer\Renderer;
use Engelsystem\Http\UrlGenerator; use Engelsystem\Http\UrlGenerator;
use Engelsystem\Renderer\Renderer;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
/** /**
@ -139,7 +139,7 @@ function url($path = null, $parameters = [])
* @param mixed[] $data * @param mixed[] $data
* @return Renderer|string * @return Renderer|string
*/ */
function view($template = null, $data = null) function view($template = null, $data = [])
{ {
$renderer = app('renderer'); $renderer = app('renderer');

View File

@ -1,62 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>%title% - Engelsystem</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/theme%theme%.css"/>
<link rel="stylesheet" type="text/css" href="vendor/icomoon/style.css"/>
<link rel="stylesheet" type="text/css" href="vendor/bootstrap-datepicker-1.7.1/css/bootstrap-datepicker3.min.css"/>
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/jquery-ui.min.js"></script>
%atom_link%
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="%start_page_url%">
<span class="icon-icon_angel"></span> <strong class="visible-lg-inline">ENGELSYSTEM</strong>
</a>
</div>
<div class="collapse navbar-collapse" id="navbar-collapse-1">%menu% %header_toolbar%</div>
</div>
</div>
<div class="container-fluid">
<div class="row">%content%</div>
<div class="row" id="footer">
<div class="col-md-12">
<hr/>
<div class="text-center footer" style="margin-bottom: 10px;">
%event_info%
<a href="%faq_url%">FAQ</a>
· <a href="%contact_email%"><span class="glyphicon glyphicon-envelope"></span> Contact</a>
· <a href="https://github.com/engelsystem/engelsystem/issues">Bugs / Features</a>
· <a href="https://github.com/engelsystem/engelsystem/">Development Platform</a>
· <a href="%credits_url%">Credits</a>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker-1.7.1/js/bootstrap-datepicker.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker-1.7.1/locales/bootstrap-datepicker.de.min.js"></script>
<script type="text/javascript" src="vendor/Chart.min.js"></script>
<script type="text/javascript" src="js/forms.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.min.js"></script>
<script type="text/javascript">
$(function () {
moment.locale("%locale%");
});
</script>
<script type="text/javascript" src="js/moment-countdown.js"></script>
<script type="text/javascript" src="js/sticky-headers.js"></script>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}{{ title }}{% endblock %} - Engelsystem</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/theme{{ theme }}.css"/>
<link rel="stylesheet" type="text/css" href="vendor/icomoon/style.css"/>
<link rel="stylesheet" type="text/css" href="vendor/bootstrap-datepicker-1.7.1/css/bootstrap-datepicker3.min.css"/>
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/jquery-ui.min.js"></script>
{{ atom_link|raw }}
{% endblock %}
</head>
<body>
{% block body %}
<div class="navbar navbar-default navbar-fixed-top">
{% block header %}
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ start_page_url }}">
<span class="icon-icon_angel"></span> <strong class="visible-lg-inline">ENGELSYSTEM</strong>
</a>
</div>
{% block navbar %}
<div class="collapse navbar-collapse"
id="navbar-collapse-1">{{ menu|raw }} {{ header_toolbar|raw }}</div>
{% endblock %}
</div>
{% endblock %}
</div>
<div class="container-fluid">
<div class="row">{% block content %}{{ content|raw }}{% endblock %}</div>
<div class="row" id="footer">
{% block footer %}
<div class="col-md-12">
<hr/>
<div class="text-center footer" style="margin-bottom: 10px;">
{% block eventinfo %}
{{ event_info|raw }}
{% endblock %}
<a href="{{ faq_url }}">FAQ</a>
· <a href="{{ contact_email }}"><span class="glyphicon glyphicon-envelope"></span> Contact</a>
· <a href="https://github.com/engelsystem/engelsystem/issues">Bugs / Features</a>
· <a href="https://github.com/engelsystem/engelsystem/">Development Platform</a>
· <a href="{{ credits_url }}">Credits</a>
</div>
</div>
{% endblock %}
</div>
</div>
<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker-1.7.1/js/bootstrap-datepicker.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker-1.7.1/locales/bootstrap-datepicker.de.min.js"></script>
<script type="text/javascript" src="vendor/Chart.min.js"></script>
<script type="text/javascript" src="js/forms.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.min.js"></script>
<script type="text/javascript">
$(function () {
moment.locale("{{ locale|escape('js') }}");
});
</script>
<script type="text/javascript" src="js/moment-countdown.js"></script>
<script type="text/javascript" src="js/sticky-headers.js"></script>
{% endblock %}
</body>
</html>

View File

@ -6,15 +6,15 @@
<p> <p>
The original system was written by <a href="https://github.com/cookieBerlin/engelsystem">cookie</a>. The original system was written by <a href="https://github.com/cookieBerlin/engelsystem">cookie</a>.
It was then completely rewritten and enhanced by It was then completely rewritten and enhanced by
<a href="http://notrademark.de/">msquare</a> (maintainer), <a href="https://notrademark.de">msquare</a> (maintainer),
<a href="http://myigel.name/">MyIgel</a>, <a href="https://myigel.name">MyIgel</a>,
<a href="http://mortzu.de/">mortzu</a>, <a href="https://mortzu.de">mortzu</a>,
<a href="http://jplitza.de/">jplitza</a> and <a href="https://jplitza.de">jplitza</a> and
gnomus. <a href="https://github.com/gnomus">gnomus</a>.
</p> </p>
<p> <p>
Please look at the <a href="https://github.com/engelsystem/engelsystem/graphs/contributors">contributor Please look at the <a href="https://github.com/engelsystem/engelsystem/graphs/contributors">
list on github</a> for a more complete version. contributor list on github</a> for a more complete version.
</p> </p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@ -22,8 +22,8 @@
<p> <p>
Webspace, development platform and domain on <a href="https://engelsystem.de">engelsystem.de</a> Webspace, development platform and domain on <a href="https://engelsystem.de">engelsystem.de</a>
is currently provided by <a href="https://www.wybt.net/">would you buy this?</a> (ichdasich) is currently provided by <a href="https://www.wybt.net/">would you buy this?</a> (ichdasich)
and adminstrated by <a href="http://mortzu.de/">mortzu</a>, and adminstrated by <a href="https://mortzu.de">mortzu</a>,
<a href="http://derf.homelinux.org/">derf</a> and ichdasich. <a href="http://derf.homelinux.org">derf</a> and ichdasich.
</p> </p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">

View File

@ -48,6 +48,7 @@ class ApplicationTest extends TestCase
$this->assertTrue($app->has('path')); $this->assertTrue($app->has('path'));
$this->assertTrue($app->has('path.config')); $this->assertTrue($app->has('path.config'));
$this->assertTrue($app->has('path.lang')); $this->assertTrue($app->has('path.lang'));
$this->assertTrue($app->has('path.views'));
$this->assertEquals(realpath('.'), $app->path()); $this->assertEquals(realpath('.'), $app->path());
$this->assertEquals(realpath('.') . '/config', $app->get('path.config')); $this->assertEquals(realpath('.') . '/config', $app->get('path.config'));

View File

@ -0,0 +1,60 @@
<?php
namespace Engelsystem\Test\Unit\Renderer;
use Engelsystem\Renderer\TwigEngine;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Twig_Environment as Twig;
use Twig_LoaderInterface as LoaderInterface;
class TwigEngineTest extends TestCase
{
/**
* @covers \Engelsystem\Renderer\TwigEngine::__construct
* @covers \Engelsystem\Renderer\TwigEngine::get
*/
public function testGet()
{
/** @var Twig|MockObject $twig */
$twig = $this->createMock(Twig::class);
$path = 'foo.twig';
$data = ['lorem' => 'ipsum'];
$twig->expects($this->once())
->method('render')
->with($path, $data)
->willReturn('LoremIpsum!');
$engine = new TwigEngine($twig);
$return = $engine->get($path, $data);
$this->assertEquals('LoremIpsum!', $return);
}
/**
* @covers \Engelsystem\Renderer\TwigEngine::canRender
*/
public function testCanRender()
{
/** @var Twig|MockObject $twig */
$twig = $this->createMock(Twig::class);
/** @var LoaderInterface|MockObject $loader */
$loader = $this->getMockForAbstractClass(LoaderInterface::class);
$path = 'foo.twig';
$twig->expects($this->once())
->method('getLoader')
->willReturn($loader);
$loader->expects($this->once())
->method('exists')
->with($path)
->willReturn(true);
$engine = new TwigEngine($twig);
$return = $engine->canRender($path);
$this->assertTrue($return);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Test\Unit\Renderer;
use Engelsystem\Renderer\TwigLoader;
use PHPUnit\Framework\TestCase;
use ReflectionClass as Reflection;
class TwigLoaderTest extends TestCase
{
/**
* @covers \Engelsystem\Renderer\TwigLoader::findTemplate
*/
public function testFindTemplate()
{
$loader = new TwigLoader();
$reflection = new Reflection(get_class($loader));
$property = $reflection->getProperty('cache');
$property->setAccessible(true);
$realPath = __DIR__ . '/Stub/foo.twig';
$property->setValue($loader, ['Stub/foo.twig' => $realPath]);
$return = $loader->findTemplate('Stub/foo.twig');
$this->assertEquals($realPath, $return);
$return = $loader->findTemplate('Stub/foo');
$this->assertEquals($realPath, $return);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Engelsystem\Test\Unit\Renderer;
use Engelsystem\Renderer\TwigEngine;
use Engelsystem\Renderer\TwigLoader;
use Engelsystem\Renderer\TwigServiceProvider;
use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject;
use Twig_Environment as Twig;
use Twig_LoaderInterface as TwigLoaderInterface;
class TwigServiceProviderTest extends ServiceProviderTest
{
/**
* @covers \Engelsystem\Renderer\TwigServiceProvider::register
* @covers \Engelsystem\Renderer\TwigServiceProvider::registerTwigEngine
*/
public function testRegister()
{
/** @var TwigEngine|MockObject $htmlEngine */
$twigEngine = $this->createMock(TwigEngine::class);
/** @var TwigLoader|MockObject $twigLoader */
$twigLoader = $this->createMock(TwigLoader::class);
/** @var Twig|MockObject $twig */
$twig = $this->createMock(Twig::class);
$app = $this->getApp(['make', 'instance', 'tag', 'get']);
$viewsPath = __DIR__ . '/Stub';
$app->expects($this->exactly(3))
->method('make')
->withConsecutive(
[TwigLoader::class, ['paths' => $viewsPath]],
[Twig::class],
[TwigEngine::class]
)->willReturnOnConsecutiveCalls(
$twigLoader,
$twig,
$twigEngine
);
$app->expects($this->exactly(4))
->method('instance')
->withConsecutive(
[TwigLoader::class, $twigLoader],
[TwigLoaderInterface::class, $twigLoader],
[Twig::class, $twig],
['renderer.twigEngine', $twigEngine]
);
$app->expects($this->once())
->method('get')
->with('path.views')
->willReturn($viewsPath);
$this->setExpects($app, 'tag', ['renderer.twigEngine', ['renderer.engine']]);
$serviceProvider = new TwigServiceProvider($app);
$serviceProvider->register();
}
}