Szablony

Nette używa systemu szablonów Latte. Po pierwsze dlatego, że jest to najlepiej zabezpieczony system szablonów dla PHP, a jednocześnie system najbardziej intuicyjny. Nie musisz uczyć się wielu nowych rzeczy, wystarczy znajomość PHP i kilku znaczników.

Jest typowe, że strona składa się z szablonu layoutu + szablonu danej akcji. Tak na przykład może wyglądać szablon layoutu, zwróć uwagę na bloki {block} i znacznik {include}:

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

A to będzie szablon akcji:

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

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

Definiuje on blok content, który zostanie wstawiony w miejsce {include content} w layoucie, a także redefiniuje blok title, którym nadpisze {block title} w layoucie. Spróbuj sobie wyobrazić wynik.

Wyszukiwanie szablonów

Nie musisz w presenterach podawać, jaki szablon ma być wyrenderowany, framework sam wywnioskuje ścieżkę i oszczędzi Ci pisania.

Jeśli używasz struktury katalogów, gdzie każdy presenter ma własny katalog, po prostu umieść szablon w tym katalogu pod nazwą akcji (resp. view), tj. dla akcji default użyj szablonu default.latte:

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

Jeśli używasz struktury, gdzie presentery są razem w jednym katalogu, a szablony w folderze templates, zapisz go albo w pliku <Presenter>.<view>.latte albo <Presenter>/<view>.latte:

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

Katalog templates może być umieszczony również o poziom wyżej, tj. na tym samym poziomie, co katalog z klasami presenterów.

Jeśli szablon nie zostanie znaleziony, presenter odpowie błędem 404 – page not found.

View zmienisz za pomocą $this->setView('innyView'). Można również bezpośrednio określić plik z szablonem za pomocą $this->template->setFile('/path/to/template.latte').

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

Wyszukiwanie szablonu layoutu

Nette również automatycznie wyszukuje plik z layoutem.

Jeśli używasz struktury katalogów, gdzie każdy presenter ma własny katalog, umieść layout albo w folderze z presenterem, jeśli jest specyficzny tylko dla niego, albo o poziom wyżej, jeśli jest wspólny dla wielu 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, gdzie presentery są razem 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 znajduje się w module, będzie wyszukiwany również o kolejne poziomy katalogów wyżej, zgodnie z zagnieżdżeniem modułu.

Nazwę layoutu można zmienić za pomocą $this->setLayout('layoutAdmin'), a wtedy będzie oczekiwany w pliku @layoutAdmin.latte. Można również bezpośrednio określić plik z szablonem layoutu za pomocą $this->setLayout('/path/to/template.latte').

Za pomocą $this->setLayout(false) lub znacznika {layout none} wewnątrz szablonu wyszukiwanie layoutu zostanie wyłączone.

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

Zmienne w szablonie

Zmienne do szablonu przekazujemy tak, że zapisujemy je do $this->template, a potem mamy je dostępne w szablonie jako zmienne lokalne:

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

W ten prosty sposób możemy przekazać do szablonów dowolne zmienne. Jednak przy tworzeniu solidnych aplikacji bywa bardziej użyteczne ograniczenie się. Na przykład tak, że jawnie zdefiniujemy wykaz zmiennych, których oczekuje szablon, oraz ich typów. Dzięki temu PHP będzie mógł kontrolować typy, IDE poprawnie podpowiadać, a analiza statyczna wykrywać błędy.

A jak taki wykaz zdefiniujemy? Po prostu w postaci klasy i jej właściwości. Nazwiemy 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. Więc PHP podczas zapisu będzie kontrolował zadeklarowane typy. A począwszy od wersji PHP 8.2 powiadomi również o zapisie do nieistniejącej zmiennej, w poprzednich wersjach tego samego można osiągnąć używając traity Nette\SmartObject.

Adnotacja @property-read jest przeznaczona dla IDE i analizy statycznej, dzięki niej będzie działać podpowiadanie, zobacz PhpStorm and code completion for $this⁠-⁠>⁠template.

