Nette Documentation Preview

syntax
Praktyki dla programistów
*************************


Instalacja
==========

Najlepszym sposobem instalacji Latte jest Composer:

```shell
composer require latte/latte
```

Obsługiwane wersje PHP (dotyczy najnowszych wersji patch Latte):

| wersja          | zgodna z PHP
|-----------------|-------------------
| Latte 3.1       | PHP 8.2 - 8.5
| Latte 3.0       | PHP 8.0 - 8.5


Jak wyrenderować szablon
========================

Jak wyrenderować szablon? Wystarczy ten prosty kod:

```php
$latte = new Latte\Engine;
// katalog cache
$latte->setCacheDirectory('/path/to/tempdir');

$params = [ /* template variables */ ];
// albo $params = new TemplateParameters(/* ... */);

// renderowanie na wyjście
$latte->render('template.latte', $params);
// albo renderowanie do zmiennej
$output = $latte->renderToString('template.latte', $params);
```

Parametrami mogą być tablice albo, jeszcze lepiej, [obiekt |#Parametry jako klasa], który zapewni kontrolę typów i podpowiadanie w edytorze.

.[note]
Przykłady użycia znajdziesz też w repozytorium [Latte examples |https://github.com/nette-examples/latte].


Wydajność i cache
=================

Szablony Latte są niezwykle szybkie, bo Latte kompiluje je bezpośrednio do kodu PHP i przechowuje w cache na dysku. Nie mają więc żadnego dodatkowego narzutu w porównaniu z szablonami napisanymi w czystym PHP.

Cache jest automatycznie odświeżany za każdym razem, gdy zmienisz plik źródłowy. Podczas tworzenia możesz więc wygodnie edytować szablony Latte i od razu widzieć zmiany w przeglądarce. W środowisku produkcyjnym tę funkcję można wyłączyć i oszczędzić odrobinę wydajności:

```php
$latte->setAutoRefresh(false);
```

Po wdrożeniu na serwer produkcyjny wygenerowanie cache po raz pierwszy, zwłaszcza przy większych aplikacjach, może zrozumiale chwilę potrwać. Latte ma wbudowaną ochronę przed "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. To sytuacja, w której serwer otrzymuje dużą liczbę równoczesnych żądań i ponieważ cache Latte jeszcze nie istnieje, wszystkie zaczęłyby generować go naraz. Co powoduje skok obciążenia CPU. Latte jest sprytne i przy wielu równoczesnych żądaniach cache generuje tylko pierwszy wątek, a pozostałe czekają i potem z niego korzystają.

Cache możesz też wygenerować z wyprzedzeniem przy wdrożeniu (na przykład w skrypcie deploya) metodą `Engine::warmupCache()`. Kompiluje ona podany szablon do cache zawczasu, dzięki czemu pierwszy odwiedzający nie musi czekać: `$latte->warmupCache('template.latte')`.


Sposoby rozszerzania Latte
==========================

Latte można dostosować na kilka sposobów, od prostych pomocników po zupełnie nowe konstrukcje językowe. Strona [rozszerzanie Latte |extending-latte] omawia je szczegółowo; tutaj szybki przegląd:

- **[Własne filtry|custom-filters]:** do formatowania lub przekształcania danych w wyniku szablonu (np. `{$var|myFilter}`).
- **[Własne funkcje|custom-functions]:** do własnej logiki wywoływanej w wyrażeniach szablonu (np. `{myFunction($arg)}`).
- **[Własne tagi|custom-tags]:** do zupełnie nowych konstrukcji językowych (`{mytag}...{/mytag}` albo `n:mytag`).
- **[Compiler passy|compiler-passes]:** funkcje modyfikujące AST szablonu między parsowaniem a wygenerowaniem kodu PHP (na przykład optymalizacje albo kontrole bezpieczeństwa).
- **[Własne loadery|loaders]:** do zmiany sposobu, w jaki Latte odnajduje i wczytuje pliki szablonów.

Jeśli chcesz wykorzystywać swoje rozszerzenia w różnych projektach albo udostępnić je innym, spakuj je w klasę [Latte Extension |extending-latte#Latte Extension].


Parametry jako klasa
====================

Lepiej niż przekazywać zmienne do szablonu jako tablice jest utworzyć klasę. Zyskujesz [zapis bezpieczny typowo|type-system], [wygodne podpowiadanie w IDE |recipes#Edytory i IDE] i możliwość [rejestrowania filtrów |custom-filters#Filtry używające klasy] oraz [funkcji |custom-functions#Funkcje używające klasy].

```php
class MailTemplateParameters
{
	public function __construct(
		public string $lang,
		public Address $address,
		public string $subject,
		public array $items,
		public ?float $price = null,
	) {}
}

$latte->render('mail.latte', new MailTemplateParameters(
	lang: $this->lang,
	subject: $title,
	price: $this->getPrice(),
	items: [],
	address: $userAddress,
));
```


Wyłączenie automatycznego escapowania zmiennej
==============================================

Jeśli zmienna zawiera łańcuch HTML, możesz ją oznaczyć tak, aby Latte nie escapowało jej automatycznie (a więc podwójnie). Unikniesz dzięki temu podawania `|noescape` w szablonie.

Najprościej opakować łańcuch w obiekt `Latte\Runtime\Html`:

```php
$params = [
	'articleBody' => new Latte\Runtime\Html($article->htmlBody),
];
```

Latte nie escapuje też wszystkich obiektów implementujących interfejs `Latte\Runtime\HtmlStringable`. Możesz więc utworzyć własną klasę, której metoda `__toString()` zwróci kod HTML, który nie zostanie automatycznie zescapowany:

```php
class Emphasis implements Latte\Runtime\HtmlStringable
{
	public function __construct(
		private string $str,
	) {
	}

	public function __toString(): string
	{
		return '<em>' . htmlspecialchars($this->str) . '</em>';
	}
}

$params = [
	'foo' => new Emphasis('hello'),
];
```

.[warning]
Metoda `__toString` musi zwracać poprawny HTML i zapewniać escapowanie parametrów, w przeciwnym razie może powstać podatność XSS!


Jak rozszerzyć Latte o filtry, tagi itd.
========================================

Jak dodać do Latte własny filtr, funkcję, tag itd.? Dowiesz się w rozdziale [rozszerzanie Latte|extending-latte]. Jeśli chcesz wykorzystywać swoje zmiany w różnych projektach albo udostępnić je innym, powinieneś następnie [utworzyć rozszerzenie |extending-latte#Latte Extension].


Dowolny kod w szablonie `{php ...}` .{toc: RawPhpExtension}
===========================================================

Wewnątrz tagu [`{do}` |tags#{do}] można zapisywać wyłącznie wyrażenia PHP, nie da się więc wstawić na przykład konstrukcji w rodzaju `if ... else` ani instrukcji zakończonych średnikiem.

Możesz jednak zarejestrować rozszerzenie `RawPhpExtension`, które dodaje tag `{php ...}`. Za jego pomocą wstawisz dowolny kod PHP. Nie podlega on żadnym regułom trybu sandbox, więc jego użycie jest na odpowiedzialność autora szablonu.

```php
$latte->addExtension(new Latte\Essential\RawPhpExtension);
```


Kontrola wygenerowanego kodu .{data-version:3.0.7}
==================================================

Latte kompiluje szablony do kodu PHP. Oczywiście dba o to, aby wygenerowany kod był poprawny składniowo. Przy używaniu rozszerzeń innych autorów albo `RawPhpExtension` Latte nie może jednak zagwarantować poprawności wygenerowanego pliku. W PHP da się też napisać kod poprawny składniowo, ale zabroniony (na przykład przypisanie wartości do zmiennej `$this`), powodujący PHP Compile Error. Jeśli zapiszesz taką operację w szablonie, trafi ona również do wygenerowanego kodu PHP. Ponieważ różnych zabronionych operacji jest w PHP ponad dwieście, Latte nie stawia sobie za cel ich wykrywania. Zgłosi je samo PHP przy renderowaniu, co zwykle nie stanowi problemu.

Bywają jednak sytuacje, w których chcesz już przy kompilacji szablonu wiedzieć, że nie zawiera on żadnych PHP Compile Error. Zwłaszcza gdy szablony mogą edytować użytkownicy albo gdy używasz [Sandboxa |sandbox]. W takim przypadku każ sprawdzać szablony podczas kompilacji. Tę funkcjonalność włączysz metodą `Engine::enablePhpLinter()`. Ponieważ do sprawdzenia musi wywołać binarkę PHP, przekaż jej ścieżkę jako parametr:

```php
$latte = new Latte\Engine;
$latte->enablePhpLinter('/path/to/php');

try {
	$latte->compile('home.latte');
} catch (Latte\CompileException $e) {
	// wyłapuje błędy Latte, a także Compile Error w PHP
	echo 'Error: ' . $e->getMessage();
}
```


Locale .{data-version:3.0.18}
=============================

Latte pozwala ustawić locale, które wpływa na formatowanie liczb, dat i sortowanie. Ustawia się je metodą `setLocale()`. Identyfikator locale jest zgodny ze standardem IETF language tag, którego używa rozszerzenie PHP `intl`. Składa się z kodu języka i ewentualnie kodu kraju, na przykład `en_US` dla angielskiego w Stanach Zjednoczonych, `de_DE` dla niemieckiego w Niemczech itd.

```php
$latte = new Latte\Engine;
$latte->setLocale('en_US');
```

Ustawienie locale wpływa na filtry [localDate |filters#localDate], [sort |filters#sort], [number |filters#number] i [bytes |filters#bytes].

.[note]
Wymaga rozszerzenia PHP `intl`. Ustawienie w Latte nie wpływa na globalne ustawienie locale w PHP.


Tryb ścisły .{data-version:3.0.8}
=================================

W trybie ścisłego parsowania Latte sprawdza brakujące zamykające tagi HTML, a także wyłącza możliwość używania zmiennej `$this`. Aby go włączyć:

```php
$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::StrictParsing);
```

Aby generować szablony z nagłówkiem `declare(strict_types=1)`, zrób tak:

```php
$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::StrictTypes);
```

.[note]
Od Latte 3.1 ścisłe typy są włączone domyślnie. Możesz je wyłączyć przez `$latte->setFeature(Latte\Feature::StrictTypes, false)`.


Ostrzeżenia migracyjne .{data-version:3.1.0}
============================================

Latte 3.1 zmienia zachowanie niektórych [atrybutów HTML|html-attributes]. Na przykład wartości `null` teraz usuwają atrybut zamiast wypisywać pusty łańcuch. Aby łatwo znaleźć miejsca, w których ta zmiana dotyka Twoich szablonów, możesz włączyć ostrzeżenia migracyjne:

```php
$latte->setFeature(Latte\Feature::MigrationWarnings);
```

Po włączeniu Latte sprawdza renderowane atrybuty i zgłasza ostrzeżenie użytkownika (`E_USER_WARNING`), jeśli wynik różni się od tego, co wyprodukowałoby Latte 3.0. Gdy natkniesz się na ostrzeżenie, zastosuj jedno z rozwiązań:

1. Jeśli nowy wynik jest w Twoim przypadku poprawny (np. wolisz, aby atrybut przy `null` znikał), wycisz ostrzeżenie, dodając filtr `|accept`
2. Jeśli chcesz, aby atrybut renderował się jako pusty (np. `title=""`), zamiast znikać, gdy zmienna ma wartość `null`, podaj pusty łańcuch jako wartość awaryjną: `title={$val ?? ''}`
3. Jeśli koniecznie potrzebujesz starego zachowania (np. wypisywania `"1"` dla `true` zamiast `"true"`), rzutuj wartość jawnie na łańcuch: `data-foo={(string) $val}`

Po rozwiązaniu wszystkich ostrzeżeń wyłącz ostrzeżenia migracyjne i **usuń wszystkie** filtry `|accept` z szablonów, bo nie są już potrzebne.


Zmienne pętli w zasięgu lokalnym .{data-version:3.1.3}
======================================================

Domyślnie zmienne zdefiniowane w pętli `{foreach}` (jak `$key` i `$value`) pozostają dostępne po jej zakończeniu - tak samo jak w samym PHP. Może to prowadzić do niezamierzonego nadpisania zmiennych, gdy zmienna pętli ma taką samą nazwę jak istniejąca zmienna szablonu.

Funkcja `ScopedLoopVariables` ogranicza zasięg zmiennych pętli do jej ciała. Po zakończeniu pętli pierwotna wartość zmiennej zostaje przywrócona (jeśli istniała wcześniej), a w przeciwnym razie zmienna zostaje usunięta:

```php
$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::ScopedLoopVariables);
```

Przykład różnicy:

```latte
{var $item = 'original'}
{foreach [1, 2] as $item}{$item}, {/foreach}
{$item}
```

Bez `ScopedLoopVariables`: wypisze `1, 2, 2` (zmienna zostaje nadpisana)
Z `ScopedLoopVariables`: wypisze `1, 2, original` (zmienna zostaje przywrócona)

Działa to również przy składni z destrukturyzacją, np. `{foreach $array as [$a, $b]}`.

.[note]
Zmienne pętli używające referencji (`{foreach $array as &$value}`) albo przypisania do właściwości (`{foreach $array as $obj->prop}`) nie są ograniczane zasięgiem, bo zniweczyłoby to ich zamierzone działanie.


Automatyczne usuwanie wcięć .{toc: Dedent}{data-version:3.1.3}
==============================================================

Przy używaniu tagów parzystych, takich jak `{if}`, `{foreach}` czy `{block}`, często wcinasz zagnieżdżoną treść dla czytelności. Domyślnie to wcięcie trafia jednak do wygenerowanego wyniku. Funkcja `Dedent` automatycznie je usuwa, dzięki czemu wynik pozostaje czysty niezależnie od tego, jak głęboko zagnieżdżasz tagi Latte:

```php
$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::Dedent);
```

Przykład:

```latte
{if true}
	Hello
	World
{/if}
```

Bez `Dedent` wynik zawierałby wcięcia (`\tHello\n\tWorld\n`). Z `Dedent` wcięcia są usuwane, a wynikiem jest `Hello\nWorld\n`.

Głębsze wcięcie wewnątrz bloku jest zachowywane względem wcięcia bazowego:

```latte
{if true}
	Hello
		Indented
{/if}
```

Wynik: `Hello\n\tIndented\n`.

Wcięcia wewnątrz bloku muszą być spójne (albo tabulatory, albo spacje). Jeśli zostaną wymieszane, Latte zgłosi wyjątek `Inconsistent indentation`.


Tłumaczenie w szablonach .{toc: TranslatorExtension}
====================================================

Aby dodać do szablonu [`{_...}` |tags#], [`{translate}` |tags#{translate}] i filtr [`translate` |filters#translate], użyj rozszerzenia `TranslatorExtension`. Służą one do tłumaczenia wartości lub części szablonu na inne języki. Parametrem jest callable wykonujący tłumaczenie albo obiekt typu `Nette\Localization\Translator` (podaj `null`, aby wyłączyć tłumaczenia):

```php
class MyTranslator
{
	public function __construct(private string $lang)
	{}

	public function translate(string $original): string
	{
		// tworzymy $translated z $original zgodnie z $this->lang
		return $translated;
	}
}

$translator = new MyTranslator($lang);
$extension = new Latte\Essential\TranslatorExtension(
	$translator->translate(...), // [$translator, 'translate'] w PHP 8.0
);
$latte->addExtension($extension);
```

Translator jest wywoływany w czasie działania, przy renderowaniu szablonu. Latte potrafi jednak przetłumaczyć wszystkie teksty statyczne już podczas kompilacji szablonu. Oszczędza to wydajność, bo każdy łańcuch tłumaczony jest tylko raz, a powstałe tłumaczenie zapisywane jest do skompilowanego pliku. W katalogu cache powstaje wtedy kilka skompilowanych wersji szablonu, po jednej na język. Wystarczy do tego podać język jako drugi parametr:

```php
$extension = new Latte\Essential\TranslatorExtension(
	$translator->translate(...),
	$lang,
);
```

Przez tekst statyczny rozumiemy na przykład `{_'hello'}` albo `{translate}hello{/translate}`. Teksty niestatyczne, takie jak `{_$foo}`, nadal będą tłumaczone w czasie działania.

Szablon może też przekazać translatorowi dodatkowe parametry przez `{_$original, foo: bar}` albo `{translate foo: bar}`, które otrzyma on jako tablicę `$params`:

```php
public function translate(string $original, ...$params): string
{
	// $params['foo'] === 'bar'
}
```


Debugowanie i Tracy
===================

Latte stara się, aby tworzenie aplikacji było jak najprzyjemniejsze. Do celów debugowania służą trzy tagi: [`{dump}` |tags#{dump}], [`{debugbreak}` |tags#{debugbreak}] i [`{trace}` |tags#{trace}].

Największy komfort uzyskasz, instalując świetne [narzędzie do debugowania Tracy|tracy:] i aktywując plugin do Latte:

```php
// włącza Tracy
Tracy\Debugger::enable();

$latte = new Latte\Engine;
// aktywuje rozszerzenie Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
```

Wszystkie błędy zobaczysz teraz na zgrabnym czerwonym ekranie, w tym błędy w szablonach z podświetleniem wiersza i kolumny ([wideo|https://github.com/nette/tracy/releases/tag/v2.9.0]). Jednocześnie w prawym dolnym rogu, w tzw. pasku Tracy, pojawi się zakładka Latte, gdzie przejrzyście zobaczysz wszystkie wyrenderowane szablony i ich zależności (łącznie z możliwością kliknięcia w szablon albo skompilowany kod), a także zmienne:

[* latte-debugging.webp *]

Ponieważ Latte kompiluje szablony do czytelnego kodu PHP, możesz wygodnie krokować po nich w swoim IDE.


Linter: walidacja składni szablonu .{toc: Linter}
=================================================

Narzędzie **Linter** służy do walidacji wszystkich szablonów. Jego celem jest przeskanowanie wskazanych plików i upewnienie się, że nie zawierają błędów składniowych ani odwołań do nieistniejących tagów, filtrów, funkcji, klas czy podobnych konstrukcji.

Linter uruchamia się z wiersza poleceń:

```shell
vendor/bin/latte-lint <path>
```

Parametrem `--strict` włączysz [tryb ścisły |#Tryb ścisły]. Parametr `--debug` wypisuje nazwę każdego przetwarzanego pliku i pełne szczegóły wyjątku, co pomaga przy szukaniu problemów.

Jeśli używasz własnych tagów, filtrów albo innych rozszerzeń Latte, musisz utworzyć własny wariant Lintera, na przykład `custom-latte-lint`. W tym skrypcie rejestrujesz wszystkie potrzebne rozszerzenia, zanim dojdzie do właściwej walidacji szablonów:

```php
#!/usr/bin/env php
<?php

// podaj rzeczywistą ścieżkę do pliku autoload.php
require __DIR__ . '/vendor/autoload.php';

$path = $argv[1] ?? '.';

$linter = new Latte\Tools\Linter;
$latte = $linter->getEngine();
// tutaj dodaj swoje własne rozszerzenia
$latte->addExtension(/* ... */);

$ok = $linter->scanDirectory($path);
exit($ok ? 0 : 1);
```

Alternatywnie możesz przekazać Linterowi własny obiekt `Latte\Engine`:

```php
$latte = new Latte\Engine;
// tutaj konfigurujemy obiekt $latte
$linter = new Latte\Tools\Linter(engine: $latte);
```

Powstałego, dostosowanego lintera można potem używać tak samo jak standardowego narzędzia, ale z pełną znajomością wszystkich Twoich własnych rozszerzeń.


Wczytywanie szablonów z łańcucha
================================

Potrzebujesz wczytywać szablony z łańcuchów zamiast z plików, choćby do celów testowych? Pomoże Ci [StringLoader |loaders#StringLoader]:

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

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


Handler wyjątków
================

Możesz zdefiniować własny handler dla oczekiwanych wyjątków. Trafiają do niego wyjątki zgłoszone wewnątrz [`{try}` |tags#{try}] oraz w [sandboxie|sandbox].

```php
$loggingHandler = function (Throwable $e, Latte\Runtime\Template $template) use ($logger) {
	$logger->log($e);
};

$latte = new Latte\Engine;
$latte->setExceptionHandler($loggingHandler);
```


Automatyczne wyszukiwanie layoutu
=================================

Za pomocą tagu [`{layout}` |template-inheritance#Dziedziczenie layoutu] szablon określa swój szablon nadrzędny. Można też sprawić, aby layout był wyszukiwany automatycznie, co uprości pisanie szablonów, bo nie będą musiały zawierać tagu `{layout}`.

Osiąga się to tak:

```php
// zwraca ścieżkę do pliku szablonu nadrzędnego
$finder = fn(Latte\Runtime\Template $template) => 'automatic.layout.latte';
$latte = new Latte\Engine;
$latte->addProvider('coreParentFinder', $finder);
```

Jeśli szablon nie ma mieć layoutu, zasygnalizuje to tagiem `{layout none}`.

Praktyki dla programistów

Instalacja

Najlepszym sposobem instalacji Latte jest Composer:

composer require latte/latte

Obsługiwane wersje PHP (dotyczy najnowszych wersji patch Latte):

wersja zgodna z PHP
Latte 3.1 PHP 8.2 – 8.5
Latte 3.0 PHP 8.0 – 8.5

Jak wyrenderować szablon

Jak wyrenderować szablon? Wystarczy ten prosty kod:

$latte = new Latte\Engine;
// katalog cache
$latte->setCacheDirectory('/path/to/tempdir');

$params = [ /* template variables */ ];
// albo $params = new TemplateParameters(/* ... */);

// renderowanie na wyjście
$latte->render('template.latte', $params);
// albo renderowanie do zmiennej
$output = $latte->renderToString('template.latte', $params);

Parametrami mogą być tablice albo, jeszcze lepiej, obiekt, który zapewni kontrolę typów i podpowiadanie w edytorze.

Przykłady użycia znajdziesz też w repozytorium Latte examples.

Wydajność i cache

Szablony Latte są niezwykle szybkie, bo Latte kompiluje je bezpośrednio do kodu PHP i przechowuje w cache na dysku. Nie mają więc żadnego dodatkowego narzutu w porównaniu z szablonami napisanymi w czystym PHP.

Cache jest automatycznie odświeżany za każdym razem, gdy zmienisz plik źródłowy. Podczas tworzenia możesz więc wygodnie edytować szablony Latte i od razu widzieć zmiany w przeglądarce. W środowisku produkcyjnym tę funkcję można wyłączyć i oszczędzić odrobinę wydajności:

$latte->setAutoRefresh(false);

Po wdrożeniu na serwer produkcyjny wygenerowanie cache po raz pierwszy, zwłaszcza przy większych aplikacjach, może zrozumiale chwilę potrwać. Latte ma wbudowaną ochronę przed cache stampede. To sytuacja, w której serwer otrzymuje dużą liczbę równoczesnych żądań i ponieważ cache Latte jeszcze nie istnieje, wszystkie zaczęłyby generować go naraz. Co powoduje skok obciążenia CPU. Latte jest sprytne i przy wielu równoczesnych żądaniach cache generuje tylko pierwszy wątek, a pozostałe czekają i potem z niego korzystają.

Cache możesz też wygenerować z wyprzedzeniem przy wdrożeniu (na przykład w skrypcie deploya) metodą Engine::warmupCache(). Kompiluje ona podany szablon do cache zawczasu, dzięki czemu pierwszy odwiedzający nie musi czekać: $latte->warmupCache('template.latte').

Sposoby rozszerzania Latte

Latte można dostosować na kilka sposobów, od prostych pomocników po zupełnie nowe konstrukcje językowe. Strona rozszerzanie Latte omawia je szczegółowo; tutaj szybki przegląd:

  • Własne filtry: do formatowania lub przekształcania danych w wyniku szablonu (np. {$var|myFilter}).
  • Własne funkcje: do własnej logiki wywoływanej w wyrażeniach szablonu (np. {myFunction($arg)}).
  • Własne tagi: do zupełnie nowych konstrukcji językowych ({mytag}...{/mytag} albo n:mytag).
  • Compiler passy: funkcje modyfikujące AST szablonu między parsowaniem a wygenerowaniem kodu PHP (na przykład optymalizacje albo kontrole bezpieczeństwa).
  • Własne loadery: do zmiany sposobu, w jaki Latte odnajduje i wczytuje pliki szablonów.

Jeśli chcesz wykorzystywać swoje rozszerzenia w różnych projektach albo udostępnić je innym, spakuj je w klasę Latte Extension.

Parametry jako klasa

Lepiej niż przekazywać zmienne do szablonu jako tablice jest utworzyć klasę. Zyskujesz zapis bezpieczny typowo, wygodne podpowiadanie w IDE i możliwość rejestrowania filtrów oraz funkcji.

class MailTemplateParameters
{
	public function __construct(
		public string $lang,
		public Address $address,
		public string $subject,
		public array $items,
		public ?float $price = null,
	) {}
}

$latte->render('mail.latte', new MailTemplateParameters(
	lang: $this->lang,
	subject: $title,
	price: $this->getPrice(),
	items: [],
	address: $userAddress,
));

Wyłączenie automatycznego escapowania zmiennej

Jeśli zmienna zawiera łańcuch HTML, możesz ją oznaczyć tak, aby Latte nie escapowało jej automatycznie (a więc podwójnie). Unikniesz dzięki temu podawania |noescape w szablonie.

Najprościej opakować łańcuch w obiekt Latte\Runtime\Html:

$params = [
	'articleBody' => new Latte\Runtime\Html($article->htmlBody),
];

Latte nie escapuje też wszystkich obiektów implementujących interfejs Latte\Runtime\HtmlStringable. Możesz więc utworzyć własną klasę, której metoda __toString() zwróci kod HTML, który nie zostanie automatycznie zescapowany:

class Emphasis implements Latte\Runtime\HtmlStringable
{
	public function __construct(
		private string $str,
	) {
	}

	public function __toString(): string
	{
		return '<em>' . htmlspecialchars($this->str) . '</em>';
	}
}

$params = [
	'foo' => new Emphasis('hello'),
];

Metoda __toString musi zwracać poprawny HTML i zapewniać escapowanie parametrów, w przeciwnym razie może powstać podatność XSS!

Jak rozszerzyć Latte o filtry, tagi itd.

Jak dodać do Latte własny filtr, funkcję, tag itd.? Dowiesz się w rozdziale rozszerzanie Latte. Jeśli chcesz wykorzystywać swoje zmiany w różnych projektach albo udostępnić je innym, powinieneś następnie utworzyć rozszerzenie.

Dowolny kod w szablonie {php ...}

Wewnątrz tagu {do} można zapisywać wyłącznie wyrażenia PHP, nie da się więc wstawić na przykład konstrukcji w rodzaju if ... else ani instrukcji zakończonych średnikiem.

Możesz jednak zarejestrować rozszerzenie RawPhpExtension, które dodaje tag {php ...}. Za jego pomocą wstawisz dowolny kod PHP. Nie podlega on żadnym regułom trybu sandbox, więc jego użycie jest na odpowiedzialność autora szablonu.

$latte->addExtension(new Latte\Essential\RawPhpExtension);

Kontrola wygenerowanego kodu

Latte kompiluje szablony do kodu PHP. Oczywiście dba o to, aby wygenerowany kod był poprawny składniowo. Przy używaniu rozszerzeń innych autorów albo RawPhpExtension Latte nie może jednak zagwarantować poprawności wygenerowanego pliku. W PHP da się też napisać kod poprawny składniowo, ale zabroniony (na przykład przypisanie wartości do zmiennej $this), powodujący PHP Compile Error. Jeśli zapiszesz taką operację w szablonie, trafi ona również do wygenerowanego kodu PHP. Ponieważ różnych zabronionych operacji jest w PHP ponad dwieście, Latte nie stawia sobie za cel ich wykrywania. Zgłosi je samo PHP przy renderowaniu, co zwykle nie stanowi problemu.

Bywają jednak sytuacje, w których chcesz już przy kompilacji szablonu wiedzieć, że nie zawiera on żadnych PHP Compile Error. Zwłaszcza gdy szablony mogą edytować użytkownicy albo gdy używasz Sandboxa. W takim przypadku każ sprawdzać szablony podczas kompilacji. Tę funkcjonalność włączysz metodą Engine::enablePhpLinter(). Ponieważ do sprawdzenia musi wywołać binarkę PHP, przekaż jej ścieżkę jako parametr:

$latte = new Latte\Engine;
$latte->enablePhpLinter('/path/to/php');

try {
	$latte->compile('home.latte');
} catch (Latte\CompileException $e) {
	// wyłapuje błędy Latte, a także Compile Error w PHP
	echo 'Error: ' . $e->getMessage();
}

Locale

Latte pozwala ustawić locale, które wpływa na formatowanie liczb, dat i sortowanie. Ustawia się je metodą setLocale(). Identyfikator locale jest zgodny ze standardem IETF language tag, którego używa rozszerzenie PHP intl. Składa się z kodu języka i ewentualnie kodu kraju, na przykład en_US dla angielskiego w Stanach Zjednoczonych, de_DE dla niemieckiego w Niemczech itd.

$latte = new Latte\Engine;
$latte->setLocale('en_US');

Ustawienie locale wpływa na filtry localDate, sort, numberbytes.

Wymaga rozszerzenia PHP intl. Ustawienie w Latte nie wpływa na globalne ustawienie locale w PHP.

Tryb ścisły

W trybie ścisłego parsowania Latte sprawdza brakujące zamykające tagi HTML, a także wyłącza możliwość używania zmiennej $this. Aby go włączyć:

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::StrictParsing);

Aby generować szablony z nagłówkiem declare(strict_types=1), zrób tak:

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::StrictTypes);

Od Latte 3.1 ścisłe typy są włączone domyślnie. Możesz je wyłączyć przez $latte->setFeature(Latte\Feature::StrictTypes, false).

Ostrzeżenia migracyjne

Latte 3.1 zmienia zachowanie niektórych atrybutów HTML. Na przykład wartości null teraz usuwają atrybut zamiast wypisywać pusty łańcuch. Aby łatwo znaleźć miejsca, w których ta zmiana dotyka Twoich szablonów, możesz włączyć ostrzeżenia migracyjne:

$latte->setFeature(Latte\Feature::MigrationWarnings);

Po włączeniu Latte sprawdza renderowane atrybuty i zgłasza ostrzeżenie użytkownika (E_USER_WARNING), jeśli wynik różni się od tego, co wyprodukowałoby Latte 3.0. Gdy natkniesz się na ostrzeżenie, zastosuj jedno z rozwiązań:

  1. Jeśli nowy wynik jest w Twoim przypadku poprawny (np. wolisz, aby atrybut przy null znikał), wycisz ostrzeżenie, dodając filtr |accept
  2. Jeśli chcesz, aby atrybut renderował się jako pusty (np. title=""), zamiast znikać, gdy zmienna ma wartość null, podaj pusty łańcuch jako wartość awaryjną: title={$val ?? ''}
  3. Jeśli koniecznie potrzebujesz starego zachowania (np. wypisywania "1" dla true zamiast "true"), rzutuj wartość jawnie na łańcuch: data-foo={(string) $val}

Po rozwiązaniu wszystkich ostrzeżeń wyłącz ostrzeżenia migracyjne i usuń wszystkie filtry |accept z szablonów, bo nie są już potrzebne.

Zmienne pętli w zasięgu lokalnym

Domyślnie zmienne zdefiniowane w pętli {foreach} (jak $key i $value) pozostają dostępne po jej zakończeniu – tak samo jak w samym PHP. Może to prowadzić do niezamierzonego nadpisania zmiennych, gdy zmienna pętli ma taką samą nazwę jak istniejąca zmienna szablonu.

Funkcja ScopedLoopVariables ogranicza zasięg zmiennych pętli do jej ciała. Po zakończeniu pętli pierwotna wartość zmiennej zostaje przywrócona (jeśli istniała wcześniej), a w przeciwnym razie zmienna zostaje usunięta:

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::ScopedLoopVariables);

Przykład różnicy:

{var $item = 'original'}
{foreach [1, 2] as $item}{$item}, {/foreach}
{$item}

Bez ScopedLoopVariables: wypisze 1, 2, 2 (zmienna zostaje nadpisana) Z ScopedLoopVariables: wypisze 1, 2, original (zmienna zostaje przywrócona)

Działa to również przy składni z destrukturyzacją, np. {foreach $array as [$a, $b]}.

Zmienne pętli używające referencji ({foreach $array as &$value}) albo przypisania do właściwości ({foreach $array as $obj->prop}) nie są ograniczane zasięgiem, bo zniweczyłoby to ich zamierzone działanie.

Automatyczne usuwanie wcięć

Przy używaniu tagów parzystych, takich jak {if}, {foreach} czy {block}, często wcinasz zagnieżdżoną treść dla czytelności. Domyślnie to wcięcie trafia jednak do wygenerowanego wyniku. Funkcja Dedent automatycznie je usuwa, dzięki czemu wynik pozostaje czysty niezależnie od tego, jak głęboko zagnieżdżasz tagi Latte:

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::Dedent);

Przykład:

{if true}
	Hello
	World
{/if}

Bez Dedent wynik zawierałby wcięcia (\tHello\n\tWorld\n). Z Dedent wcięcia są usuwane, a wynikiem jest Hello\nWorld\n.

Głębsze wcięcie wewnątrz bloku jest zachowywane względem wcięcia bazowego:

{if true}
	Hello
		Indented
{/if}

Wynik: Hello\n\tIndented\n.

Wcięcia wewnątrz bloku muszą być spójne (albo tabulatory, albo spacje). Jeśli zostaną wymieszane, Latte zgłosi wyjątek Inconsistent indentation.

Tłumaczenie w szablonach

Aby dodać do szablonu {_...}, {translate} i filtr translate, użyj rozszerzenia TranslatorExtension. Służą one do tłumaczenia wartości lub części szablonu na inne języki. Parametrem jest callable wykonujący tłumaczenie albo obiekt typu Nette\Localization\Translator (podaj null, aby wyłączyć tłumaczenia):

class MyTranslator
{
	public function __construct(private string $lang)
	{}

	public function translate(string $original): string
	{
		// tworzymy $translated z $original zgodnie z $this->lang
		return $translated;
	}
}

$translator = new MyTranslator($lang);
$extension = new Latte\Essential\TranslatorExtension(
	$translator->translate(...), // [$translator, 'translate'] w PHP 8.0
);
$latte->addExtension($extension);

Translator jest wywoływany w czasie działania, przy renderowaniu szablonu. Latte potrafi jednak przetłumaczyć wszystkie teksty statyczne już podczas kompilacji szablonu. Oszczędza to wydajność, bo każdy łańcuch tłumaczony jest tylko raz, a powstałe tłumaczenie zapisywane jest do skompilowanego pliku. W katalogu cache powstaje wtedy kilka skompilowanych wersji szablonu, po jednej na język. Wystarczy do tego podać język jako drugi parametr:

$extension = new Latte\Essential\TranslatorExtension(
	$translator->translate(...),
	$lang,
);

Przez tekst statyczny rozumiemy na przykład {_'hello'} albo {translate}hello{/translate}. Teksty niestatyczne, takie jak {_$foo}, nadal będą tłumaczone w czasie działania.

Szablon może też przekazać translatorowi dodatkowe parametry przez {_$original, foo: bar} albo {translate foo: bar}, które otrzyma on jako tablicę $params:

public function translate(string $original, ...$params): string
{
	// $params['foo'] === 'bar'
}

Debugowanie i Tracy

Latte stara się, aby tworzenie aplikacji było jak najprzyjemniejsze. Do celów debugowania służą trzy tagi: {dump}, {debugbreak} i {trace}.

Największy komfort uzyskasz, instalując świetne narzędzie do debugowania Tracy i aktywując plugin do Latte:

// włącza Tracy
Tracy\Debugger::enable();

$latte = new Latte\Engine;
// aktywuje rozszerzenie Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);

Wszystkie błędy zobaczysz teraz na zgrabnym czerwonym ekranie, w tym błędy w szablonach z podświetleniem wiersza i kolumny (wideo). Jednocześnie w prawym dolnym rogu, w tzw. pasku Tracy, pojawi się zakładka Latte, gdzie przejrzyście zobaczysz wszystkie wyrenderowane szablony i ich zależności (łącznie z możliwością kliknięcia w szablon albo skompilowany kod), a także zmienne:

Ponieważ Latte kompiluje szablony do czytelnego kodu PHP, możesz wygodnie krokować po nich w swoim IDE.

Linter: walidacja składni szablonu

Narzędzie Linter służy do walidacji wszystkich szablonów. Jego celem jest przeskanowanie wskazanych plików i upewnienie się, że nie zawierają błędów składniowych ani odwołań do nieistniejących tagów, filtrów, funkcji, klas czy podobnych konstrukcji.

Linter uruchamia się z wiersza poleceń:

vendor/bin/latte-lint <path>

Parametrem --strict włączysz tryb ścisły. Parametr --debug wypisuje nazwę każdego przetwarzanego pliku i pełne szczegóły wyjątku, co pomaga przy szukaniu problemów.

Jeśli używasz własnych tagów, filtrów albo innych rozszerzeń Latte, musisz utworzyć własny wariant Lintera, na przykład custom-latte-lint. W tym skrypcie rejestrujesz wszystkie potrzebne rozszerzenia, zanim dojdzie do właściwej walidacji szablonów:

#!/usr/bin/env php
<?php

// podaj rzeczywistą ścieżkę do pliku autoload.php
require __DIR__ . '/vendor/autoload.php';

$path = $argv[1] ?? '.';

$linter = new Latte\Tools\Linter;
$latte = $linter->getEngine();
// tutaj dodaj swoje własne rozszerzenia
$latte->addExtension(/* ... */);

$ok = $linter->scanDirectory($path);
exit($ok ? 0 : 1);

Alternatywnie możesz przekazać Linterowi własny obiekt Latte\Engine:

$latte = new Latte\Engine;
// tutaj konfigurujemy obiekt $latte
$linter = new Latte\Tools\Linter(engine: $latte);

Powstałego, dostosowanego lintera można potem używać tak samo jak standardowego narzędzia, ale z pełną znajomością wszystkich Twoich własnych rozszerzeń.

Wczytywanie szablonów z łańcucha

Potrzebujesz wczytywać szablony z łańcuchów zamiast z plików, choćby do celów testowych? Pomoże Ci StringLoader:

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

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

Handler wyjątków

Możesz zdefiniować własny handler dla oczekiwanych wyjątków. Trafiają do niego wyjątki zgłoszone wewnątrz {try} oraz w sandboxie.

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

$latte = new Latte\Engine;
$latte->setExceptionHandler($loggingHandler);

Automatyczne wyszukiwanie layoutu

Za pomocą tagu {layout} szablon określa swój szablon nadrzędny. Można też sprawić, aby layout był wyszukiwany automatycznie, co uprości pisanie szablonów, bo nie będą musiały zawierać tagu {layout}.

Osiąga się to tak:

// zwraca ścieżkę do pliku szablonu nadrzędnego
$finder = fn(Latte\Runtime\Template $template) => 'automatic.layout.latte';
$latte = new Latte\Engine;
$latte->addProvider('coreParentFinder', $finder);

Jeśli szablon nie ma mieć layoutu, zasygnalizuje to tagiem {layout none}.