Szablony

Nette używa systemu szablonów Latte. Latte jest używane, bo to najbezpieczniejszy system szablonów dla PHP, a zarazem najbardziej intuicyjny. Nie musisz uczyć się wiele nowego; wystarczy znajomość PHP i kilku tagów.

Powszechnie strona składa się z szablonu layoutu i szablonu konkretnej akcji. Tak może wyglądać szablon layoutu; zwróć uwagę na bloki {block} i tag {include}:

<!DOCTYPE html>
<html>
<head>
	<title>{block title}Moja aplikacja{/block}</title>
</head>
<body>
	<header>...</header>
	{include content}
	<footer>...</footer>
</body>
</html>

A tak wyglądałby szablon akcji:

{block title}Strona główna{/block}

{block content}
<h1>Strona główna</h1>
...
{/block}

Definiuje on blok content, który wstawiany jest w layoucie w miejsce {include content}, a także na nowo definiuje blok title, który nadpisuje {block title} w layoucie. Spróbuj wyobrazić sobie wynik.

Wyszukiwanie szablonów

W presenterach nie musisz podawać, który szablon ma zostać wyrenderowany; framework sam wywnioskuje ścieżkę, oszczędzając Ci pisania.

Jeśli używasz struktury katalogów, w której każdy presenter ma własny katalog, po prostu umieść szablon w tym katalogu pod nazwą akcji (czyli widoku). Na przykład dla akcji default użyj szablonu default.latte:

app/
└── Presentation/
    └── Home/
        ├── HomePresenter.php
        └── default.latte

Jeśli używasz struktury, w której presentery leżą razem w jednym katalogu, a szablony w folderze templates, zapisz go albo w pliku <Presenter>.<widok>.latte, albo <Presenter>/<widok>.latte:

app/
└── Presenters/
    ├── HomePresenter.php
    └── templates/
        ├── Home/
        │   └── default.latte   ← 1. wariant
        └── Home.default.latte  ← 2. wariant

Katalog templates może leżeć też o poziom wyżej, czyli na tym samym poziomie co katalog z klasami presenterów.

Jeśli szablon się nie znajdzie, presenter odpowiada błędem 404 – strona nie znaleziona.

Widok możesz zmienić przez $this->setView('otherView'). Można też bezpośrednio wskazać plik szablonu przez $this->template->setFile('/path/to/template.latte').

Pliki, w których wyszukiwane są szablony, można zmienić, nadpisując metodę formatTemplateFiles(), która zwraca tablicę możliwych nazw plików.

Wyszukiwanie szablonu layoutu

Nette automatycznie wyszukuje również plik layoutu.

Jeśli używasz struktury katalogów, w której każdy presenter ma własny katalog, umieść layout albo w folderze z presenterem, jeśli jest przeznaczony tylko dla niego, albo o poziom wyżej, jeśli jest wspólny dla kilku presenterów:

app/
└── Presentation/
    ├── @layout.latte           ← wspólny layout
    └── Home/
        ├── @layout.latte       ← tylko dla presentera Home
        ├── HomePresenter.php
        └── default.latte

Jeśli używasz struktury, w której presentery zgrupowane są w jednym katalogu, a szablony w folderze templates, layout będzie oczekiwany w tych miejscach:

app/
└── Presenters/
    ├── HomePresenter.php
    └── templates/
        ├── @layout.latte       ← wspólny layout
        ├── Home/
        │   └── @layout.latte   ← tylko dla Home, 1. wariant
        └── Home.@layout.latte  ← tylko dla Home, 2. wariant

Jeśli presenter leży w module, wyszukiwanie postępuje też wyżej po poziomach katalogów, zgodnie z zagnieżdżeniem modułów.

Nazwę layoutu można zmienić przez $this->setLayout('layoutAdmin'), a wtedy będzie oczekiwany w pliku @layoutAdmin.latte. Możesz też bezpośrednio wskazać plik szablonu layoutu przez $this->setLayout('/path/to/template.latte').

Użycie $this->setLayout(false) albo tagu {layout none} wewnątrz szablonu wyłącza wyszukiwanie layoutu.

Pliki, w których wyszukiwane są szablony layoutu, można zmienić, nadpisując metodę formatLayoutTemplateFiles(), która zwraca tablicę możliwych nazw plików.

Zmienne szablonu

Zmienne przekazuje się do szablonów, zapisując je do $this->template. Stają się wtedy dostępne w szablonie jako zmienne lokalne:

$this->template->article = $this->articles->getById($id);

Aby wartość właściwości była automatycznie przekazywana do szablonu jako zmienna, oznacz ją atrybutem #[TemplateVariable] i widocznością public:

use Nette\Application\Attributes\TemplateVariable;

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	#[TemplateVariable]
	public string $siteName = 'Mój blog';
}

