Průvodce

Latte je šablonovací systém pro PHP, který vám ušetří a zpříjemní práci a zabezpečí výstup před zranitelnostmi jako je XSS.

  • Latte je rychlé: překládá šablony na jednoduchý optimalizovaný PHP kód.
  • Latte je bezpečné: je prvním PHP engineem, který přichází s Kontextově sensitivním escapováním a kontrolou odkazů.
  • Latte mluví vaším jazykem: má intuitivní syntaxi a pomáhá vám snadno vytvářet lepší webové stránky.

Ačkoliv je PHP původem šablonovací jazyk, k jejich kódování se příliš nehodí. Podívejme se, jak v šablonovacím PHP vypsat pole prvků $items jako seznam:

<?php if ($items): ?>
	<?php $counter = 1 ?>
	<ul>
	<?php foreach ($items as $item): ?>
		<li id="item-<?php echo $counter++ ?>"><?php
		echo htmlSpecialChars(mb_convert_case($item, MB_CASE_TITLE)) ?>
		</li>
	<?php endforeach ?>
	</ul>
<?php endif?>

Zápis je poměrně nepřehledný a nesmí se zapomínat na volání htmlSpecialChars. Proto vznikají v PHP nejrůznější šablonovací systémy. Jeden z nejskvělejších šablonovacích systémů je součástí Nette Framework a nese název Latte. Budete ho milovat!

V Latte se stejná šablona napíše řádově jednodušeji:

<ul n:if="$items">
{foreach $items as $item}
	<li id="item-{$iterator->counter}">{$item|capitalize}</li>
{/foreach}
</ul>

Jak vidno, v Latte se používají dva druhy direktiv:

  • tagy ve složených závorkách, například {foreach …}
  • n:atributy, například n:if="…"

Instalace a použití

Nejlepší způsob, jak nainstalovat Latte je stáhnout nejnovější balíček nebo použít Composer:

composer require latte/latte

Latte 2.5 vyžaduje PHP verze 7.1 nebo novější a je kompatibilní s PHP až 7.4. Latte 2.4 vyžaduje PHP verze 5.4 a je také kompatibilní s PHP 7.4.

Jak vykreslit šablonu? Stačí spustit tento kód:

$latte = new Latte\Engine;

$latte->setTempDirectory('/path/to/tempdir');

$params = [
	'items' => ['one', 'two', 'three'],
];

// kresli na výstup
$latte->render('template.latte', $params);
// kresli do řetězce
$html = $latte->renderToString('template.latte', $params);

Latte automaticky přegenerovává cache při každé změně šablony, což můžeme v produkčním prostředí vypnout a ušetřit tím malinko výkonu:

$latte->setAutoRefresh(false);

Místo pole $params můžete také použít objekt, což přináší některé výhody. Získáte pohodlné napovídání v IDE a cestu pro registraci filtrů a funkcí:

class MyTemplate
{
	public $items = ['one', 'two', 'three'];
}

$params = new MyTemplate;
$latte->render('template.latte', $params);

Integrace a pluginy

Pište šablony v editoru nebo IDE, který má podporu pro Latte. Bude to mnohem příjemnější.

  • NetBeans IDE má podporu vestavěnou
  • PhpStorm: nainstalujte v Settings > Plugins > Marketplace plugin Latte
  • VS Code: hledejte v markerplace „Nette Latte + Neon“ plugin
  • Sublime Text 3: v Package Control najděte a nainstalujte balíček Nette a zvolte Latte ve View > Syntax
  • pro zvýrazňování kódu na webu použijte Prism.js
  • ve starých editorech použijte pro soubory .latte zvýrazňování Smarty

Plugin pro PhpStorm je velmi pokročilý a umí výborně napovídat PHP kód. Aby fungoval optimálně, používejte typované šablony.

Tagy

Popis všech výchozích tagů (neboli značek či maker) najdete na samostatné stránce. Krom toho můžete vytvářet i vlastní tagy.

Všechny párové značky, například {if} … {/if}, operující nad jedním HTML elementem, se dají přepsat do podoby n:atributů. Takto by bylo možné zapsat například i {foreach}

<ul n:if="$items">
	<li n:foreach="$items as $item">{$item|capitalize}</li>
</ul>

A to se s n:atributy dají dělat ještě daleko zajímavější kousky, jak se za chvíli dozvíte.

