Merge remote-tracking branch 'MyIgel/templating'

This commit is contained in:
Igor Scheller 2018-09-08 12:48:08 +02:00
commit 222c9fed7d
57 changed files with 1801 additions and 183 deletions

View File

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

View File

@ -13,8 +13,10 @@ return [
\Engelsystem\Database\DatabaseServiceProvider::class,
\Engelsystem\Http\RequestServiceProvider::class,
\Engelsystem\Http\SessionServiceProvider::class,
\Engelsystem\Helpers\TranslationServiceProvider::class,
\Engelsystem\Http\ResponseServiceProvider::class,
\Engelsystem\Http\Psr7ServiceProvider::class,
\Engelsystem\Renderer\TwigServiceProvider::class,
\Engelsystem\Middleware\RouteDispatcherServiceProvider::class,
\Engelsystem\Middleware\RequestHandlerServiceProvider::class,
],
@ -23,6 +25,8 @@ return [
'middleware' => [
\Engelsystem\Middleware\SendResponseHandler::class,
\Engelsystem\Middleware\ExceptionHandler::class,
\Engelsystem\Middleware\SetLocale::class,
\Engelsystem\Middleware\ErrorHandler::class,
\Engelsystem\Middleware\RouteDispatcher::class,
\Engelsystem\Middleware\RequestHandler::class,
],

View File

@ -16,17 +16,11 @@ require __DIR__ . '/includes.php';
* Check for 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();
}
/**
* Init translations
*/
gettext_init();
/**
* Init authorization
*/

View File

@ -15,14 +15,16 @@ function engelsystem_email_to_user($recipient_user, $title, $message, $not_if_it
return true;
}
gettext_locale($recipient_user['Sprache']);
/** @var \Engelsystem\Helpers\Translator $translator */
$translator = app()->get('translator');
$locale = $translator->getLocale();
$translator->setLocale($recipient_user['Sprache']);
$message = sprintf(_('Hi %s,'), $recipient_user['Nick']) . "\n\n"
. _('here is a message for you from the engelsystem:') . "\n\n"
. $message . "\n\n"
. _('This email is autogenerated and has not been signed. You got this email because you are registered in the engelsystem.');
gettext_locale();
$translator->setLocale($locale);
return engelsystem_email($recipient_user['email'], $title, $message);
}

View File

@ -1,80 +0,0 @@
<?php
/**
* Return currently active locale
*
* @return string
*/
function locale()
{
return session()->get('locale');
}
/**
* Returns two letter language code from currently active locale
*
* @return string
*/
function locale_short()
{
return substr(locale(), 0, 2);
}
/**
* Initializes gettext for internationalization and updates the sessions locale to use for translation.
*/
function gettext_init()
{
$locales = config('locales');
$request = request();
$session = session();
if ($request->has('set_locale') && isset($locales[$request->input('set_locale')])) {
$session->set('locale', $request->input('set_locale'));
} elseif (!$session->has('locale')) {
$session->set('locale', config('default_locale'));
}
gettext_locale();
bindtextdomain('default', app('path.lang'));
bind_textdomain_codeset('default', 'UTF-8');
textdomain('default');
}
/**
* Swich gettext locale.
*
* @param string $locale
*/
function gettext_locale($locale = null)
{
if (empty($locale)) {
$locale = session()->get('locale');
}
putenv('LC_ALL=' . $locale);
setlocale(LC_ALL, $locale);
}
/**
* Renders language selection.
*
* @return array
*/
function make_langselect()
{
$request = app('request');
$items = [];
foreach (config('locales') as $locale => $name) {
$url = url($request->getPathInfo(), ['set_locale' => $locale]);
$items[] = toolbar_item_link(
htmlspecialchars($url),
'',
$name,
$locale == locale()
);
}
return $items;
}

View File

