Szablony

Nette wykorzystuje system szablonów Latte. Po pierwsze dlatego, że jest to najbezpieczniejszy system szablonowania dla PHP, a także najbardziej intuicyjny. Nie musisz uczyć się wielu nowych rzeczy, wystarczy znajomość PHP i kilka tagów.

Często zdarza się, że strona składa się z szablonu układu + szablonu akcji. Na przykład tak może wyglądać szablon układu, zauważ bloki {block} i znacznik {include}:

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

I to będzie szablon akcji:

{block title}Homepage{/block}

{block content}
<h1>Homepage</h1>
...
{/block}

Definiuje blok content, który zostanie wstawiony w miejsce {include content} w układzie, a także redefiniuje blok title, który zastąpi {block title} w układzie. Spróbujcie sobie wyobrazić ten rezultat.

Wyszukiwanie szablonów

W prezenterach nie trzeba określać, który szablon ma być renderowany; framework automatycznie określi ścieżkę, ułatwiając kodowanie.

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

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

Jeśli używasz struktury, w której prezenterzy znajdują się w jednym katalogu, a szablony w folderze templates, zapisz je w pliku <Presenter>.<view>.latte lub <Presenter>/<view>.latte:

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

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

Jeśli szablon nie zostanie znaleziony, prezenter odpowie błędem 404 – nie znaleziono strony.

Widok można zmienić za pomocą $this->setView('anotherView'). Możliwe jest również bezpośrednie określenie pliku szablonu za pomocą $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 układu

Nette automatycznie wyszukuje również plik układu.

Jeśli używasz struktury katalogów, w której każdy prezenter ma swój własny katalog, umieść układ w folderze z prezenterem, jeśli jest on specyficzny tylko dla niego, lub poziom wyżej, jeśli jest wspólny dla wielu prezenterów:

app/
└── UI/
    ├── @layout.latte           ← common layout
    └── Home/
        ├── @layout.latte       ← only for Home presenter
        ├── HomePresenter.php
        └── default.latte

Jeśli używasz struktury, w której prezenterzy są zgrupowani w jednym katalogu, a szablony znajdują się w folderze templates, layout będzie oczekiwany w następujących miejscach:

app/
└── Presenters/
    ├── HomePresenter.php
    └── templates/
        ├── @layout.latte       ← common layout
        ├── Home.@layout.latte  ← only for Home, 1st variant
        └── Home/
            └── @layout.latte   ← only for Home, 2nd variant

Jeśli prezenter znajduje się w module, będzie on również wyszukiwany dalej w drzewie katalogów zgodnie z zagnieżdżeniem modułu.

Nazwę layoutu można zmienić za pomocą $this->setLayout('layoutAdmin'), a następnie będzie ona oczekiwana w pliku @layoutAdmin.latte. Można również bezpośrednio określić plik szablonu układu za pomocą $this->setLayout('/path/to/template.latte').

Użycie $this->setLayout(false) lub znacznika {layout none} wewnątrz szablonu wyłącza wyszukiwanie układu.

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

Zmienne w szablonie

Zmienne są przekazywane do szablonu poprzez zapisanie ich do $this->template, a następnie są dostępne w szablonie jako zmienne lokalne:

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

W ten sposób możemy łatwo przekazać dowolne zmienne do szablonów. Jednak podczas tworzenia solidnych aplikacji często bardziej przydatne jest ograniczenie się. Na przykład poprzez jawne zdefiniowanie listy zmiennych, których oczekuje szablon i ich typów. Pozwoli to PHP na sprawdzanie typu, IDE na prawidłowe szeptanie, a analiza statyczna na wykrywanie błędów.

A jak zdefiniować taką wyliczankę? Po prostu w postaci klasy i jej właściwości. Nazwiemy go jak presenter, ale 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;

	// a další proměnné
}

Obiekt $this->template w prezenterze będzie teraz instancją klasy ArticleTemplate. Tak więc PHP będzie sprawdzać zadeklarowane typy podczas pisania. A począwszy od PHP 8.2, będzie również ostrzegać przy zapisie do nieistniejącej zmiennej; w poprzednich wersjach to samo można osiągnąć za pomocą cechy Nette\SmartObject.