Jeśli przekażesz do szablonu zmienną o tej samej nazwie, #[TemplateVariable] jej nie nadpisze.

Zmienne domyślne

Presentery i komponenty automatycznie przekazują do szablonów kilka przydatnych zmiennych:

  • $basePath to bezwzględna ścieżka URL do katalogu głównego (np. /eshop)
  • $baseUrl to bezwzględny URL katalogu głównego (np. http://localhost/eshop)
  • $user to obiekt reprezentujący użytkownika
  • $presenter to bieżący presenter
  • $control to bieżący komponent albo presenter
  • $flashes to tablica wiadomości wysłanych funkcją flashMessage()

Jeśli używasz własnej klasy szablonu, zmienne te są przekazywane, o ile utworzysz dla nich właściwość.

Szablony bezpieczne typowo

Przy tworzeniu solidnych aplikacji przydaje się jawne określenie, jakich zmiennych oczekuje szablon i jakich są typów. Daje to kontrolę typów w PHP, inteligentne podpowiedzi w IDE i pozwala analizie statycznej wyłapywać błędy.

Jak zdefiniować taką listę? Po prostu jako klasę z właściwościami reprezentującymi zmienne szablonu. Nazwij ją podobnie jak presenter, tylko z Template na końcu:

/**
 * @property-read ArticleTemplate $template
 */
class ArticlePresenter extends Nette\Application\UI\Presenter
{
}

class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template
{
	public Model\Article $article;
	public Nette\Security\User $user;

	// i inne zmienne
}

Obiekt $this->template w presenterze będzie teraz instancją klasy ArticleTemplate. PHP będzie więc przy zapisie sprawdzać zadeklarowane typy.

Nette wybiera klasę szablonu automatycznie. Najpierw szuka klasy o nazwie <Presenter><Akcja>Template, np. ArticleEditTemplate dla akcji edit, i dopiero gdy jej nie ma, sięga po <Presenter>Template.

Adnotacja @property-read przeznaczona jest dla IDE i analizy statycznej, umożliwia uzupełnianie kodu, zobacz PhpStorm i uzupełnianie kodu dla $this⁠-⁠>⁠template.

Uzupełniania kodu możesz używać również bezpośrednio w szablonach. Wystarczy zainstalować plugin Latte do PhpStorm i podać na początku szablonu nazwę klasy parametrów szablonu, więcej w rozdziale Latte: system typów:

{templateType App\Presentation\Article\ArticleTemplate}
...

To samo dotyczy komponentów. Wystarczy trzymać się konwencji nazewniczej i dla komponentu w rodzaju FifteenControl utworzyć klasę parametrów FifteenTemplate.

Jeśli potrzebujesz użyć innej klasy parametrów, skorzystaj z metody createTemplate():

public function renderDefault(): void
{
	$template = $this->createTemplate(SpecialTemplate::class);
	$template->foo = 123;
	// ...
	$this->sendTemplate($template);
}

Jeśli potrzebujesz wpłynąć na to, jak szablon jest finalizowany przed renderowaniem, na przykład aby dodać zmienne wspólne dla wszystkich akcji, możesz nadpisać w presenterze metodę completeTemplate(). Wywoływana jest tuż przed wyrenderowaniem szablonu:

protected function completeTemplate(Nette\Application\UI\Template $template): void
{
	parent::completeTemplate($template);
	$template->siteName = 'Mój blog';
}

Tworzenie odnośników

W szablonie odnośniki do innych presenterów i akcji tworzy się tak:

<a n:href="Product:show">szczegóły produktu</a>

Atrybut n:href jest bardzo poręczny dla tagów HTML <a>. Jeśli chcemy wypisać odnośnik gdzie indziej, na przykład w tekście, użyjemy {link}:

URL to: {link Home:default}

Więcej informacji znajdziesz w rozdziale Tworzenie odnośników URL.

Własne filtry, tagi itd.

System szablonów Latte można rozszerzać o własne filtry, funkcje, tagi i inne elementy. Dostępne są trzy podejścia, od szybkich rozwiązań doraźnych po wzorce architektoniczne dla całych aplikacji.

Doraźnie w metodach presentera

Najszybszym podejściem jest dodawanie filtrów albo funkcji bezpośrednio w kodzie presentera lub komponentu. W presenterach dobrze nadają się do tego metody beforeRender() albo render<Widok>():

protected function beforeRender(): void
{
	// dodanie filtra
	$this->template->addFilter('money', fn($val) => '$' . number_format($val, 2));

	// dodanie funkcji
	$this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6);
}

W szablonie:

<p>Cena: {$price|money}</p>

{if isWeekend($now)} ... {/if}

Przy bardziej złożonej logice możesz skonfigurować bezpośrednio obiekt Latte\Engine:

protected function beforeRender(): void
{
	$latte = $this->template->getLatte();
	$latte->setFeature(Latte\Feature::MigrationWarnings);
}