@ -61,7 +61,6 @@ $includeFiles = [
__DIR__ . '/../includes/controller/user_worklog_controller.php',
__DIR__ . '/../includes/helper/graph_helper.php',
__DIR__ . '/../includes/helper/internationalization_helper.php',
__DIR__ . '/../includes/helper/message_helper.php',
__DIR__ . '/../includes/helper/error_helper.php',
__DIR__ . '/../includes/helper/email_helper.php',

View File

@ -13,5 +13,5 @@ function credits_title()
*/
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([
div('col-md-12', [
msg(),
view(__DIR__ . '/../../templates/user_shifts.html', [
view(__DIR__ . '/../../templates/pages/user-shifts.html', [
'title' => shifts_title(),
'room_select' => make_select($rooms, $shiftsFilter->getRooms(), 'rooms', _('Rooms')),
'start_select' => html_select_key(

View File

@ -66,6 +66,9 @@ function form_date($name, $label, $value, $start_date = '', $end_date = '')
$value = is_numeric($value) ? date('Y-m-d', $value) : '';
$start_date = is_numeric($start_date) ? date('Y-m-d', $start_date) : '';
$end_date = is_numeric($end_date) ? date('Y-m-d', $end_date) : '';
$locale = $locale = session()->get('locale');
$shortLocale = substr($locale, 0, 2);
return form_element($label, '
<div class="input-group date" id="' . $dom_id . '">
<input name="' . $name . '" class="form-control" value="' . htmlspecialchars($value) . '">'
@ -73,13 +76,13 @@ function form_date($name, $label, $value, $start_date = '', $end_date = '')
</div>
<script type="text/javascript">
$(function(){
$(\'#' . $dom_id . '\').datepicker({
language: \'' . locale_short() . '\',
todayBtn: \'linked\',
format: \'yyyy-mm-dd\',
startDate: \'' . $start_date . '\',
endDate: \'' . $end_date . '\',
orientation: \'bottom\'
$("#' . $dom_id . '").datepicker({
language: "' . $shortLocale . '",
todayBtn: "linked",
format: "yyyy-mm-dd",
startDate: "' . $start_date . '",
endDate: "' . $end_date . '",
orientation: "bottom"
});
});
</script>

View File

@ -110,7 +110,7 @@ function make_user_submenu()
{
global $privileges, $page;
$user_submenu = make_langselect();
$user_submenu = make_language_select();
if (in_array('user_settings', $privileges) || in_array('logout', $privileges)) {
$user_submenu[] = toolbar_item_divider();
@ -227,6 +227,30 @@ function make_room_navigation($menu)
return $menu;
}
/**
* Renders language selection.
*
* @return array
*/
function make_language_select()
{
$request = app('request');
$activeLocale = session()->get('locale');
$items = [];
foreach (config('locales') as $locale => $name) {
$url = url($request->getPathInfo(), ['set-locale' => $locale]);
$items[] = toolbar_item_link(
htmlspecialchars($url),
'',
$name,
$locale == $activeLocale
);
}
return $items;
}
/**
* @return string
*/

View File

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

View File

@ -13,6 +13,7 @@ class ConfigServiceProvider extends ServiceProvider
public function register()
{
$config = $this->app->make(Config::class);
$this->app->instance(Config::class, $config);
$this->app->instance('config', $config);
foreach ($this->configFiles as $file) {

View File

@ -0,0 +1,58 @@
<?php
namespace Engelsystem\Helpers;
use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider;
use Symfony\Component\HttpFoundation\Session\Session;
class TranslationServiceProvider extends ServiceProvider
{
public function register()
{
/** @var Config $config */
$config = $this->app->get('config');
/** @var Session $session */
$session = $this->app->get('session');
$locales = $config->get('locales');
$locale = $config->get('default_locale');
$sessionLocale = $session->get('locale', $locale);
if (isset($locales[$sessionLocale])) {
$locale = $sessionLocale;
}
$this->initGettext();
$session->set('locale', $locale);
$translator = $this->app->make(
Translator::class,
['locale' => $locale, 'locales' => $locales, 'localeChangeCallback' => [$this, 'setLocale']]
);
$this->app->instance(Translator::class, $translator);
$this->app->instance('translator', $translator);
}
/**
* @param string $textDomain
* @param string $encoding
* @codeCoverageIgnore
*/
protected function initGettext($textDomain = 'default', $encoding = 'UTF-8')
{
bindtextdomain($textDomain, $this->app->get('path.lang'));
bind_textdomain_codeset($textDomain, $encoding);
textdomain($textDomain);
}
/**
* @param string $locale
* @codeCoverageIgnore
*/
public function setLocale($locale)
{
putenv('LC_ALL=' . $locale);
setlocale(LC_ALL, $locale);
}
}

105
src/Helpers/Translator.php Normal file
View File

@ -0,0 +1,105 @@
<?php
namespace Engelsystem\Helpers;
class Translator
{
/** @var string[] */
protected $locales;
/** @var string */
protected $locale;
/** @var callable */
protected $localeChangeCallback;
/**
* Translator constructor.
*
* @param string $locale
* @param string[] $locales
* @param callable $localeChangeCallback
*/
public function __construct(string $locale, array $locales = [], callable $localeChangeCallback = null)
{
$this->localeChangeCallback = $localeChangeCallback;
$this->setLocale($locale);
$this->setLocales($locales);
}
/**
* Get the translation for a given key
*
* @param string $key
* @param array $replace
* @return string
*/
public function translate(string $key, array $replace = []): string
{
$translated = $this->translateGettext($key);
if (!empty($replace)) {
$translated = call_user_func_array('sprintf', array_merge([$translated], $replace));
}
return $translated;
}
/**
* Translate the key via gettext
*
* @param string $key
* @return string
* @codeCoverageIgnore
*/
protected function translateGettext(string $key): string
{
return _($key);
}
/**
* @return string
*/
public function getLocale(): string
{
return $this->locale;
}
/**
* @param string $locale
*/
public function setLocale(string $locale)
{
$this->locale = $locale;
if (is_callable($this->localeChangeCallback)) {
call_user_func_array($this->localeChangeCallback, [$locale]);
}
}
/**
* @return string[]
*/
public function getLocales(): array
{
return $this->locales;
}
/**
* @param string $locale
* @return bool
*/
public function hasLocale(string $locale): bool
{
return isset($this->locales[$locale]);
}
/**
* @param string[] $locales
*/
public function setLocales(array $locales)
{
$this->locales = $locales;
}
}

View File

@ -2,6 +2,7 @@
namespace Engelsystem\Http;
use Engelsystem\Renderer\Renderer;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
@ -9,6 +10,25 @@ class Response extends SymfonyResponse implements ResponseInterface
{
use MessageTrait;
/** @var Renderer */
protected $view;
/**
* @param string $content
* @param int $status
* @param array $headers
* @param Renderer $view
*/
public function __construct(
$content = '',
int $status = 200,
array $headers = array(),
Renderer $view = null
) {
$this->view = $view;
parent::__construct($content, $status, $headers);
}
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
@ -72,4 +92,25 @@ class Response extends SymfonyResponse implements ResponseInterface
return $new;
}
/**
* Return an instance with the rendered content.
*
* THis method retains the immutability of the message and returns
* an instance with the updated status and headers
*
* @param string $view
* @param array $data
* @param int $status
* @param string[]|string[][] $headers
* @return Response
*/
public function withView($view, $data = [], $status = 200, $headers = [])
{
if (!$this->view instanceof Renderer) {
throw new \InvalidArgumentException('Renderer not defined');
}
return $this->create($this->view->render($view, $data), $status, $headers);
}
}

View File

@ -17,6 +17,7 @@ class SessionServiceProvider extends ServiceProvider
$this->app->bind(SessionStorageInterface::class, 'session.storage');
$session = $this->app->make(Session::class);
$this->app->instance(Session::class, $session);
$this->app->instance('session', $session);
/** @var Request $request */

View File

@ -9,6 +9,7 @@ class UrlGeneratorServiceProvider extends ServiceProvider
public function register()
{
$urlGenerator = $this->app->make(UrlGenerator::class);
$this->app->instance(UrlGenerator::class, $urlGenerator);
$this->app->instance('http.urlGenerator', $urlGenerator);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Engelsystem\Middleware;
use Engelsystem\Http\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Twig_LoaderInterface as TwigLoader;
class ErrorHandler implements MiddlewareInterface
{
/** @var TwigLoader */
protected $loader;
/** @var string */
protected $viewPrefix = 'errors/';
/**
* @param TwigLoader $loader
*/
public function __construct(TwigLoader $loader)
{
$this->loader = $loader;
}
/**
* Handles any error messages
*
* Should be added at the beginning
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$response = $handler->handle($request);
$statusCode = $response->getStatusCode();
if ($statusCode < 400 || !$response instanceof Response) {
return $response;
}
$view = $this->selectView($statusCode);
return $response->withView(
$this->viewPrefix . $view,
[
'status' => $statusCode,
'content' => $response->getContent(),
],
$statusCode,
$response->getHeaders()
);
}
/**
* Select a view based on the given status code
*
* @param int $statusCode
* @return string
*/
protected function selectView(int $statusCode): string
{
$hundreds = intdiv($statusCode, 100);
$viewsList = [$statusCode, $hundreds, $hundreds * 100];
foreach ($viewsList as $view) {
if ($this->loader->exists($this->viewPrefix . $view)) {
return $view;
}
}
return 'default';
}
}

View File

@ -83,7 +83,7 @@ class LegacyMiddleware implements MiddlewareInterface
}
if (empty($title) and empty($content)) {
$page = '404';
$page = 404;
$title = _('Page not found');
$content = _('This page could not be found or you don\'t have permission to view it. You probably have to sign in or register in order to gain access!');
}
@ -277,29 +277,17 @@ class LegacyMiddleware implements MiddlewareInterface
$parameters['meetings'] = 1;
}
$status = 200;
if ($page == '404') {
$status = 404;
$content = info($content, true);
if (!empty($page) && is_int($page)) {
return response($content, (int)$page);
}
return response(view(__DIR__ . '/../../templates/layout.html', [
'theme' => isset($user) ? $user['color'] : config('theme'),
return response(view('layouts/app', [
'title' => $title,
'atom_link' => ($page == 'news' || $page == 'user_meetings')
? ' <link href="'
. page_link_to('atom', $parameters)
. '" type = "application/atom+xml" rel = "alternate" title = "Atom Feed">'
: '',
'start_page_url' => page_link_to('/'),
'credits_url' => page_link_to('credits'),
'atom_feed' => ($page == 'news' || $page == 'user_meetings') ? $parameters : [],
'menu' => make_menu(),
'content' => msg() . $content,
'header_toolbar' => header_toolbar(),
'faq_url' => config('faq_url'),
'contact_email' => config('contact_email'),
'locale' => locale(),
'event_info' => EventConfig_info($event_config) . ' <br />'
]), $status);
]), 200);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Engelsystem\Middleware;
use Engelsystem\Helpers\Translator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
class SetLocale implements MiddlewareInterface
{
/** @var Translator */
protected $translator;
/** @var Session */
protected $session;
/**
* @param Translator $translator
* @param Session $session
*/
public function __construct(Translator $translator, Session $session)
{
$this->translator = $translator;
$this->session = $session;
}
/**
* Process an incoming server request and setting the locale if required
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$query = $request->getQueryParams();
if (isset($query['set-locale']) && $this->translator->hasLocale($query['set-locale'])) {
$locale = $query['set-locale'];
$this->translator->setLocale($locale);
$this->session->set('locale', $locale);
}
return $handler->handle($request);
}
}

View File

@ -24,12 +24,14 @@ class RendererServiceProvider extends ServiceProvider
protected function registerRenderer()
{
$renderer = $this->app->make(Renderer::class);
$this->app->instance(Renderer::class, $renderer);
$this->app->instance('renderer', $renderer);
}
protected function registerHtmlEngine()
{
$htmlEngine = $this->app->make(HtmlEngine::class);
$this->app->instance(HtmlEngine::class, $htmlEngine);
$this->app->instance('renderer.htmlEngine', $htmlEngine);
$this->app->tag('renderer.htmlEngine', ['renderer.engine']);
}

View File

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Renderer\Twig\Extensions;
use Engelsystem\Config\Config as EngelsystemConfig;
use Twig_Extension as TwigExtension;
use Twig_Function as TwigFunction;
class Config extends TwigExtension
{
/** @var EngelsystemConfig */
protected $config;
/**
* @param EngelsystemConfig $config
*/
public function __construct(EngelsystemConfig $config)
{
$this->config = $config;
}
/**
* @return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('config', [$this->config, 'get']),
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Engelsystem\Renderer\Twig\Extensions;
use Twig_Extension as TwigExtension;
use Twig_Extension_GlobalsInterface as GlobalsInterface;
class Globals extends TwigExtension implements GlobalsInterface
{
/**
* Returns a list of global variables to add to the existing list.
*
* @return array An array of global variables
*/
public function getGlobals()
{
global $user;
return [
'user' => isset($user) ? $user : [],
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Renderer\Twig\Extensions;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
use Twig_Extension as TwigExtension;
use Twig_Function as TwigFunction;
class Session extends TwigExtension
{
/** @var SymfonySession */
protected $session;
/**
* @param SymfonySession $session
*/
public function __construct(SymfonySession $session)
{
$this->session = $session;
}
/**
* @return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('session_get', [$this->session, 'get']),
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Engelsystem\Renderer\Twig\Extensions;
use Engelsystem\Helpers\Translator;
use Twig_Extension as TwigExtension;
use Twig_Extensions_TokenParser_Trans as TranslationTokenParser;
use Twig_Filter as TwigFilter;
use Twig_Function as TwigFunction;
use Twig_TokenParserInterface as TwigTokenParser;
class Translation extends TwigExtension
{
/** @var Translator */
protected $translator;
/** @var TranslationTokenParser */
protected $tokenParser;
/**
* @param Translator $translator
* @param TranslationTokenParser $tokenParser
*/
public function __construct(Translator $translator, TranslationTokenParser $tokenParser)
{
$this->translator = $translator;
$this->tokenParser = $tokenParser;
}
/**
* @return array
*/
public function getFilters()
{
return [
new TwigFilter('trans', [$this->translator, 'translate']),
];
}
/**
* @return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('__', [$this->translator, 'translate']),
];
}
/**
* @return TwigTokenParser[]
*/
public function getTokenParsers()
{
return [$this->tokenParser];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Engelsystem\Renderer\Twig\Extensions;
use Engelsystem\Http\UrlGenerator;
use Twig_Extension as TwigExtension;
use Twig_Function as TwigFunction;
class Url extends TwigExtension
{
/** @var UrlGenerator */
protected $urlGenerator;
/**
* @param UrlGenerator $urlGenerator
*/
public function __construct(UrlGenerator $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* @return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('url', [$this, 'getUrl']),
];
}
/**
* @param string $path
* @param array $parameters
* @return UrlGenerator|string
*/
public function getUrl($path, $parameters = [])
{
$path = str_replace('_', '-', $path);
return $this->urlGenerator->to($path, $parameters);
}
}

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,77 @@
<?php
namespace Engelsystem\Renderer;
use Engelsystem\Container\ServiceProvider;
use Engelsystem\Renderer\Twig\Extensions\Config;
use Engelsystem\Renderer\Twig\Extensions\Globals;
use Engelsystem\Renderer\Twig\Extensions\Session;
use Engelsystem\Renderer\Twig\Extensions\Translation;
use Engelsystem\Renderer\Twig\Extensions\Url;
use Twig_Environment as Twig;
use Twig_LoaderInterface as TwigLoaderInterface;
class TwigServiceProvider extends ServiceProvider
{
/** @var array */
protected $extensions = [
'config' => Config::class,
'globals' => Globals::class,
'session' => Session::class,
'url' => Url::class,
'translation' => Translation::class,
];
public function register()
{
$this->registerTwigEngine();
foreach ($this->extensions as $alias => $class) {
$this->registerTwigExtensions($class, $alias);
}
}
public function boot()
{
/** @var Twig $renderer */
$renderer = $this->app->get('twig.environment');
foreach ($this->app->tagged('twig.extension') as $extension) {
$renderer->addExtension($extension);
}
}
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);
$this->app->instance('twig.loader', $twigLoader);
$twig = $this->app->make(Twig::class);
$this->app->instance(Twig::class, $twig);
$this->app->instance('twig.environment', $twig);
$twigEngine = $this->app->make(TwigEngine::class);
$this->app->instance('renderer.twigEngine', $twigEngine);
$this->app->tag('renderer.twigEngine', ['renderer.engine']);
}
/**
* @param string $class
* @param string $alias
*/
protected function registerTwigExtensions($class, $alias)
{
$alias = 'twig.extension.' . $alias;
$extension = $this->app->make($class);
$this->app->instance($class, $extension);
$this->app->instance($alias, $extension);
$this->app->tag($alias, ['twig.extension']);
}
}

View File

@ -3,10 +3,11 @@
use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Helpers\Translator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Renderer\Renderer;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Renderer\Renderer;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
@ -118,6 +119,40 @@ function session($key = null, $default = null)
return $session->get($key, $default);
}
/**
* Translate the given message
*
* @param string $key
* @param array $replace
* @return string|Translator
*/
function trans($key = null, $replace = [])
{
/** @var Translator $translator */
$translator = app('translator');
if (is_null($key)) {
return $translator;
}
return $translator->translate($key, $replace);
}
/**
* Translate the given message
*
* @param string $key
* @param array $replace
* @return string
*/
function __($key, $replace = [])
{
/** @var Translator $translator */
$translator = app('translator');
return $translator->translate($key, $replace);
}
/**
* @param string $path
* @param array $parameters
@ -139,7 +174,7 @@ function url($path = null, $parameters = [])
* @param mixed[] $data
* @return Renderer|string
*/
function view($template = null, $data = null)
function view($template = null, $data = [])
{
$renderer = app('renderer');

View File

@ -0,0 +1,7 @@
{% extends "layouts/app.twig" %}
{% block title %}{% if status == 404 %}{{ __("Page not found") }}{% else %}Error {{ status }}{% endif %}{% endblock %}
{% block content %}
<div class="alert alert-info">{{ content }}</div>
{% endblock %}

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="%locale%">
<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="%start_page_url%assets/theme%theme%.css"/>
<script type="text/javascript" src="%start_page_url%assets/vendor.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>
</body>
</html>

View File

@ -0,0 +1,87 @@
{% set theme = user.color|default(config('theme')) %}
<!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>
{% if atom_feed -%}
<link href="{{ url('atom', atom_feed) }}" type="application/atom+xml" rel="alternate" title="Atom Feed">
{% endif %}
{% 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="{{ 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="{{ config('faq_url') }}">FAQ</a>
· <a href="{{ config('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="{{ url('credits') }}">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("{{ session_get('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>
The original system was written by <a href="https://github.com/cookieBerlin/engelsystem">cookie</a>.
It was then completely rewritten and enhanced by
<a href="http://notrademark.de/">msquare</a> (maintainer),
<a href="http://myigel.name/">MyIgel</a>,
<a href="http://mortzu.de/">mortzu</a>,
<a href="http://jplitza.de/">jplitza</a> and
gnomus.
<a href="https://notrademark.de">msquare</a> (maintainer),
<a href="https://myigel.name">MyIgel</a>,
<a href="https://mortzu.de">mortzu</a>,
<a href="https://jplitza.de">jplitza</a> and
<a href="https://github.com/gnomus">gnomus</a>.
</p>
<p>
Please look at the <a href="https://github.com/engelsystem/engelsystem/graphs/contributors">contributor
list on github</a> for a more complete version.
Please look at the <a href="https://github.com/engelsystem/engelsystem/graphs/contributors">
contributor list on github</a> for a more complete version.
</p>
</div>
<div class="col-md-4">
@ -22,8 +22,8 @@
<p>
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)
and adminstrated by <a href="http://mortzu.de/">mortzu</a>,
<a href="http://derf.homelinux.org/">derf</a> and ichdasich.
and adminstrated by <a href="https://mortzu.de">mortzu</a>,
<a href="http://derf.homelinux.org">derf</a> and ichdasich.
</p>
</div>
<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.config'));
$this->assertTrue($app->has('path.lang'));
$this->assertTrue($app->has('path.views'));
$this->assertEquals(realpath('.'), $app->path());
$this->assertEquals(realpath('.') . '/config', $app->get('path.config'));

View File

@ -24,8 +24,13 @@ class ConfigServiceProviderTest extends ServiceProviderTest
Application::setInstance($app);
$this->setExpects($app, 'make', [Config::class], $config);
$this->setExpects($app, 'instance', ['config', $config]);
$this->setExpects($app, 'get', ['path.config'], __DIR__ . '/../../../config', $this->atLeastOnce());
$app->expects($this->exactly(2))
->method('instance')
->withConsecutive(
[Config::class, $config],
['config', $config]
);
$this->setExpects($config, 'set', null, null, $this->exactly(2));
$config->expects($this->exactly(3))
@ -60,7 +65,12 @@ class ConfigServiceProviderTest extends ServiceProviderTest
Application::setInstance($app);
$this->setExpects($app, 'make', [Config::class], $config);
$this->setExpects($app, 'instance', ['config', $config]);
$app->expects($this->exactly(2))
->method('instance')
->withConsecutive(
[Config::class, $config],
['config', $config]
);
$this->setExpects($app, 'get', ['path.config'], __DIR__ . '/not_existing', $this->atLeastOnce());
$this->setExpects($config, 'set', null, null, $this->never());

View File

@ -0,0 +1,86 @@
<?php
namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Config\Config;
use Engelsystem\Helpers\TranslationServiceProvider;
use Engelsystem\Helpers\Translator;
use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session;
class TranslationServiceProviderTest extends ServiceProviderTest
{
/**
* @covers \Engelsystem\Helpers\TranslationServiceProvider::register()
*/
public function testRegister()
{
$app = $this->getApp(['make', 'instance', 'get']);
/** @var Config|MockObject $config */
$config = $this->createMock(Config::class);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
/** @var Translator|MockObject $translator */
$translator = $this->createMock(Translator::class);
/** @var TranslationServiceProvider|MockObject $serviceProvider */
$serviceProvider = $this->getMockBuilder(TranslationServiceProvider::class)
->setConstructorArgs([$app])
->setMethods(['initGettext', 'setLocale'])
->getMock();
$serviceProvider->expects($this->once())
->method('initGettext');
$app->expects($this->exactly(2))
->method('get')
->withConsecutive(['config'], ['session'])
->willReturnOnConsecutiveCalls($config, $session);
$defaultLocale = 'fo_OO';
$locale = 'te_ST.WTF-9';
$locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?'];
$config->expects($this->exactly(2))
->method('get')
->withConsecutive(
['locales'],
['default_locale']
)
->willReturnOnConsecutiveCalls(
$locales,
$defaultLocale
);
$session->expects($this->once())
->method('get')
->with('locale', $defaultLocale)
->willReturn($locale);
$session->expects($this->once())
->method('set')
->with('locale', $locale);
$app->expects($this->once())
->method('make')
->with(
Translator::class,
[
'locale' => $locale,
'locales' => $locales,
'localeChangeCallback' => [$serviceProvider, 'setLocale']
]
)
->willReturn($translator);
$app->expects($this->exactly(2))
->method('instance')
->withConsecutive(
[Translator::class, $translator],
['translator', $translator]
);
$serviceProvider->register();
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Helpers\Translator;
use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject;
use stdClass;
class TranslatorTest extends ServiceProviderTest
{
/**
* @covers \Engelsystem\Helpers\Translator::__construct()
* @covers \Engelsystem\Helpers\Translator::setLocale()
* @covers \Engelsystem\Helpers\Translator::setLocales()
* @covers \Engelsystem\Helpers\Translator::getLocale()
* @covers \Engelsystem\Helpers\Translator::getLocales()
* @covers \Engelsystem\Helpers\Translator::hasLocale()
*/
public function testInit()
{
$locales = ['te_ST.ER-01' => 'Tests', 'fo_OO' => 'SomeFOO'];
$locale = 'te_ST.ER-01';
/** @var callable|MockObject $callable */
$callable = $this->getMockBuilder(stdClass::class)
->setMethods(['__invoke'])
->getMock();
$callable->expects($this->exactly(2))
->method('__invoke')
->withConsecutive(['te_ST.ER-01'], ['fo_OO']);
$translator = new Translator($locale, $locales, $callable);
$this->assertEquals($locales, $translator->getLocales());
$this->assertEquals($locale, $translator->getLocale());
$translator->setLocale('fo_OO');
$this->assertEquals('fo_OO', $translator->getLocale());
$newLocales = ['lo_RM' => 'Lorem', 'ip_SU-M' => 'Ipsum'];
$translator->setLocales($newLocales);
$this->assertEquals($newLocales, $translator->getLocales());
$this->assertTrue($translator->hasLocale('ip_SU-M'));
$this->assertFalse($translator->hasLocale('te_ST.ER-01'));
}
/**
* @covers \Engelsystem\Helpers\Translator::translate()
*/
public function testTranslate()
{
/** @var Translator|MockObject $translator */
$translator = $this->getMockBuilder(Translator::class)
->setConstructorArgs(['de_DE.UTF-8', ['de_DE.UTF-8' => 'Deutsch']])
->setMethods(['translateGettext'])
->getMock();
$translator->expects($this->once())
->method('translateGettext')
->with('My favourite number is %u!')
->willReturn('Meine Lieblingszahl ist die %u!');
$return = $translator->translate('My favourite number is %u!', [3]);
$this->assertEquals('Meine Lieblingszahl ist die 3!', $return);
}
}

View File

@ -5,10 +5,10 @@ namespace Engelsystem\Test\Unit;
use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Container\Container;
use Engelsystem\Helpers\Translator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Renderer\Renderer;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Http\UrlGeneratorInterface;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
@ -195,6 +195,29 @@ class HelpersTest extends TestCase
$this->assertEquals('rendered template', view('template.name', ['template' => 'data']));
}
/**
* @covers \__
* @covers \trans
*/
public function testTrans()
{
/** @var Translator|MockObject $translator */
$translator = $this->getMockBuilder(Translator::class)
->disableOriginalConstructor()
->getMock();
$this->getAppMock('translator', $translator);
$translator->expects($this->exactly(2))
->method('translate')
->with('Lorem %s Ipsum', ['foo'])
->willReturn('Lorem foo Ipsum');
$this->assertEquals($translator, trans());
$this->assertEquals('Lorem foo Ipsum', trans('Lorem %s Ipsum', ['foo']));
$this->assertEquals('Lorem foo Ipsum', __('Lorem %s Ipsum', ['foo']));
}
/**
* @covers \url
*/

View File

@ -3,6 +3,8 @@
namespace Engelsystem\Test\Unit\Http;
use Engelsystem\Http\Response;
use Engelsystem\Renderer\Renderer;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
@ -46,4 +48,37 @@ class ResponseTest extends TestCase
$this->assertNotEquals($response, $newResponse);
$this->assertEquals('Lorem Ipsum?', $newResponse->getContent());
}
}
/**
* @covers \Engelsystem\Http\Response::withView
*/
public function testWithView()
{
/** @var REnderer|MockObject $renderer */
$renderer = $this->createMock(Renderer::class);
$renderer->expects($this->once())
->method('render')
->with('foo', ['lorem' => 'ipsum'])
->willReturn('Foo ipsum!');
$response = new Response('', 200, [], $renderer);
$newResponse = $response->withView('foo', ['lorem' => 'ipsum'], 505, ['test' => 'er']);
$this->assertNotEquals($response, $newResponse);
$this->assertEquals('Foo ipsum!', $newResponse->getContent());
$this->assertEquals(505, $newResponse->getStatusCode());
$this->assertArraySubset(['test' => ['er']], $newResponse->getHeaders());
}
/**
* @covers \Engelsystem\Http\Response::withView
*/
public function testWithViewNoRenderer()
{
$this->expectException(\InvalidArgumentException::class);
$response = new Response();
$response->withView('foo');
}
}

View File

@ -54,6 +54,7 @@ class SessionServiceProviderTest extends ServiceProviderTest
->method('instance')
->withConsecutive(
['session.storage', $sessionStorage],
[Session::class, $session],
['session', $session]
);
@ -88,10 +89,11 @@ class SessionServiceProviderTest extends ServiceProviderTest
$sessionStorage,
$session
);
$app->expects($this->exactly(2))
$app->expects($this->exactly(3))
->method('instance')
->withConsecutive(
['session.storage', $sessionStorage],
[Session::class, $session],
['session', $session]
);

View File

@ -21,7 +21,12 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest
$app = $this->getApp();
$this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator);
$this->setExpects($app, 'instance', ['http.urlGenerator', $urlGenerator]);
$app->expects($this->exactly(2))
->method('instance')
->withConsecutive(
[UrlGenerator::class, $urlGenerator],
['http.urlGenerator', $urlGenerator]
);
$serviceProvider = new UrlGeneratorServiceProvider($app);
$serviceProvider->register();

View File

@ -0,0 +1,88 @@
<?php
namespace Engelsystem\Test\Unit\Middleware;
use Engelsystem\Http\Response;
use Engelsystem\Middleware\ErrorHandler;
use Engelsystem\Test\Unit\Middleware\Stub\ReturnResponseMiddlewareHandler;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Twig_LoaderInterface as TwigLoader;
class ErrorHandlerTest extends TestCase
{
/**
* @covers \Engelsystem\Middleware\ErrorHandler::__construct
* @covers \Engelsystem\Middleware\ErrorHandler::process
* @covers \Engelsystem\Middleware\ErrorHandler::selectView
*/
public function testProcess()
{
/** @var TwigLoader|MockObject $twigLoader */
$twigLoader = $this->createMock(TwigLoader::class);
/** @var ServerRequestInterface|MockObject $request */
$request = $this->createMock(ServerRequestInterface::class);
/** @var ResponseInterface|MockObject $psrResponse */
$psrResponse = $this->getMockForAbstractClass(ResponseInterface::class);
$returnResponseHandler = new ReturnResponseMiddlewareHandler($psrResponse);
$psrResponse->expects($this->once())
->method('getStatusCode')
->willReturn(505);
$errorHandler = new ErrorHandler($twigLoader);
$return = $errorHandler->process($request, $returnResponseHandler);
$this->assertEquals($psrResponse, $return, 'Plain PSR-7 Response should be passed directly');
/** @var Response|MockObject $response */
$response = $this->createMock(Response::class);
$response->expects($this->exactly(3))
->method('getStatusCode')
->willReturnOnConsecutiveCalls(
200,
418,
505
);
$returnResponseHandler->setResponse($response);
$return = $errorHandler->process($request, $returnResponseHandler);
$this->assertEquals($response, $return, 'Only Responses >= 400 should be processed');
$twigLoader->expects($this->exactly(4))
->method('exists')
->withConsecutive(
['errors/418'],
['errors/4'],
['errors/400'],
['errors/505']
)
->willReturnOnConsecutiveCalls(
false,
false,
false,
true
);
$response->expects($this->exactly(2))
->method('getContent')
->willReturnOnConsecutiveCalls(
'Teapot',
'Internal Error!'
);
$response->expects($this->exactly(2))
->method('withView')
->withConsecutive(
['errors/default', ['status' => 418, 'content' => 'Teapot'], 418],
['errors/505', ['status' => 505, 'content' => 'Internal Error!'], 505]
)
->willReturn($response);
$errorHandler->process($request, $returnResponseHandler);
$errorHandler->process($request, $returnResponseHandler);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Engelsystem\Test\Unit\Middleware;
use Engelsystem\Helpers\Translator;
use Engelsystem\Middleware\SetLocale;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
class SetLocaleTest extends TestCase
{
/**
* @covers \Engelsystem\Middleware\SetLocale::__construct
* @covers \Engelsystem\Middleware\SetLocale::process
*/
public function testRegister()
{
/** @var Translator|MockObject $translator */
$translator = $this->createMock(Translator::class);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockForAbstractClass(ServerRequestInterface::class);
/** @var RequestHandlerInterface|MockObject $handler */
$handler = $this->getMockForAbstractClass(RequestHandlerInterface::class);
/** @var ResponseInterface|MockObject $response */
$response = $this->getMockForAbstractClass(ResponseInterface::class);
$locale = 'te_ST.UTF8';
$request->expects($this->exactly(3))
->method('getQueryParams')
->willReturnOnConsecutiveCalls(
[],
['set-locale' => 'en_US.UTF8'],
['set-locale' => $locale]
);
$translator->expects($this->exactly(2))
->method('hasLocale')
->withConsecutive(
['en_US.UTF8'],
[$locale]
)
->willReturnOnConsecutiveCalls(
false,
true
);
$translator->expects($this->once())
->method('setLocale')
->with($locale);
$session->expects($this->once())
->method('set')
->with('locale', $locale);
$handler->expects($this->exactly(3))
->method('handle')
->with($request)
->willReturn($response);
$middleware = new SetLocale($translator, $session);
$middleware->process($request, $handler);
$middleware->process($request, $handler);
$middleware->process($request, $handler);
}
}

View File

@ -27,4 +27,14 @@ class ReturnResponseMiddlewareHandler implements RequestHandlerInterface
{
return $this->response;
}
/**
* Set the response
*
* @param ResponseInterface $response
*/
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
}

View File

@ -37,10 +37,12 @@ class RendererServiceProviderTest extends ServiceProviderTest
$htmlEngine
);
$app->expects($this->exactly(2))
$app->expects($this->exactly(4))
->method('instance')
->withConsecutive(
[Renderer::class, $renderer],
['renderer', $renderer],
[HtmlEngine::class, $htmlEngine],
['renderer.htmlEngine', $htmlEngine]
);

View File

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Config\Config as EngelsystemConfig;
use Engelsystem\Renderer\Twig\Extensions\Config;
use PHPUnit\Framework\MockObject\MockObject;
class ConfigTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Config::__construct
* @covers \Engelsystem\Renderer\Twig\Extensions\Config::getFunctions
*/
public function testGetFunctions()
{
/** @var EngelsystemConfig|MockObject $config */
$config = $this->createMock(EngelsystemConfig::class);
$extension = new Config($config);
$functions = $extension->getFunctions();
$this->assertExtensionExists('config', [$config, 'get'], $functions);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use PHPUnit\Framework\TestCase;
use Twig_Function as TwigFunction;
abstract class ExtensionTest extends TestCase
{
/**
* Assert that a twig filter was registered
*
* @param string $name
* @param callable $callback
* @param TwigFunction[] $functions
*/
protected function assertFilterExists($name, $callback, $functions)
{
foreach ($functions as $function) {
if ($function->getName() != $name) {
continue;
}
$this->assertEquals($callback, $function->getCallable());
return;
}
$this->fail(sprintf('Filter %s not found', $name));
}
/**
* Assert that a twig function was registered
*
* @param string $name
* @param callable $callback
* @param TwigFunction[] $functions
*/
protected function assertExtensionExists($name, $callback, $functions)
{
foreach ($functions as $function) {
if ($function->getName() != $name) {
continue;
}
$this->assertEquals($callback, $function->getCallable());
return;
}
$this->fail(sprintf('Function %s not found', $name));
}
/**
* Assert that a global exists
*
* @param string $name
* @param mixed $value
* @param mixed[] $globals
*/
protected function assertGlobalsExists($name, $value, $globals)
{
if (isset($globals[$name])) {
$this->assertArraySubset([$name => $value], $globals);
return;
}
$this->fail(sprintf('Global %s not found', $name));
}
/**
* Assert that a token parser was set
*
* @param $tokenParser
* @param $tokenParsers
*/
protected function assertTokenParserExists($tokenParser, $tokenParsers)
{
$this->assertArraySubset(
[$tokenParser],
$tokenParsers,
sprintf('Token parser %s not found', get_class($tokenParser))
);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Renderer\Twig\Extensions\Globals;
class GlobalsTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Globals::getGlobals
*/
public function testGetGlobals()
{
$extension = new Globals();
$globals = $extension->getGlobals();
$this->assertGlobalsExists('user', [], $globals);
global $user;
$user['foo'] = 'bar';
$globals = $extension->getGlobals();
$this->assertGlobalsExists('user', ['foo' => 'bar'], $globals);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Renderer\Twig\Extensions\Session;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
class SessionTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Session::__construct
* @covers \Engelsystem\Renderer\Twig\Extensions\Session::getFunctions
*/
public function testGetGlobals()
{
/** @var SymfonySession|MockObject $session */
$session = $this->createMock(SymfonySession::class);
$extension = new Session($session);
$functions = $extension->getFunctions();
$this->assertExtensionExists('session_get', [$session, 'get'], $functions);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Helpers\Translator;
use Engelsystem\Renderer\Twig\Extensions\Translation;
use PHPUnit\Framework\MockObject\MockObject;
use Twig_Extensions_TokenParser_Trans as TranslationTokenParser;
class TranslationTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Translation::__construct
* @covers \Engelsystem\Renderer\Twig\Extensions\Translation::getFilters
*/
public function testGeFilters()
{
/** @var Translator|MockObject $translator */
$translator = $this->createMock(Translator::class);
/** @var TranslationTokenParser|MockObject $parser */
$parser = $this->createMock(TranslationTokenParser::class);
$extension = new Translation($translator, $parser);
$filters = $extension->getFilters();
$this->assertExtensionExists('trans', [$translator, 'translate'], $filters);
}
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Translation::getFunctions
*/
public function testGetFunctions()
{
/** @var Translator|MockObject $translator */
$translator = $this->createMock(Translator::class);
/** @var TranslationTokenParser|MockObject $parser */
$parser = $this->createMock(TranslationTokenParser::class);
$extension = new Translation($translator, $parser);
$functions = $extension->getFunctions();
$this->assertExtensionExists('__', [$translator, 'translate'], $functions);
}
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Translation::getTokenParsers
*/
public function testGetTokenParsers()
{
/** @var Translator|MockObject $translator */
$translator = $this->createMock(Translator::class);
/** @var TranslationTokenParser|MockObject $parser */
$parser = $this->createMock(TranslationTokenParser::class);
$extension = new Translation($translator, $parser);
$tokenParsers = $extension->getTokenParsers();
$this->assertTokenParserExists($parser, $tokenParsers);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Renderer\Twig\Extensions\Url;
use PHPUnit\Framework\MockObject\MockObject;
class UrlTest extends ExtensionTest
{
/**
* @covers \Engelsystem\Renderer\Twig\Extensions\Url::__construct
* @covers \Engelsystem\Renderer\Twig\Extensions\Url::getFunctions
*/
public function testGetGlobals()
{
/** @var UrlGenerator|MockObject $urlGenerator */
$urlGenerator = $this->createMock(UrlGenerator::class);
$extension = new Url($urlGenerator);
$functions = $extension->getFunctions();
$this->assertExtensionExists('url', [$extension, 'getUrl'], $functions);
}
/**
* @return string[][]
*/
public function getUrls()
{
return [
['/', '/', 'http://foo.bar/'],
['/foo', '/foo', 'http://foo.bar/foo'],
['foo_bar', 'foo-bar', 'http://foo.bar/foo-bar'],
['dolor', 'dolor', 'http://foo.bar/dolor?lorem_ipsum=dolor', ['lorem_ipsum' => 'dolor']],
];
}
/**
* @dataProvider getUrls
*
* @param string $url
* @param string $return
* @param string $urlTo
* @param array $parameters
*
* @covers \Engelsystem\Renderer\Twig\Extensions\Url::getUrl
*/
public function testGetUrl($url, $urlTo, $return, $parameters = [])
{
/** @var UrlGenerator|MockObject $urlGenerator */
$urlGenerator = $this->createMock(UrlGenerator::class);
$urlGenerator->expects($this->once())
->method('to')
->with($urlTo, $parameters)
->willReturn($return);
$extension = new Url($urlGenerator);
$generatedUrl = $extension->getUrl($url, $parameters);
$this->assertEquals($return, $generatedUrl);
}
}

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,155 @@
<?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 ReflectionClass as Reflection;
use stdClass;
use Twig_Environment as Twig;
use Twig_ExtensionInterface as ExtensionInterface;
use Twig_LoaderInterface as TwigLoaderInterface;
class TwigServiceProviderTest extends ServiceProviderTest
{
/**
* @covers \Engelsystem\Renderer\TwigServiceProvider::register
* @covers \Engelsystem\Renderer\TwigServiceProvider::registerTwigExtensions
*/
public function testRegister()
{
$app = $this->getApp(['make', 'instance', 'tag']);
$class = $this->createMock(stdClass::class);
$className = 'Foo\Bar\Class';
$classAlias = 'twig.extension.foo';
$app->expects($this->once())
->method('make')
->with('Foo\Bar\Class')
->willReturn($class);
$app->expects($this->exactly(2))
->method('instance')
->withConsecutive(
[$className, $class],
[$classAlias, $class]
);
$app->expects($this->once())
->method('tag')
->with($classAlias, ['twig.extension']);
/** @var TwigServiceProvider|MockObject $serviceProvider */
$serviceProvider = $this->getMockBuilder(TwigServiceProvider::class)
->setConstructorArgs([$app])
->setMethods(['registerTwigEngine'])
->getMock();
$serviceProvider->expects($this->once())
->method('registerTwigEngine');
$this->setExtensionsTo($serviceProvider, ['foo' => 'Foo\Bar\Class']);
$serviceProvider->register();
}
/**
* @covers \Engelsystem\Renderer\TwigServiceProvider::boot
*/
public function testBoot()
{
/** @var Twig|MockObject $twig */
$twig = $this->createMock(Twig::class);
/** @var ExtensionInterface|MockObject $firsExtension */
$firsExtension = $this->getMockForAbstractClass(ExtensionInterface::class);
/** @var ExtensionInterface|MockObject $secondExtension */
$secondExtension = $this->getMockForAbstractClass(ExtensionInterface::class);
$app = $this->getApp(['get', 'tagged']);
$app->expects($this->once())
->method('get')
->with('twig.environment')
->willReturn($twig);
$app->expects($this->once())
->method('tagged')
->with('twig.extension')
->willReturn([$firsExtension, $secondExtension]);
$twig->expects($this->exactly(2))
->method('addExtension')
->withConsecutive($firsExtension, $secondExtension);
$serviceProvider = new TwigServiceProvider($app);
$serviceProvider->boot();
}
/**
* @covers \Engelsystem\Renderer\TwigServiceProvider::registerTwigEngine
*/
public function testRegisterTWigEngine()
{
/** @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(6))
->method('instance')
->withConsecutive(
[TwigLoader::class, $twigLoader],
[TwigLoaderInterface::class, $twigLoader],
['twig.loader', $twigLoader],
[Twig::class, $twig],
['twig.environment', $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);
$this->setExtensionsTo($serviceProvider, []);
$serviceProvider->register();
}
/**
* @param TwigServiceProvider $serviceProvider
* @param array $extensions
* @throws \ReflectionException
*/
protected function setExtensionsTo($serviceProvider, $extensions)
{
$reflection = new Reflection(get_class($serviceProvider));
$property = $reflection->getProperty('extensions');
$property->setAccessible(true);
$property->setValue($serviceProvider, $extensions);
}
}