Luksusu podpowiadania możesz sobie pozwolić również w szablonach, wystarczy zainstalować w PhpStorm wtyczkę dla Latte i podać na początku szablonu nazwę klasy, więcej w artykule Latte: jak na typowy system:

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

Tak działają również szablony w komponentach, wystarczy tylko przestrzegać konwencji nazewnictwa i dla komponentu np. FifteenControl utworzyć klasę szablonu FifteenTemplate.

Jeśli potrzebujesz utworzyć $template jako instancję innej klasy, wykorzystaj metodę createTemplate():

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

Zmienne domyślne

Presentery i komponenty przekazują do szablonów kilka użytecznych zmiennych automatycznie:

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

Jeśli używasz własnej klasy szablonu, te zmienne zostaną przekazane, jeśli utworzysz dla nich właściwość.

Tworzenie linków

W szablonie tworzy się linki do innych presenterów & akcji w ten sposób:

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

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

Adres to: {link Home:default}

Więcej informacji znajdziesz w rozdziale Tworzenie linków URL.

Własne filtry, znaczniki itp.

System szablonów Latte można rozszerzyć o własne filtry, funkcje, znaczniki itp. Można to zrobić bezpośrednio w metodzie render<View> lub beforeRender():

public function beforeRender(): void
{
	// dodanie filtra
	$this->template->addFilter('foo', /* ... */);

	// lub konfigurujemy bezpośrednio obiekt Latte\Engine
	$latte = $this->template->getLatte();
	$latte->addFilterLoader(/* ... */);
}

Latte w wersji 3 oferuje bardziej zaawansowany sposób, a mianowicie utworzenie sobie extension dla każdego projektu internetowego. Przykładowy fragment takiej klasy:

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()),
			// ...
		];
	}

	// ...
}

Zarejestrujemy ją za pomocą konfiguracji:

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

Tłumaczenie

Jeśli programujesz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować niektóre teksty w szablonie wypisać w różnych językach. Nette Framework w tym celu definiuje interfejs do tłumaczenia Nette\Localization\Translator, który ma jedyną metodę translate(). Przyjmuje ona wiadomość $message, co zazwyczaj jest ciągiem znaków, oraz dowolne inne parametry. Zadaniem jest zwrócenie przetłumaczonego ciągu. W Nette nie ma żadnej domyślnej implementacji, możesz wybrać według swoich potrzeb spośród kilku gotowych rozwiązań, które znajdziesz na Componette. W ich dokumentacji dowiesz się, jak konfigurować translator.

Szablonom można ustawić translator, który sobie przekażemy, metodą setTranslator():

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

Translator alternatywnie można ustawić za pomocą konfiguracji:

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

Następnie można używać translatora na przykład jako filtra |translate, w tym z dodatkowymi parametrami, które zostaną przekazane metodzie translate() (zobacz foo, bar):

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

Lub jako znacznika z podkreśleniem:

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

Do tłumaczenia fragmentu szablonu istnieje parzysty znacznik {translate} (od Latte 2.11, wcześniej używano znacznika {_}):

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

Translator standardowo jest wywoływany w czasie rzeczywistym podczas renderowania szablonu. Latte w wersji 3 jednak potrafi wszystkie statyczne teksty tłumaczyć już podczas kompilacji szablonu. Tym samym oszczędza się wydajność, ponieważ każdy ciąg jest tłumaczony tylko raz, a wynikowe tłumaczenie jest zapisywane w skompilowanej postaci. W katalogu z cache powstaje więc więcej skompilowanych wersji szablonu, jedna dla każdego języka. Do tego wystarczy tylko podać język jako drugi parametr:

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

Tekstem statycznym jest na przykład {_'hello'} lub {translate}hello{/translate}. Teksty niestatyczne, jak na przykład {_$foo}, nadal będą tłumaczone w czasie rzeczywistym.