Za pomocą atrybutów

Eleganckim podejściem jest zdefiniowanie filtrów i funkcji jako metod bezpośrednio w klasie parametrów szablonu presentera albo komponentu, oznaczonych atrybutami:

class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template
{
	#[Latte\Attributes\TemplateFilter]
	public function money(float $val): string
	{
		return '$' . number_format($val, 2);
	}

	#[Latte\Attributes\TemplateFunction]
	public function isWeekend(DateTimeInterface $date): bool
	{
		return $date->format('N') >= 6;
	}
}

Latte automatycznie odnajduje i rejestruje metody oznaczone tymi atrybutami. Nazwa filtra albo funkcji w szablonach odpowiada nazwie metody. Metody te muszą być publiczne.

Globalnie za pomocą rozszerzeń

Poprzednie podejścia nadają się do filtrów i funkcji potrzebnych tylko w konkretnych presenterach albo komponentach, a nie w całej aplikacji. Dla całej aplikacji najlepiej sprawdza się utworzenie rozszerzenia. Klasa ta centralizuje wszystkie rozszerzenia Latte w Twoim projekcie. Krótki przykład:

namespace App\Presentation\Accessory;

final class LatteExtension extends Latte\Extension
{
	public function __construct(
		private App\Model\Facade $facade,
		private Nette\Security\User $user,
		// ...
	) {
	}

	public function getFilters(): array
	{
		return [
			'timeAgoInWords' => $this->filterTimeAgoInWords(...),
			'money' => $this->filterMoney(...),
			// ...
		];
	}

	public function getFunctions(): array
	{
		return [
			'canEditArticle' =>
				fn($article) => $this->facade->canEditArticle($article, $this->user->getId()),
			// ...
		];
	}

	private function filterTimeAgoInWords(DateTimeInterface $time): string
	{
		// ...
	}

	// ...
}

Rozszerzenie zarejestrujesz przez konfigurację:

latte:
	extensions:
		- App\Presentation\Accessory\LatteExtension

Rozszerzenia dają kilka korzyści: wsparcie dla wstrzykiwania zależności, dostęp do warstwy modelu Twojej aplikacji i centralne zarządzanie wszystkimi rozszerzeniami. Obsługują też własne tagi, providery, compiler passy i inne rzeczy.

Ustawienie wszystkich szablonów

Usługa TemplateFactory, która tworzy wszystkie szablony, oferuje publiczną tablicę callbacków $onCreate. Wywoływane są one przy każdym utworzeniu dowolnego szablonu, dzięki czemu z jednego miejsca ustawisz filtry, funkcje albo zmienne dla wszystkich szablonów w aplikacji. Każdy callback otrzymuje nowo utworzony szablon. Każ sobie wstrzyknąć usługę TemplateFactory i zarejestruj callbacki, np. przy starcie aplikacji:

$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void {
	$template->addFilter('money', fn($val) => '$' . number_format($val, 2));
};

Tłumaczenie

Jeśli programujesz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wypisywać w szablonie niektóre teksty w różnych językach. Nette Framework definiuje w tym celu interfejs tłumaczenia Nette\Localization\Translator, który ma jedną metodę translate(). Przyjmuje ona komunikat $message, którym zwykle jest string, oraz dowolne inne parametry. Zadaniem jest zwrócenie przetłumaczonego stringa. Nette nie ma domyślnej implementacji; możesz wybrać z kilku gotowych rozwiązań dostępnych na Componette według swoich potrzeb. Ich dokumentacja wyjaśnia, jak skonfigurować translator.

Szablonom można ustawić translator, który otrzymamy wstrzyknięty, metodą setTranslator():

protected function beforeRender(): void
{
	// ...
	$this->template->setTranslator($translator);
}

Alternatywnie translator można ustawić przez konfigurację:

latte:
	extensions:
		- Latte\Essential\TranslatorExtension(@Nette\Localization\Translator)

Translatora można potem używać na przykład jako filtra |translate, wraz z dodatkowymi parametrami przekazywanymi do metody translate() (zobacz foo, bar):

<a href="basket">{='Koszyk'|translate}</a>
<span>{$item|translate}</span>
<span>{$item|translate, foo, bar}</span>

Albo jako tagu z podkreśleniem:

<a href="basket">{_'Koszyk'}</a>
<span>{_$item}</span>
<span>{_$item, foo, bar}</span>

Do przetłumaczenia fragmentu szablonu służy tag parzysty {translate} (od Latte 2.11, wcześniej używano tagu {_}):

<a href="order">{translate}Zamów{/translate}</a>
<a href="order">{translate foo, bar}Zamów{/translate}</a>

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

protected function beforeRender(): void
{
	// ...
	$this->template->setTranslator($translator, $lang);
}

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