Tag {$item|capitalize}, který slouží k vypsání proměnné $item, obsahuje tzv. filtr, v tomto případě capitalize, který převede první písmenko každého slova na velké.

Velmi důležité je, že Latte vypisované proměnné automaticky escapuje. Vypsání proměnné totiž vyžaduje escapování, tj. převedení znaků majících v HTML speciální význam na jiné odpovídající sekvence. Opomenutí by vedlo ke vzniku závažné bezpečnostní díry Cross Site Scripting (XSS).

Protože v různých dokumentech a na různých místech stránky se používají jiné escapovací funkce, disponuje Latte zcela unikátní technologií Kontextově sensitivní escapování, která rozezná, ve které části dokumentu se značka nachází a podle toho zvolí správné escapování. Nemusíte se proto bát, že váš kodér na escapování zapomene a způsobí vám velké starosti kvůli bezpečnostní díře. Což je skvělé!

Pokud by proměnná $item obsahovala HTML kód a chtěli bychom ji vypsat bez jakékoliv transformace, stačí přidat modifikátor noescape: {$item|noescape}. Opomenutí přitom nezpůsobí bezpečností díru, dle principu „less code, more security“.

Uvnitř značek přitom můžeme používat PHP takové, jaké ho známe. Dokonce včetně komentářů. Latte navíc syntaxi PHP rozšiřuje o tři příjemné novinky:

  1. můžeme vynechat uvozovky kolem řetězce z písmen, číslic a pomlček
  2. stručnější zápis podmínek $a ? 'b' odpovídající $a ? 'b' : null
  3. můžete používat optional chaining $var?->call()?->elem[1]?->item

Příklad:

{$cond ? hello}  // vypíše 'hello' pokud je $cond truthy

{$order->item?->name} // znamená isset($order->item) ? $order->item->name : null

Komentáře se zapisují {* tímto způsobem *} a do výstupu se nedostanou.

n:atributy

Ukazovali jsme si, že n:atributy (též nazývané n:makra) se zapisují přímo do HTML značek jako jejich speciální atributy. A také si říkali, že všechny párové tagy (například {if} … {/if}) se dají přepsat do podoby n:atributu. Funkcionalita se pak vztahuje na HTML element, do něhož je umístěný:

{var $items = ['I', '♥', 'Nette Framework']}

<p n:foreach="$items as $item">{$item}</p>

vypíše:

<p>I</p>
<p>♥</p>
<p>Nette Framework</p>

Pomocí prefixu inner- můžeme chování poupravit tak, aby se vztahovalo jen na vnitřní část elementu:

<div n:inner-foreach="$items as $item">
	<p>{$item}</p>
	<hr>
</div>

Vypíše se:

<div>
	<p>I</p>
	<hr>
	<p>♥</p>
	<hr>
	<p>Nette Framework</p>
	<hr>
</div>

Nebo pomocí prefixu tag- aplikujeme funkcionalitu jen na samotné HTML značky:

<p><a href="{$url}" n:tag-if="$url">Title</a></p>

Což vypíše v závislosti na proměnné $url:

{* když je $url prázdné *}
<p>Title</p>

{* když $url obsahuje 'https://nette.org' *}
<p><a href="https://nette.org">Title</a></p>

Avšak n:atributy nejsou jen zkratkou pro párové značky. Existují i ryzí n:atributy, jako třeba n:href nebo velešikovný pomocník kodéra n:class.

Filtry

Podívejte se na přehled standardních filtrů.

Filtry se zapisují za svislítko (může být před ním mezera) do tzv. modifikátoru:

<h1>{$heading|upper}</h1>

Filtry lze zřetězit a poté se aplikují v pořadí od levého k pravému:

<h1>{$heading|lower|capitalize}</h1>

Parametry se zadávají za jménem filtru oddělené dvojtečkami nebo čárkami:

<h1>{$heading|truncate:20,''}</h1>

Filtry lze aplikovat i na výraz:

{var $name = ($title|upper) . ($subtitle|lower)}</h1>

Vlastní filter

Jako filtr lze do šablony zaregistrovat libovolný callback:

$latte = new Latte\Engine;
$latte->addFilter('shortify', function (string $s): string {
	return mb_substr($s, 0, 10); // zkrátí text na 10 písmen
});

V tomto případě by bylo šikovnější, kdyby filtr přijímal další parametr:

$latte->addFilter('shortify', function (string $s, int $len = 10): string {
	return mb_substr($s, 0, $len);
});

V šabloně se potom volá takto:

<p>{$text|shortify}</p>
<p>{$text|shortify:100}</p>

Druhým způsobem definice filtru je třída šablony. Důležité je uvést anotaci @filter:

class MyTemplate
{
	/** @filter */
	public function shortify(string $s, int $len = 10): string
	{
		return mb_substr($s, 0, $len); // zkrátí text na 10 písmen
	}
}

$params = new MyTemplate;
...
$latte->render('template.latte', $params);

Univerzální filter

Manuální registraci více filtrů lze nahradit registrací jednoho univerzálního:

$latte->addFilter(null, 'Filters::common');

Ten dostane jako parametr název požadovaného filtru:

class Filters
{
	public static function common($filter, $value)
	{
		if (method_exists(__CLASS__, $filter)) {
			$args = func_get_args();
			array_shift($args);
			return call_user_func_array([__CLASS__, $filter], $args);
		}
	}

	public static function shortify($s, $len = 10)
	{
		return mb_substr($s, 0, $len);
	}
}

Funkce

V Latte můžete používat všechny funkce PHP a zároveň si definovat své vlastní:

$latte = new Latte\Engine;
$latte->addFunction('random', function (...$args) {
	return array_rand($args);
});

Použití je pak stejné, jako když voláte PHP funkci:

{random(jablko, pomeranč, citron)} // vypíše například: jablko

Druhým způsobem definice funkce je třída šablony. Důležité je uvést anotaci @function:

class MyTemplate
{
	/** @function */
	public function random(...$args)
	{
		return array_rand($args);
	}
}

$params = new MyTemplate;
...
$latte->render('template.latte', $params);

Typované šablony

Typový systém je klíčový pro vývoj robustních aplikací. Latte přináší podporou typů i do šablon. Díky tomu, že víme, jaký datový či objektový typ je v každé proměnné, může

Obojí zásadním způsobem zvyšuje kvalitu a pohodlí vývoje.

Jak začít používat typy? Vytvořte si třídu šablony, např. CatalogTemplate, reprezentující předávané parametry:

class CatalogTemplate
{
	/** @var array */
	public $langs;

	/** @var ProductEntity[] */
	public $products;
}

$params = new CatalogTemplate;
$params->langs = $settings->getLanguages();
$params->products = $entityManager->getRepository('Product')->findAll();
$latte->render('template.latte', $params);

A dále na začátek šablony vložte značku {templateType} s plným názvem třídy (včetně namespace):

{templateType MyApp\CatalogTemplate}

Tím jsme definovali, že v šabloně budou proměnné $langs a $products včetně příslušných typů. Od té chvíle vám může IDE správně našeptávat.

Typy lokálních proměnných můžete uvést pomocí značek {var}, {varType}, {define}:

{var string $name = $article->getTitle()}
{varType Nette\Security\User $user}

Jak si ušetřit práci? Jak co nejsnáze napsat třídu s parametry šablony nebo značky {varType}? Nechte si je vygenerovat. Od toho existuje dvojice značek {templatePrint} a {varPrint}. Pokud je umístíte do šablony, místo běžného vykreslení se zobrazí návrh kódu třídy resp. seznam značek {varPrint}. Kód pak stačí jedním kliknutím označit a zkopírovat do projektu.

Sandbox režim

Latte má pancéřový bunkr přímo pod kapotou. Říká se mu sandbox režim a jde o důležitou funkci chránící aplikace, ve kterých se používají šablony z nedůvěryhodných zdrojů. Například když je editují samotní uživatelé.

Sandbox znamená pískoviště a tento režim hlídá, aby se písek nedostal mimo vyhrazenou plochu. Tedy poskytuje omezený přístup k makrům, filtrům, funkcím, metodám atd. Jak to funguje? Jednoduše nadefinujeme, co všechno šabloně dovolíme. Přičemž v základu je všechno zakázané a my postupně povolujeme:

Následujícím kódem umožníme autorovi šablony používat značky {block}, {if}, {else} a {=}, což je značka pro vypsání proměnné nebo výrazu a všechny filtry:

