diff options
57 files changed, 1801 insertions, 183 deletions
diff --git a/composer.json b/composer.json index f38bb972..1f53e546 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/config/app.php b/config/app.php index 9af35eb4..e309abe4 100644 --- a/config/app.php +++ b/config/app.php @@ -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, ], diff --git a/includes/engelsystem.php b/includes/engelsystem.php index f7d813c5..4c096b43 100644 --- a/includes/engelsystem.php +++ b/includes/engelsystem.php @@ -16,18 +16,12 @@ 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 */ load_auth(); diff --git a/includes/helper/email_helper.php b/includes/helper/email_helper.php index c223d026..8d1cb794 100644 --- a/includes/helper/email_helper.php +++ b/includes/helper/email_helper.php @@ -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); } diff --git a/includes/helper/internationalization_helper.php b/includes/helper/internationalization_helper.php deleted file mode 100644 index bb6d0abd..00000000 --- a/includes/helper/internationalization_helper.php +++ /dev/null @@ -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; -} diff --git a/includes/includes.php b/includes/includes.php index e316e550..7a68c3c9 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -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', diff --git a/includes/pages/guest_credits.php b/includes/pages/guest_credits.php index db86132d..ecfa8f7c 100644 --- a/includes/pages/guest_credits.php +++ b/includes/pages/guest_credits.php @@ -13,5 +13,5 @@ function credits_title() */ function guest_credits() { - return view(__DIR__ . '/../../templates/guest_credits.html'); + return view(__DIR__ . '/../../templates/pages/credits.html'); } diff --git a/includes/pages/user_shifts.php b/includes/pages/user_shifts.php index 186301db..020cfe54 100644 --- a/includes/pages/user_shifts.php +++ b/includes/pages/user_shifts.php @@ -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( diff --git a/includes/sys_form.php b/includes/sys_form.php index c974c1d1..cd1c84e6 100644 --- a/includes/sys_form.php +++ b/includes/sys_form.php @@ -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> diff --git a/includes/sys_menu.php b/includes/sys_menu.php index fae94de5..5609c0ab 100644 --- a/includes/sys_menu.php +++ b/includes/sys_menu.php @@ -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(); @@ -228,6 +228,30 @@ function make_room_navigation($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 */ function make_menu() diff --git a/src/Application.php b/src/Application.php index 6644a6cf..86397a2c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -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'); } /** diff --git a/src/Config/ConfigServiceProvider.php b/src/Config/ConfigServiceProvider.php index 9fbccd68..63f43ced 100644 --- a/src/Config/ConfigServiceProvider.php +++ b/src/Config/ConfigServiceProvider.php @@ -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) { diff --git a/src/Helpers/TranslationServiceProvider.php b/src/Helpers/TranslationServiceProvider.php new file mode 100644 index 00000000..9d86df7d --- /dev/null +++ b/src/Helpers/TranslationServiceProvider.php @@ -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); + } +} diff --git a/src/Helpers/Translator.php b/src/Helpers/Translator.php new file mode 100644 index 00000000..1e953a21 --- /dev/null +++ b/src/Helpers/Translator.php @@ -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; + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php index 9db6fa83..d79ab98b 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -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); + } } diff --git a/src/Http/SessionServiceProvider.php b/src/Http/SessionServiceProvider.php index 55e3f48b..59121a3b 100644 --- a/src/Http/SessionServiceProvider.php +++ b/src/Http/SessionServiceProvider.php @@ -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 */ diff --git a/src/Http/UrlGeneratorServiceProvider.php b/src/Http/UrlGeneratorServiceProvider.php index 37304076..9b9988aa 100644 --- a/src/Http/UrlGeneratorServiceProvider.php +++ b/src/Http/UrlGeneratorServiceProvider.php @@ -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); } } diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php new file mode 100644 index 00000000..a7c4cfe6 --- /dev/null +++ b/src/Middleware/ErrorHandler.php @@ -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'; + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 276fb3ee..78132815 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -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); } } diff --git a/src/Middleware/SetLocale.php b/src/Middleware/SetLocale.php new file mode 100644 index 00000000..86fa0b7f --- /dev/null +++ b/src/Middleware/SetLocale.php @@ -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); + } +} diff --git a/src/Renderer/RendererServiceProvider.php b/src/Renderer/RendererServiceProvider.php index 3e8d69bc..2e41837b 100644 --- a/src/Renderer/RendererServiceProvider.php +++ b/src/Renderer/RendererServiceProvider.php @@ -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']); } diff --git a/src/Renderer/Twig/Extensions/Config.php b/src/Renderer/Twig/Extensions/Config.php new file mode 100644 index 00000000..dbbe93e7 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Config.php @@ -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']), + ]; + } +} diff --git a/src/Renderer/Twig/Extensions/Globals.php b/src/Renderer/Twig/Extensions/Globals.php new file mode 100644 index 00000000..f9bffbc8 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Globals.php @@ -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 : [], + ]; + } +} diff --git a/src/Renderer/Twig/Extensions/Session.php b/src/Renderer/Twig/Extensions/Session.php new file mode 100644 index 00000000..4690f701 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Session.php @@ -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']), + ]; + } +} diff --git a/src/Renderer/Twig/Extensions/Translation.php b/src/Renderer/Twig/Extensions/Translation.php new file mode 100644 index 00000000..63f9800e --- /dev/null +++ b/src/Renderer/Twig/Extensions/Translation.php @@ -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]; + } +} diff --git a/src/Renderer/Twig/Extensions/Url.php b/src/Renderer/Twig/Extensions/Url.php new file mode 100644 index 00000000..62e59782 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Url.php @@ -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); + } +} diff --git a/src/Renderer/TwigEngine.php b/src/Renderer/TwigEngine.php new file mode 100644 index 00000000..55a2e299 --- /dev/null +++ b/src/Renderer/TwigEngine.php @@ -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); + } +} diff --git a/src/Renderer/TwigLoader.php b/src/Renderer/TwigLoader.php new file mode 100644 index 00000000..154e6dbb --- /dev/null +++ b/src/Renderer/TwigLoader.php @@ -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); + } +} diff --git a/src/Renderer/TwigServiceProvider.php b/src/Renderer/TwigServiceProvider.php new file mode 100644 index 00000000..0f453989 --- /dev/null +++ b/src/Renderer/TwigServiceProvider.php @@ -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']); + } +} diff --git a/src/helpers.php b/src/helpers.php index 95571a40..84f26dfa 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -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; /** @@ -119,6 +120,40 @@ function session($key = null, $default = null) } /** + * 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 * @return UrlGeneratorInterface|string @@ -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'); diff --git a/templates/errors/default.twig b/templates/errors/default.twig new file mode 100644 index 00000000..5fb8bcbd --- /dev/null +++ b/templates/errors/default.twig @@ -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 %} diff --git a/templates/layout.html b/templates/layout.html deleted file mode 100644 index 832bdcf3..00000000 --- a/templates/layout.html +++ /dev/null @@ -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> diff --git a/templates/layouts/app.twig b/templates/layouts/app.twig new file mode 100644 index 00000000..42d5610c --- /dev/null +++ b/templates/layouts/app.twig @@ -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> diff --git a/templates/maintenance.html b/templates/layouts/maintenance.html index f7ab5772..f7ab5772 100644 --- a/templates/maintenance.html +++ b/templates/layouts/maintenance.html diff --git a/templates/guest_credits.html b/templates/pages/credits.html index db7fac57..4e247113 100644 --- a/templates/guest_credits.html +++ b/templates/pages/credits.html @@ -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"> diff --git a/templates/user_shifts.html b/templates/pages/user-shifts.html index 2fdade29..2fdade29 100644 --- a/templates/user_shifts.html +++ b/templates/pages/user-shifts.html diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index 866eb957..012226b2 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -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')); diff --git a/tests/Unit/Config/ConfigServiceProviderTest.php b/tests/Unit/Config/ConfigServiceProviderTest.php index 998c0ba1..925854be 100644 --- a/tests/Unit/Config/ConfigServiceProviderTest.php +++ b/tests/Unit/Config/ConfigServiceProviderTest.php @@ -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()); diff --git a/tests/Unit/Helpers/TranslationServiceProviderTest.php b/tests/Unit/Helpers/TranslationServiceProviderTest.php new file mode 100644 index 00000000..c5ba7386 --- /dev/null +++ b/tests/Unit/Helpers/TranslationServiceProviderTest.php @@ -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(); + } +} + + diff --git a/tests/Unit/Helpers/TranslatorTest.php b/tests/Unit/Helpers/TranslatorTest.php new file mode 100644 index 00000000..396d2b65 --- /dev/null +++ b/tests/Unit/Helpers/TranslatorTest.php @@ -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); + } +} + + diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index fb9e6f00..b9cedd30 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -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; @@ -196,6 +196,29 @@ class HelpersTest extends TestCase } /** + * @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 */ public function testUrl() diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index f6c24767..d7dc37c0 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -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'); + } +}
\ No newline at end of file diff --git a/tests/Unit/Http/SessionServiceProviderTest.php b/tests/Unit/Http/SessionServiceProviderTest.php index a78b4f72..d0125bc2 100644 --- a/tests/Unit/Http/SessionServiceProviderTest.php +++ b/tests/Unit/Http/SessionServiceProviderTest.php @@ -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] ); diff --git a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php index 874268b0..720af631 100644 --- a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php +++ b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php @@ -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(); diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php new file mode 100644 index 00000000..abf9c52f --- /dev/null +++ b/tests/Unit/Middleware/ErrorHandlerTest.php @@ -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); + } +} diff --git a/tests/Unit/Middleware/SetLocaleTest.php b/tests/Unit/Middleware/SetLocaleTest.php new file mode 100644 index 00000000..c4e9d2a4 --- /dev/null +++ b/tests/Unit/Middleware/SetLocaleTest.php @@ -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); + } +} diff --git a/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php index 323e07b4..370187dd 100644 --- a/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php +++ b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php @@ -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; + } } diff --git a/tests/Unit/Renderer/RendererServiceProviderTest.php b/tests/Unit/Renderer/RendererServiceProviderTest.php index 3826da7e..6cdf4363 100644 --- a/tests/Unit/Renderer/RendererServiceProviderTest.php +++ b/tests/Unit/Renderer/RendererServiceProviderTest.php @@ -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] ); diff --git a/tests/Unit/Renderer/Twig/Extensions/ConfigTest.php b/tests/Unit/Renderer/Twig/Extensions/ConfigTest.php new file mode 100644 index 00000000..6a9cb78a --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/ConfigTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/Twig/Extensions/ExtensionTest.php b/tests/Unit/Renderer/Twig/Extensions/ExtensionTest.php new file mode 100644 index 00000000..e1c5a378 --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/ExtensionTest.php @@ -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)) + ); + } +} diff --git a/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php b/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php new file mode 100644 index 00000000..6cc3a4da --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/GlobalsTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/Twig/Extensions/SessionTest.php b/tests/Unit/Renderer/Twig/Extensions/SessionTest.php new file mode 100644 index 00000000..7ce4dc3a --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/SessionTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php b/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php new file mode 100644 index 00000000..f1548604 --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/Twig/Extensions/UrlTest.php b/tests/Unit/Renderer/Twig/Extensions/UrlTest.php new file mode 100644 index 00000000..c7e40bea --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/UrlTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/TwigEngineTest.php b/tests/Unit/Renderer/TwigEngineTest.php new file mode 100644 index 00000000..9d0618f1 --- /dev/null +++ b/tests/Unit/Renderer/TwigEngineTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/TwigLoaderTest.php b/tests/Unit/Renderer/TwigLoaderTest.php new file mode 100644 index 00000000..e6867643 --- /dev/null +++ b/tests/Unit/Renderer/TwigLoaderTest.php @@ -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); + } +} diff --git a/tests/Unit/Renderer/TwigServiceProviderTest.php b/tests/Unit/Renderer/TwigServiceProviderTest.php new file mode 100644 index 00000000..3cd0da4d --- /dev/null +++ b/tests/Unit/Renderer/TwigServiceProviderTest.php @@ -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); + } +} |