Adnotacja @property-read jest dla IDE i analizy statycznej, sprawi, że szeptanie będzie działać, zobacz PhpStorm i uzupełnianie kodu dla $this->template.

Możesz również mieć luksus szeptania w szablonach, wystarczy zainstalować wtyczkę Latte w PhpStorm i umieścić nazwę klasy na początku szablonu, zobacz artykuł Latte: jak wpisać system, aby uzyskać więcej informacji:

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

W ten sam sposób działają szablony w komponentach, wystarczy zastosować konwencję nazewnictwa i stworzyć klasę szablonu FifteenTemplate dla komponentu np. FifteenControl.

Jeśli potrzebujesz stworzyć $template jako instancję innej klasy, użyj metody createTemplate():

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

Zmienne domyślne

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

  • $basePath to bezwzględna ścieżka URL do katalogu głównego (np. /eshop)
  • $baseUrl to bezwzględny adres URL do katalogu głównego (np. http://localhost/eshop)
  • $user jest obiektem reprezentującym użytkownika
  • $presenter jest obecnym prezenterem
  • $control jest bieżącym elementem lub prezenterem
  • $flashes jest tablicą wiadomości wysyłanych przez funkcje flashMessage()

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

Szablon tworzy w ten sposób linki do innych prezenterów & wydarzeń:

<a n:href="Product:show">detail produktu</a>

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

Adresa je: {link Home:default}

Aby uzyskać więcej informacji, zobacz Tworzenie linków URL.

Niestandardowe filtry, tagi, itp.

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

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

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

Latte w wersji 3 oferuje bardziej zaawansowany sposób tworzenia rozszerzenia dla każdego projektu internetowego. Oto krótki przykład takiej klasy:

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

	// ...
}

Rejestrujemy go za pomocą konfiguracji:

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

Tłumaczenie

Jeśli programujesz wielojęzyczną aplikację, prawdopodobnie będziesz musiał wyprowadzić część tekstu w szablonie w różnych językach. Aby to zrobić, Nette Framework definiuje interfejs tłumaczący Nette\Localization\Translator, który posiada pojedynczą metodę translate(). Przyjmuje ona komunikat $message, który zwykle jest ciągiem znaków, oraz dowolne inne parametry. Jej zadaniem jest zwrócenie przetłumaczonego ciągu znaków. W Nette nie ma domyślnej implementacji, można wybrać według własnych potrzeb spośród kilku gotowych rozwiązań, które można znaleźć na Componette. Ich dokumentacja podpowiada, jak skonfigurować translator.

Szablony można skonfigurować z tłumaczem, którego będziemy mieli przekazanego, za pomocą metody setTranslator():

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

Alternatywnie, tłumacz może być ustawiony za pomocą konfiguracji:

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

Translator może być wtedy użyty np. jako filtr |translate, z dodatkowymi parametrami przekazywanymi do metody translate() (patrz foo, bar):

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

Albo jako znacznik podkreślenia:

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

Do tłumaczenia sekcji szablonu służy sparowany tag {translate} (od wersji Latte 2.11, wcześniej używany był tag {_} ):

<a href="order">{translate}Order{/translate}</a>
<a href="order">{translate foo, bar}Order{/translate}</a>

Translator jest domyślnie wywoływany w trybie runtime podczas renderowania szablonu. Latte w wersji 3 może jednak tłumaczyć cały tekst statyczny podczas kompilacji szablonu. Oszczędza to wydajność, ponieważ każdy ciąg jest tłumaczony tylko raz, a wynikowe tłumaczenie jest zapisywane do skompilowanej postaci. Tworzy to wiele skompilowanych wersji szablonu w katalogu cache, po jednej dla każdego języka. Aby to zrobić, musisz tylko określić język jako drugi parametr:

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

Przez tekst statyczny rozumiemy na przykład {_'hello'} lub {translate}hello{/translate}. Tekst niestatyczny, taki jak {_$foo}, będzie nadal kompilowany w locie.