$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowMacros(['block', 'if', 'else', '=']);
$policy->allowFilters($policy::ALL);

$latte->setPolicy($policy);

Dále můžeme povolit jednotlivé funkce, metody nebo properties objektů:

$policy->allowFunctions(['trim', 'strlen']);
$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']);
$policy->allowProperties(Nette\Database\Row::class, $policy::ALL);

Není to úžasné? Můžete na velmi nízké úrovni kontrolovat úplně všechno. Pokud se šablona pokusí zavolat nepovolenou funkci nebo přistoupit k nepovolené metodě nebo property, skončí to výjimkou Latte\SecurityViolationException.

Tvořit policy od bodu nula, kdy je zakázáno úplně vše, nemusí být pohodlné, proto můžete začít od bezpečného základu:

$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy();

Bezpečný základ znamená, že jsou povoleny všechny standardní makra kromě contentType, debugbreak, dump, extends, import, include, includeblock, layout, php, sandbox, snippet, snippetArea, templatePrint, varPrint, widget. Jsou povoleny standardní filtry kromě datastream, noescape a nocheck. A nakonec je povolený přístup k metodám a properites objektu $iterator.

Pravidla se aplikují pro šablonu, kterou vložíme značkou {sandbox}. Což je jakási obdoba {include}, která však zapíná bezpečný režim a také nepředává žádné proměnné:

{sandbox 'untrusted.latte'}

Tedy layout a jednotlivé stránky mohou nerušeně využívat všechna makra a proměnné, pouze na šablonu untrusted.latte budou uplatněny restrikce.

Některé prohřešky, jako použití zakázaného marka nebo filtru, se odhalí v době kompilace. Jiné, jako třeba volání nepovolených metod objektu, až za běhu. Šablona také může obsahovat jakékoliv jiné chyby. Aby vám ze sandboxované šablony nemohla vyskočit výjimka, která naruší celé vykreslování, lze definovat vlastní exception handler, který ji třeba jen zaloguje:

$latte->setExceptionHandler(function (Throwable $e, Latte\Runtime\Template $template) use ($logger) {
	$logger->log($e);
});

Pokud bychom chtěli sandbox režim zapnout přímo pro všechny šablony, jde to snadno:

$latte->setSandboxMode();

Rychlost

Latte je nesmírně rychlé. Překládá totiž šablony do nativního PHP kódu a ukládá do cache na disk. Díky tomu mají stejný výkon, jako bychom psali šablony v čistém PHP. Nicméně v přehlednosti, bezpečnosti a efektivitě jsou o několik řádů dál.

Šablona se automaticky regeneruje pokaždé, když změníme zdrojový soubor. Během vývoje si tedy pohodlně editujeme šablony v Latte a změny okamžitě vidíme v prohlížeči.

Debuggování

O jakékoliv chybě v šabloně nebo překlepu vás bude informovat Laděnka s plnou parádou. Zobrazí zdrojový kód šablony a červeně označí řádek, na kterém je chyba, společně s výstižnou chybovou zprávou. Jedním kliknutím pak otevřeme šablonu ve svém oblíbeném editoru a chybu můžeme okamžitě opravit. Snadnější to už být nemůže. Leda, že by se chyby opravovaly samy (inspirace pro budoucí verze? ;-) ).

Pokud používáte IDE s možností krokování kódu, můžete takto procházet i vygenerovaný PHP kód šablon.

Použitelnost

Syntax Latte nebyla vymyšlena inženýry od stolu, ale vzešla z ryze praktických požadavků webdesignerů. Hledali jsme tu nejpřívětivější syntax, se kterou elegantně zapíšete i konstrukce, které jinak představují skutečný oříšek. Budete překvapeni, jak moc vám Latte zjednoduší práci.

Najdete tu značky pro pokročilou tvorbu layoutů, pro tzv. dědičnost šablon, vnořené bloky a podobně. Přitom syntaxe vychází přímo z PHP, nebudete se tedy muset učit něco zcela nového a zúročíte své knowhow.

Kontextově sensitivní escapování

Byť je Cross Site Scripting (XSS) jedním z nejtriviálnějších způsobů narušení webových stránek, jde o zranitelnost nejčastější. Přitom velmi závažnou, protože může vést k zcizení identity a podobně. Obranou je důsledné escapování vypisovaných dat, tj. převod znaků majících v daném kontextu speciální význam na jiné odpovídající sekvence.

Pokud kodér na escapování zapomene, vznikne v aplikaci bezpečnostní díra. Proto šablonovací systémy začínají přicházet s automatickým escapováním. Problém je ale v tom, že na webové stránce existují různé kontexty a v každém je potřeba ošetřit vypisované proměnné trošku jinak. Může tak vzniknou bezpečnostní díra i kvůli špatně zvolené escapovací funkci.

Latte je však dál. Disponuje unikátní technologií Kontextově sensitivní escapování (Context-Aware Escaping), která rozezná, ve které části dokumentu se značka nachází a podle toho zvolí správné escapování. Co to znamená?

Že v Latte není potřeba nic manuálně ošetřovat. Vše se děje automaticky, správně a důsledně. Nemusíte se bát bezpečnostních děr.

Podívejme se, jak to funguje. Vytvoříme si šablonu:

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Pokud bude proměnná $movie obsahovat řetězec 'Amarcord & 8 1/2', vygeneruje se následující výstup. Všimněte si, že uvnitř HTML se použije jiné escapování, než uvnitř JavaScriptu a ještě jiné v atributu onclick:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

Díky kontextově sensitivnímu escapování je šablona jednoduchá a vaše aplikace přitom bude perfektně zabezpečená proti Cross Site Scripting. Dokonce je možné zcela nativně používat PHP proměnné uvnitř JavaScriptu!

JavaScript

Řetězec vkládaný do JavaScriptu se escapuje včetně uvozovek. Pokud jej chcete vložit do jiného řetězce, jednoduše je spojte:

<script>
	alert('Hello ' + {$name} + '!');  # správně
	alert('Hello {$name} !');  # špatně!
</script>

Kontrola odkazů

Latte automaticky kontroluje, zda proměnná použitá v atributech src nebo href obsahuje webovou URL (tj. protokol HTTP) a předchází vypsání odkazů, které mohou představovat bezpečnostní riziko.

{var $link = 'javascript:attack()'}

<a href="{$link}">klikni</a>

Vypíše:

<a href="">klikni</a>

Kontrola se dá vypnout pomocí filtru noCheck.

Hezký výstup

Puntičkáře potěší podoba HTML výstupu, který Latte generuje. Všechny značky budou odsazeny přesně tak, jak jsme zamýšleli. Kód bude vypadat, jako by byl zpracován nějakým HTML code beautifierem :-)

Vlastní tagy

Latte poskytuje API pro tvorbu vlastních tagů. Není to nic složitého. Značky přidáváme v sadách, přičemž sadu může tvořit i jediná značka.

$latte = new Latte\Engine;

// vytvoříme si sadu
$set = new Latte\Macros\MacroSet($latte->getCompiler());

// do sady přidáme párové tagy {try} ... {/try}
$set->addMacro(
	'try', // název tagu
	'try {',  // PHP kód nahrazující otevírací tag
	'} catch (\Exception $e) {}' // kód nahrazující uzavírací tag
);

Pokud značka není párová, třetí parametr metody addMacro() vynecháme.

PHP kód uváděný ve druhém a třetím parametru může obsahovat zástupné symboly:

  • %node.word – vloží první argument tagu
  • %node.array – vloží argumenty tagu naformátované jako PHP pole
  • %node.args – vloží argumenty tagu naformátované jako PHP kód
  • %escape(...) – nahradí za aktuální escapovací funkcí
  • %modify(...) – nahradí sérií modifikátorů

Příklad:

$set->addMacro('if', 'if (%node.args):', 'endif');

Pokud je logika značek ještě složitější, můžeme místo řetězců uvést callbacky či lambda funkce. Jako první parametr dostanou objekt MacroNode reprezentující aktuální uzel, druhým parametrem je objekt PhpWriter, který usnadňuje generování výstupního kódu.

$set->addMacro('if', function ($node, $writer) {
	return $writer->write('if (%node.args):');
}, 'endif');

Načítání šablon z řetězce

Pokud nemáme naši šablonu uloženou v souboru, ale pouze v proměnných, musíme nastavit loader na Latte\Loaders\StringLoader.

$latte->setLoader(new Latte\Loaders\StringLoader([
	'main' => '{if true} {$var} {/if}',
]));

$latte->render('main', $params);