Symfony po krůčkách – Twig II.

Dnes navážeme na předchozí díl a podíváme se na to, jak je Twig zakomponován do Symfony a na další skvělé věci, které nám tento šablonovací systém umožňuje. Tímto bych taky chtěl poděkovat všem, kteří se zapojili do diskuze pod prvním dílem a přinesli zajímavé postřehy a tipy na další témata, z nichž některým se dnes budeme věnovat blíže.

Seriál: Symfony po krůčkách (18 dílů)

  1. Symfony po krůčkách – Event Dispatcher 30.11.2015
  2. Symfony Console jako první rande se Symfony 7.12.2015
  3. Symfony po krůčkách – Filesystem a Finder 14.12.2015
  4. Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání 21.12.2015
  5. Symfony po krůčkách – spouštíme procesy 4.1.2016
  6. Symfony po krůčkách – Translation – překlady jednoduše 11.1.2016
  7. Symfony po krůčkách – Validator (1) 18.1.2016
  8. Symfony po krůčkách – Validator (2) 25.1.2016
  9. Symfony po krůčkách – Routing 1.2.2016
  10. Symfony po krůčkách – MicroKernel 9.2.2016
  11. Konfigurujeme Symfony pomocí YAMLu 16.2.2016
  12. Symfony po krůčkách – oblékáme MicroKernel 23.2.2016
  13. Symfony po krůčkách – ClassLoader 29.2.2016
  14. Symfony po krůčkách – Twig 8.3.2016
  15. Symfony po krůčkách – Twig II. 15.3.2016
  16. Symfony po krůčkách – DomCrawler a CssSelector 23.3.2016
  17. Symfony po krůčkách – HTTP fundamentalista 12.4.2016
  18. Symfony po krůčkách – ušli jsme pořádný kus 19.4.2016

Tvorba vlastní funkce a filtru

Minule jsme si popsali, co jsou ve Twigu funkce a filtry a ukázali jsme si, jak se používají. Teď se podíváme na to, jak si napsat filtr vlastní. Je to velice jednoduché, jde jen o to vytvořit instanci třídy Twig_SimpleFilter. Jako první argument se zadává název našeho filtru, jako druhý argument pak PHP callable určující, co má filtr dělat. V našem případě filtr transformuje daný řetězec do MD5 hashe:

$md5Filter = new Twig_SimpleFilter('md5', function ($string) {
 return md5($string);
});

Vzhledem k tomu, že v tomto případě používáme jednoduchou PHP funkci, nemuseli bychom jako druhý argument ani používat anonymní funkci, ale mohli bychom definovat filtr i tímto způsobem:

$md5Filter = new Twig_SimpleFilter('md5', 'md5');

Nový filtr pak musíme zaregistrovat do našeho Twig prostředí:

$twig->addFilter($md5Filter);

V šabloně teď náš filtr můžeme používat jako jakýkoliv jiný:

{{ 'example'|md5 }}

Stejně jednoduché je vytváření vlastní Twig funkce, např. pro výpočet podobnosti dvou řetězců pomocí levenshteinovy vzdálenosti:

$levenshteinFunction = new Twig_SimpleFunction('levenshtein', function ($string1, $string2) {
 return levenshtein($string1, $string2);
});

$twig->addFilter($md5Filter);

Lze také vytvořit vlastní extension, která může obsahovat více filtrů a funkcí:

//extension-example.php
require_once __DIR__ . '/vendor/autoload.php';

class MyTwigExtension extends Twig_Extension {

 public function getName() {
  return self::class;
 }

 public function getFilters() {
  return [
   new Twig_SimpleFilter('md5', [$this,     'myMd5Filter']),
  ];
 }

 public function getFunctions() {
  return [
   new Twig_SimpleFunction('levenshtein', [$this, 'myLevenshteinFunction']),
  ];
 }

 public function myMd5Filter($string) {
  return md5($string);
 }

 public function myLevenshteinFunction($string1, $string2) {
  return levenshtein($string1, $string2);
 }

}

$loader = new Twig_Loader_Filesystem('Resources/views');
$twig = new Twig_Environment($loader);
$twig->addExtension(new MyTwigExtension());

echo $twig->render('extension-example.html.twig', [
 'exampleString' => 'exampleString',
 'exampleString2' => 'exampleString2',
]);

Teď už můžeme v šablonách využívat filtr i funkce z naší extension:

