Commit 1b92f5d3 authored by Avris's avatar Avris

Merge branch 'v4.0' into 'master'

v4.0

See merge request !1
parents 6eadec2a 05d12917
tests/_output/*
vendor/*
.idea/*
/.idea/*
**/.DS_Store
/vendor/*
/tests/_output/*
## Micrus Twig Bridge ##
# Micrus Twig Bridge ##
This is a module for [Micrus framework](https://micrus.avris.it) that allows you
to integrate it with [Twig](http://twig.sensiolabs.org/) template engine.
To install this module, open the file `app/Config/modules.yml` and add:
## Installation
- Avris\Micrus\Twig\TwigModule
Then run:
Run:
composer require avris/micrus-twig
Usage of Micrus's Twig extension:
Then register the module in your `App\App:registerModules`:
yield new \Avris\Micrus\Twig\TwigModule;
## Usage
Just put your `*.twig` templates in the `/templates` directory and render them in your controller.
If you don't specify an extension, `.html.twig` will be assumed:
$this->render('Post/show', ['post' => $post]); // will render Post/show.html.twig
A twig global `app` is added, which gives you access to:
* `app.user` (`null` if not logged in)
* `app.flashBag`
* `app.request`
* `app.routeMatch`
* `app.session`
<p>Logged in as {{ app.user.identifier }}</p>
<p>Current language: {{ app.locale }}</p>
{% for flash in app.flashBag.all %}
<div class="alert alert-{{ flash.type }}">
<p>{{ flash.message }}</p>
</div>
{% endfor %}
<p>Used controller: {{ app.request.routeMatch.target }}</p>
Also some helper functions are provided:
{% if routeExists('routeName') %}
<p>
<a href="route('routeName', {foo: bar})">{{ 'menu.routeName.link'|l }}</a>
<img src="asset('images/filename.png')" alt="{{ 'menu.routeName.alt' }}"/>
</p>
{% endif %}
* `route('route', {params: value})`
* `routeExists('route')`
* `asset('asset.css')`
* `isGranted('ROLE_ADMIN')`
* `canAccess('check', object)`
* `dump(object)`
### Extending Twig ###
## Extending Twig
To create Twig extension, please follow [its documentation](http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension).
To register that extension using Micrus's DI Container, just declare it with a tag `twigExtension`:
Any class extending `Twig\Extension\AbstractExtension` in an autoloaded directory
will be automatically registered as a Twig extension.
To do it manually, just declare it with a tag `twigExtension`:
myTwigExtension:
class: App\Service\MyTwigExtension
tags: [twigExtension]
App\Service\MyTwigExtension:
tags: [twigExtension]
### Copyright ###
## Copyright ###
* **Author:** Andrzej Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://mit.avris.it)
......@@ -11,8 +11,8 @@
"homepage": "https://avris.it"
}],
"require": {
"avris/micrus": "^3.0",
"twig/twig": "^1.31"
"avris/micrus": "^4.0",
"twig/twig": "^2.4"
},
"autoload": {
"psr-4": { "Avris\\Micrus\\Twig\\": "src" }
......
instanceof:
Twig_Extension:
tags: ['twigExtension']
Twig\Extension\AbstractExtension:
tags: ['twigExtension']
Avris\Micrus\Twig\:
dir: '%MODULE_DIR%/src/'
Avris\Micrus\Twig\TwigEngine:
arguments:
$dirs: '@Avris\Micrus\View\TemplateDirsResolver'
$cacheDir: '%CACHE_DIR%/twig'
Twig\Environment:
resolve: '@Avris\Micrus\Twig\TwigEngine.twig'
<?php
namespace Avris\Micrus\Twig;
class Loader extends \Twig_Loader_Filesystem
use Twig\Loader\FilesystemLoader;
final class Loader extends FilesystemLoader
{
protected function findTemplate($name)
protected function findTemplate($name, $throw = true)
{
return parent::findTemplate(strpos($name, '.') === false ? $name . '.html.twig' : $name);
}
......
<?php
namespace Avris\Micrus\Twig;
use Avris\Micrus\Exception\NotFoundException;
use Avris\Micrus\Bootstrap\ContainerInterface;
use Avris\Micrus\View\Templater;
use Avris\Dispatcher\EventSubscriberInterface;
use Avris\Micrus\View\EngineInterface;
use Avris\Micrus\View\TemplateHelper;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\TwigFunction;
class TwigTemplater extends Templater
final class TwigEngine implements EngineInterface, EventSubscriberInterface
{
/** @var \Twig_Environment */
protected $twig;
public function __construct(ContainerInterface $container, $cacheDir)
{
parent::__construct($container);
$this->addDir(__DIR__ . '/View');
$this->twig = new \Twig_Environment(
new Loader($this->dirs),
/** @var Environment */
private $twig;
/** @var string[] */
private $dirs;
public function __construct(
array $dirs,
TemplateHelper $helper,
bool $envAppDebug,
string $cacheDir,
array $twigExtensions
) {
$this->dirs = $dirs;
$this->twig = new Environment(
new Loader($dirs),
[
'cache' => $cacheDir,
'debug' => $this->env == 'prod' ? false : true,
'debug' => $envAppDebug,
'strict_variables' => true,
]
);
$this->customizeTwig();
$this->customizeTwig($helper);
foreach ($container->getByTag('twigExtension') as $extension) {
foreach ($twigExtensions as $extension) {
$this->twig->addExtension($extension);
}
}
protected function customizeTwig()
private function customizeTwig(TemplateHelper $helper)
{
$this->twig->addGlobal('app', $this->app);
$this->twig->addGlobal('app', $helper->getApp());
$this->twig->addFunction(new \Twig_SimpleFunction(
$this->twig->addFunction(new TwigFunction(
'route',
[$this, 'route'],
[$helper, 'route'],
['is_safe' => ['html_attr']]
));
$this->twig->addFunction(new \Twig_SimpleFunction(
$this->twig->addFunction(new TwigFunction(
'routeExists',
[$this, 'routeExists']
[$helper, 'routeExists']
));
$this->twig->addFunction(new \Twig_SimpleFunction(
$this->twig->addFunction(new TwigFunction(
'asset',
[$this, 'asset'],
[$helper, 'asset'],
['is_safe' => ['html_attr']]
));
$this->twig->addFunction(new \Twig_SimpleFunction(
$this->twig->addFunction(new TwigFunction(
'isGranted',
[$this, 'isGranted']
[$helper, 'isGranted']
));
if (class_exists('Symfony\Component\VarDumper\Dumper\HtmlDumper')) {
$cloner = new VarCloner();
$htmlDumper = new HtmlDumper();
$cliDumper = new CliDumper();
$this->twig->addFunction(new \Twig_SimpleFunction(
'dump',
function ($var, $html = true) use ($cloner, $htmlDumper, $cliDumper) {
$dumper = $html ? $htmlDumper : $cliDumper;
$this->twig->addFunction(new TwigFunction(
'canAccess',
[$helper, 'canAccess']
));
return $dumper->dump($cloner->cloneVar($var), true);
},
['is_safe' => ['html']]
));
}
$cloner = new VarCloner();
$htmlDumper = new HtmlDumper();
$cliDumper = new CliDumper();
$this->twig->addFunction(new TwigFunction(
'dump',
function ($var, $html = true) use ($cloner, $htmlDumper, $cliDumper) {
$dumper = $html ? $htmlDumper : $cliDumper;
return $dumper->dump($cloner->cloneVar($var), true);
},
['is_safe' => ['html']]
));
}
/**
* @param $vars
* @return string
* @throws NotFoundException
*/
public function render(array $vars)
public function render(string $template, array $vars): string
{
return $this->twig->render($vars['_view'], $vars);
return $this->twig->render($template, $vars);
}
public function hasTemplate($templateName)
public function hasTemplate(string $template): bool
{
try {
$this->twig->load($templateName);
$this->twig->load($template);
return true;
} catch (\Twig_Error_Loader $e) {
} catch (LoaderError $e) {
return false;
}
}
/**
* @return \Twig_Environment
*/
public function getTwig()
public function getTwig(): Environment
{
return $this->twig;
}
public function onCacheWarmup()
public function warmup()
{
foreach ($this->dirs as $dir) {
$it = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
......@@ -117,9 +119,14 @@ class TwigTemplater extends Templater
if (!$file->isDir()) {
try {
$this->twig->load(substr($filename, strlen($dir) + 1));
} catch (\Twig_Error $e) {}
} catch (LoaderError $e) {}
}
}
}
}
public function getSubscribedEvents(): iterable
{
yield 'cacheWarmup' => [$this, 'warmup'];
}
}
<?php
namespace Avris\Micrus\Twig;
use Avris\Micrus\Bootstrap\Module;
use Avris\Micrus\Bootstrap\ModuleInterface;
use Avris\Micrus\Bootstrap\ModuleTrait;
class TwigModule implements Module
final class TwigModule implements ModuleInterface
{
public function extendConfig($env, $rootDir)
{
return [
'services' => [
'templater' => [
'class' => TwigTemplater::class,
'params' => ['@container', '{@rootDir}/run/cache/{@env}/twig'],
'events' => ['cacheWarmup'],
],
'twig' => [
'resolve' => '@templater.twig',
],
]
];
}
use ModuleTrait;
}
{% extends 'layout' %}
{% block content %}
<h3>{{ 'Error %code%'|l({'%code%': errorCode}) }}</h3>
<p><a href="{{ route('home') }}"><span class="fa fa-home"></span> {{ 'Back to homepage'|l }}</a></p>
{% endblock %}
{% if breadcrumb.icon is defined %}
<span class="{{ breadcrumb.icon }}"></span>
{% endif %}
{{ breadcrumb.text }}
<nav aria-label="breadcrumb {{ class|default }}">
<ol class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li class="breadcrumb-item {% if loop.last %}active{% endif %}">
{% if breadcrumb.link is defined and not loop.last %}
<a href="{{ breadcrumb.link }}">
{% include 'Bootstrap/breadcrumb' %}
</a>
{% else %}
{% include 'Bootstrap/breadcrumb' %}
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
<div class="navbar navbar-expand-lg {{ class|default('fixed-top navbar-dark bg-primary') }}">
<div class="container">
<a href="{{ brand.link }}" class="navbar-brand">
{% if brand.image is defined %}
<img src="{{ brand.image.src }}" alt="{{ brand.image.alt|default('') }}"/>
{% endif %}
{{ brand.text }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
{% for nav in navs if nav.condition is not defined or nav.condition %}
<ul class="navbar-nav {{ nav.class|default('') }}">
{% for item in nav.items if item.condition is not defined or item.condition %}
{% if item.items is defined %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {{ subitem.class|default('') }}" data-toggle="dropdown" href="#" id="{{ item.id }}" aria-expanded="false">
{% if item.icon is defined %}<span class="{{ item.icon }}"></span>{% endif %}
{% if item.wrapper is defined%}<span class="{{ item.wrapper }}">{% endif %}
{{ item.html|default('')|raw }}
{{ item.text|default('') }}
{% if item.wrapper is defined%}</span>{% endif %}
<span class="caret"></span>
</a>
<div class="dropdown-menu" aria-labelledby="{{ item.id }}">
{% for subitem in item.items if subitem.condition is not defined or subitem.condition %}
{% if subitem.divider|default(false) %}
<div class="dropdown-divider"></div>
{% else %}
<a class="dropdown-item {{ subitem.class|default('') }}" href="{{ subitem.link }}" {% if subitem.external|default(false) %}target="_blank" rel="noopener"{% endif %}>
{% if subitem.icon is defined %}<span class="{{ subitem.icon }}"></span>{% endif %}
{{ subitem.html|default('')|raw }}
{{ subitem.text|default('') }}
</a>
{% endif %}
{% endfor %}
</div>
</li>
{% else %}
<li class="nav-item {{ subitem.class|default('') }}">
<a class="nav-link" href="{{ item.link }}" {% if item.external|default(false) %}target="_blank" rel="noopener"{% endif %}>
{% if item.icon is defined %}<span class="{{ item.icon }}"></span>{% endif %}
{% if item.wrapper is defined%}<span class="{{ item.wrapper }}">{% endif %}
{{ item.html|default('')|raw }}
{{ item.text|default('') }}
{% if item.wrapper is defined%}</span>{% endif %}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endfor %}
</div>
</div>
</div>
{% extends 'layout' %}
{% block content %}
<h3>{{ 'error:header'|l({'%code%': errorCode}) }}</h3>
<p><a href="{{ route('home') }}"><span class="fas fa-home"></span> {{ 'error:backToHome'|l }}</a></p>
{% endblock %}
header: Fehler %code%
backToHome: Zurück zur Homepage
header: Error %code%
backToHome: Back to homepage
header: Błąd %code%
backToHome: Wróć na stronę startową
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment