Шаблоны

Nette использует систему шаблонов Latte. Latte выбран потому, что это самая безопасная система шаблонов для PHP и одновременно самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов.

Обычно страница состоит из шаблона макета и шаблона конкретного действия. Вот как может выглядеть шаблон макета; обратите внимание на блоки {block} и тег {include}:

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

А вот шаблон действия:

{block title}Homepage{/block}

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

Он задаёт блок content, который вставляется на место {include content} в макете, а также переопределяет блок title, перекрывающий {block title} в макете. Попробуйте представить результат.

Поиск шаблона

В презентерах вам не нужно указывать, какой шаблон отрисовать: фреймворк выводит путь автоматически и избавляет вас от его написания.

Если вы используете структуру каталогов, где у каждого презентера свой каталог, просто положите шаблон в этот каталог под именем действия (то есть представления). Например, для действия default используйте шаблон default.latte:

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

Если вы используете структуру, где презентеры лежат вместе в одном каталоге, а шаблоны в папке templates, сохраните его либо в файле <Presenter>.<view>.latte, либо <Presenter>/<view>.latte:

app/
└── Presenters/
    ├── HomePresenter.php
    └── templates/
        ├── Home/
        │   └── default.latte   ← 1-й вариант
        └── Home.default.latte  ← 2-й вариант

Каталог templates можно поместить и на уровень выше, то есть рядом с каталогом классов презентеров.

Если шаблон не найден, презентер отвечает ошибкой 404 – страница не найдена.

Изменить представление можно через $this->setView('otherView'). Можно и прямо указать файл шаблона через $this->template->setFile('/path/to/template.latte').

Файлы, в которых ищутся шаблоны, можно изменить, переопределив метод formatTemplateFiles(), возвращающий массив возможных имён файлов.

Поиск шаблона макета

Nette также автоматически ищет файл макета.

Если вы используете структуру каталогов, где у каждого презентера свой каталог, поместите макет либо в папку с презентером, если он относится только к нему, либо на уровень выше, если он общий для нескольких презентеров:

app/
└── Presentation/
    ├── @layout.latte           ← общий макет
    └── Home/
        ├── @layout.latte       ← только для презентера Home
        ├── HomePresenter.php
        └── default.latte

Если вы используете структуру, где презентеры собраны в одном каталоге, а шаблоны в папке templates, макет будет ожидаться в этих местах:

app/
└── Presenters/
    ├── HomePresenter.php
    └── templates/
        ├── @layout.latte       ← общий макет
        ├── Home/
        │   └── @layout.latte   ← только для Home, 1-й вариант
        └── Home.@layout.latte  ← только для Home, 2-й вариант

Если презентер находится в модуле, поиск идёт и выше по уровням каталогов, соответственно вложенности модулей.

Имя макета можно изменить через $this->setLayout('layoutAdmin'), и тогда он будет ожидаться в файле @layoutAdmin.latte. Можно и прямо указать файл шаблона макета через $this->setLayout('/path/to/template.latte').

Вызов $this->setLayout(false) или тег {layout none} внутри шаблона отключают поиск макета.

Файлы, в которых ищутся шаблоны макетов, можно изменить, переопределив метод formatLayoutTemplateFiles(), возвращающий массив возможных имён файлов.

Переменные шаблона

Переменные передаются в шаблоны записью в $this->template. Затем они становятся доступны в шаблоне как локальные переменные:

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

Чтобы значение свойства автоматически передавалось в шаблон как переменная, пометьте его атрибутом #[TemplateVariable] и сделайте публичным:

use Nette\Application\Attributes\TemplateVariable;

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

Если вы передадите в шаблон переменную с тем же именем, #[TemplateVariable] её не перебьёт.

Переменные по умолчанию

Презентеры и компоненты автоматически передают в шаблоны несколько полезных переменных:

  • $basePath – абсолютный путь URL к корневому каталогу (например, /eshop)
  • $baseUrl – абсолютный URL корневого каталога (например, http://localhost/eshop)
  • $user – объект, представляющий пользователя
  • $presenter – текущий презентер
  • $control – текущий компонент или презентер
  • $flashes – массив сообщений, отправленных функцией flashMessage()

Если вы используете собственный класс шаблона, эти переменные передаются, если вы создадите для них свойства.

Типобезопасные шаблоны

При разработке надёжных приложений полезно явно определить, какие переменные ожидает шаблон и какого они типа. Это даёт проверку типов в PHP, умные подсказки в IDE и позволяет статическому анализу отлавливать ошибки.

Как определить такой список? Просто как класс со свойствами, представляющими переменные шаблона. Назовите его похоже на презентер, только с Template на конце:

/**
 * @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;

	// и другие переменные
}

Объект $this->template в презентере теперь будет экземпляром класса ArticleTemplate. PHP таким образом проверит объявленные типы при записи.

Класс шаблона Nette выбирает автоматически. Сначала он ищет класс с именем <Presenter><Action>Template, например ArticleEditTemplate для действия edit, и только если такого нет, переходит к <Presenter>Template.

Аннотация @property-read предназначена для IDE и статического анализа, она включает дополнение кода, см. PhpStorm и дополнение кода для $this⁠-⁠>⁠template.

Дополнение кода можно использовать и прямо в шаблонах. Достаточно установить плагин Latte для PhpStorm и указать в начале шаблона имя класса параметров шаблона, подробнее в главе Latte: система типов:

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

То же относится и к компонентам. Достаточно придерживаться соглашения об именовании и создать класс параметров FifteenTemplate для компонента вида FifteenControl.

Если вам нужно использовать другой класс параметров, воспользуйтесь методом createTemplate():

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

Если вам нужно повлиять на то, как шаблон завершается перед отрисовкой, например добавить переменные, общие для всех действий, вы можете переопределить в презентере метод completeTemplate(). Он вызывается прямо перед отрисовкой шаблона:

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

Создание ссылок

В шаблоне ссылки на другие презентеры и действия создаются так:

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

Атрибут n:href очень удобен для HTML-тегов <a>. Если мы хотим вывести ссылку в другом месте, например в тексте, мы используем {link}:

URL is: {link Home:default}

Подробнее в главе Создание URL-ссылок.

Собственные фильтры, теги и прочее

Систему шаблонов Latte можно расширять собственными фильтрами, функциями, тегами и другими элементами. Есть три подхода, от быстрых разовых решений до архитектурных приёмов для целых приложений.

Разово в методах презентера

Самый быстрый подход – добавить фильтры или функции прямо в код презентера или компонента. В презентерах для этого хорошо подходят методы beforeRender() или render<View>():

protected function beforeRender(): void
{
	// добавляем фильтр
	$this->template->addFilter('money', fn($val) => '$' . number_format($val, 2));

	// добавляем функцию
	$this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6);
}

В шаблоне:

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

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

Для более сложной логики можно настроить объект Latte\Engine напрямую:

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

С помощью атрибутов

Более изящный подход – определить фильтры и функции методами прямо в классе параметров шаблона презентера или компонента, пометив их атрибутами:

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 автоматически обнаруживает и регистрирует методы, помеченные этими атрибутами. Имя фильтра или функции в шаблонах совпадает с именем метода. Эти методы должны быть публичными.

Глобально через расширения

Предыдущие подходы годятся для фильтров и функций, нужных только в отдельных презентерах или компонентах, а не во всём приложении. Для всего приложения лучше всего создать расширение. Этот класс собирает все расширения Latte вашего проекта в одном месте. Краткий пример:

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
	{
		// ...
	}

	// ...
}

Зарегистрируйте расширение через конфигурацию:

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

Расширения дают несколько преимуществ: поддержку внедрения зависимостей, доступ к слою модели вашего приложения и управление всеми расширениями из одного места. Они поддерживают и собственные теги, провайдеры, проходы компилятора и прочее.

Настройка всех шаблонов

Сервис TemplateFactory, создающий все шаблоны, предлагает публичный массив callback-функций $onCreate. Они вызываются каждый раз при создании любого шаблона, поэтому вы можете из одного места настроить фильтры, функции или переменные для всех шаблонов приложения. Каждая callback-функция получает только что созданный шаблон. Получите сервис TemplateFactory через внедрение и зарегистрируйте callback-функции, например при старте приложения:

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

Перевод

Если вы разрабатываете многоязычное приложение, вам, скорее всего, понадобится выводить в шаблоне часть текстов на разных языках. Для этого Nette Framework определяет интерфейс перевода Nette\Localization\Translator с единственным методом translate(). Он принимает сообщение $message, которым обычно служит строка, и любые другие параметры. Задача – вернуть переведённую строку. Реализации по умолчанию в Nette нет; вы можете выбрать из нескольких готовых решений, доступных на Componette, по своим потребностям. Их документация объясняет, как настроить переводчик.

Шаблонам можно задать переводчик, который мы получаем через внедрение, методом setTranslator():

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

Как вариант, переводчик можно задать через конфигурацию:

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

Затем переводчик можно использовать, например, как фильтр |translate, в том числе с дополнительными параметрами, которые передаются в метод translate() (см. foo, bar):

<a href="basket">{='Корзина'|translate}</a>
<span>{$item|translate}</span>
<span>{$item|translate, foo, bar}</span>

Или как тег с подчёркиванием:

<a href="basket">{_'Корзина'}</a>
<span>{_$item}</span>
<span>{_$item, foo, bar}</span>

Для перевода участка шаблона есть парный тег {translate} (начиная с Latte 2.11, раньше использовался тег {_}):

<a href="order">{translate}Заказ{/translate}</a>
<a href="order">{translate foo, bar}Заказ{/translate}</a>

Переводчик обычно вызывается во время выполнения, при отрисовке шаблона. Однако Latte версии 3 умеет переводить все статические тексты уже во время компиляции шаблона. Это экономит производительность, потому что каждая строка переводится только один раз, а готовый перевод записывается в скомпилированный вид. При этом в каталоге кеша появляется несколько скомпилированных версий шаблона, по одной на каждый язык. Для этого достаточно указать язык вторым параметром:

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

Под статическим текстом понимается, например, {_'hello'} или {translate}hello{/translate}. Нестатические тексты вроде {_$foo} по-прежнему будут переводиться во время выполнения.

версия: 4.x