{# extension-example.html #}
MD5 hash of {{ exampleString }} is {{ exampleString|md5 }}<br>
Levenshtein distance of {{ exampleString }} and {{ exampleString2 }} is {{ levenshtein(exampleString, exampleString2) }}.

Abychom se vyhnuli vynalézání kola, je v praxi dobré se před vytvářením vlastní extension kouknout do oficiálního repozitáře Twig Extensions, zda už náhodou požadovaný filtr nebo funkce neexistuje. Pokud ne, můžeme naopak do zmiňovaného repozitáře přispět.

Pro další informace koukni do dokumentace.

Makro

Makra jsou v podstatě obdoba klasických funkcí v kterémkoliv programovacím jazyce. Stejně jako používáme v programování funkce pro převádění vstupů na výstupy, lze ve Twigu použít makra. Za pomocí maker můžeme definovat často používané bloky HTML kódu a vykreslovat výstupy, které se mohou lišit na základě daných vstupů. Díky tomu se v našich šablonách nemusíme opakovat.

Rozdíl oproti funkcím ve Twigu spočívá v tom, že funkce jsou definovány v PHP, a mohou tak využívat aplikační logiku. Makro oproti tomu nemá přístup k ničemu jinému než k datům šablony.

Opět si ukažme jednoduchý příklad – v naší aplikaci chceme na více místech vykreslovat menu, pokaždé ale s jinými položkami a s jinou barvou pozadí. K tomu využijeme makro, kterému předáme jako parametry seznam odkazů a název CSS třídy. Makro definujeme v souboru macro-example.html.twig:

{# macro-example.html.twig #}
{% macro menu(menuItems, cssClass) %}
 <ul class="{{ cssClass }}">
  {% for menuItem in menuItems %}
   <li>{{ menuItem }}</li>
  {% endfor %}
 </ul>
{% endmacro %}

Do šablony si předáme proměnné, které naše makro vyžaduje:

//index.php
$loader = new Twig_Loader_Filesystem('Resources/views');
$twig = new Twig_Environment($loader);
$menuItems = [
 'About us',
 'Contact',
 'Products',
];
$menuClass = 'main-menu';
echo $twig->render('index.html.twig', [
 'menuItems' => $menuItems,
 'menuClass' => $menuClass,
]);

V naší šabloně pak makro importujeme a zavoláme následujícím způsobem:

{# index.html.twig #}
{% import 'macro-example.html.twig' as menuMacro %}
{{ menuMacro.menu(menuItems, menuClass) }}

Další možností je definovat makro přímo v šabloně, kde jej chceme použít. Pak bude volání makra vypadat takto:

{# index.html.twig #}
{% import _self as menuMacro %}
{{ menuMacro.menu(menuItems, menuClass) }}

Pokud v takovém případě nepoužijeme import a místo toho zavoláme přímo _self.menu, tak to sice fungovat bude, ale od Twigu ve verzi 2.x již nikoliv.

Embed

Další užitečná vychytávka Twigu se nazývá embed a kombinuje vlastnosti include a extend – umožňuje do šablony includovat obsah jiné šablony a navíc přepsat v ní definované bloky. Ukažme si jednoduchý příklad – máme administraci aplikace, kde chceme fixní lištu s ovládacími prvky na všech stránkách pro vytváření a editaci entit. Obsah této lišty ale bude na každé stránce jiný. Šablona pro lištu vypadá třeba takto:

{# fixedBar.html.twig #}
<div class="fixed-bar">
 <div class="fixed-bar__content">
  {% block fixed_bar_content %}{% endblock %}
 </div>
</div>

Pak např. na stránce s editací článku použijeme lištu následujícím způsobem:

{# articleEdit.html.twig #}
{% embed 'fixedBar.html.twig' %}
{% block fixed_bar_content %}
  <a href="{{ url('article_list') }}">Zpět na přehled</a>
  <input type="submit" value="Uložit článek">
 {% endblock %}
{% endembed %}

Sandbox

Sandbox je úžasná funkcionalita, kterou využijeme, pokud chceme umožnit uživatelům naší aplikace používat Twig s určitými omezeními, které si sami stanovíme. Pomocí whitelistu určíme, které funkce, filtry, tagy, třídní proměnné a funkce má uživatel k dispozici. Příkladem může být redakční systém, kde uživatel může nahrávat vlastní šablony.

Abychom mohli tag sandbox používat, je nutné nejdříve přidat příslušné rozšíření Twigu a definovat politiku sandboxu:

//sandbox-example.php
require_once __DIR__ . '/vendor/autoload.php';

$loader = new Twig_Loader_Filesystem('Resources/views');
$twig = new Twig_Environment($loader);

$tags = ['if'];
$filters = ['upper'];
$methods = [
    'Article' => ['getTitle', 'getBody']
];
$properties = [
    'Article' => ['title', 'body']
];
$functions = ['range'];

$policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods, $properties, $functions);

$sandbox = new Twig_Extension_Sandbox($policy);
$twig->addExtension($sandbox);

echo $twig->render('sandbox-example.html.twig');

Sandbox se vždy používá spolu s tagem include, pokud se do něj pokusíme psát kód přímo, nebude sandbox fungovat.

{# sandbox-example.html.twig #}
{% sandbox %}
 {% include 'untrusted-template.html.twig' %}
{% endsandbox %}

V šabloně untrusted-template.html.twig budou fungovat pouze ty prostředky, které jsme definovali politikou v sandbox-example.php.

Nejen HTML šablony

Doposud jsme si ukazovali příklady používání Twigu pro HTML šablony, ale využití Twigu je daleko širší, stačí trošku upustit uzdu fantazii. Můžeme si např. vytvořit jednoduchou šablonu mailu, ve které definujeme bloky pro předmět a tělo:

{# mail.twig #}
{% block subject %}
 Important massage for {{ name }}
{% endblock %}

{% block body %}
 Hello {{ name }}!
 {{ mailBody }}
{% endblock %}

Pomocí metody renderBlock pak můžeme pohodlně vykreslit jednotlivé bloky šablony a připravit si tak předmět a tělo emailu k odeslání:

//mail.php
require_once __DIR__ . '/vendor/autoload.php';

class FakeMail {

 public $subject;
 public $body;

 public function __construct($subject, $body) {
  $this->subject = $subject;
  $this->body = $body;
 }

}
$loader = new Twig_Loader_Filesystem('Resources/views');
$twig = new Twig_Environment($loader);

$twigMailTemplate = $twig->loadTemplate('mail.twig');
/* @var $twigMailTemplate Twig_Template */

$mailSubject = $twigMailTemplate->renderBlock('subject', [
 'fullName' => 'Quentin Tarantino',
]);
$mailBody = $twigMailTemplate->renderBlock('body', [
 'firstName' => 'Quentin',
 'mailBody' => 'I really like your movies :)',
]);

$fakeMail = new FakeMail($mailSubject, $mailBody);

echo $fakeMail->subject;
// Important massage for Quentin Tarantino
echo $fakeMail->body;
// Hello Quentin! I really like your movies :)

Další pěkný příklad využití Twigu může být šablona pro generování XML feedu zboží na Heureka.cz. Šablona může vypadat třeba nějak takto:

{# heureka-template.xml.twig #}
<?xml version="1.0" encoding="utf-8"?>
<SHOP>

{% block item %}
 <SHOPITEM>
  <ITEM_ID>{{ item.itemId }}</ITEM_ID>
  <PRODUCTNAME>{{ item.productName }}</PRODUCTNAME>
  <DESCRIPTION>{{ item.description|striptags|replace("\n", ' ')|trim }}</DESCRIPTION>
  <URL>{{ item.url }}</URL>
  <PRICE_VAT>{{ item.priceVat|number_format(2, ',', '') }}</PRICE_VAT>
  {% if item.ean -%}
   <EAN>{{ item.ean }}</EAN>
  {% endif -%}
  {% for paramName, paramVal in item.params -%}
   <PARAM>
    <PARAM_NAME>{{ paramName }}</PARAM_NAME>
    <VAL>{{ paramVal }}</VAL>
   </PARAM>
  {% endfor -%}
 </SHOPITEM>
{% endblock %}

</SHOP>

Jak bude vypadat skript pro vygenerování feedu z šablony, to už nechám na vás :) Zdrojáky všech příkladů zase najdete na GitHubu.

Zase o krok dále

Dnes jsme navázali na předchozí díl povídání o Twigu a proletěli jsme spoustu dalších zajímavých možností, které nám Twig nabízí. Ukázali jsme si,

  • jak vytvořit vlastní Twig extension,
  • jak používat embed, macro a sandbox,
  • že Twig nejsou jen HTML šablony.

Vystudoval jsem softwarové inženýrství na FEI v Ostravě a od září 2014 pracuji v ShopSys jako vývojář nové e-commerce platformy postavené na Symfony frameworku.

Komentáře: 2

Přehled komentářů

Lukáš Brzák Super
Rostislav Vítek Re: Super
Zdroj: https://www.zdrojak.cz/?p=17926