Практики для разработчиков

Установка

Лучший способ установить Latte – через Composer:

composer require latte/latte

Поддерживаемые версии PHP (относится к последним патч-версиям Latte):

версия совместимость с PHP
Latte 3.1 PHP 8.2 – 8.5
Latte 3.0 PHP 8.0 – 8.5

Как отрисовать шаблон

Как отрисовать шаблон? Достаточно такого простого кода:

$latte = new Latte\Engine;
// каталог кеша
$latte->setCacheDirectory('/path/to/tempdir');

$params = [ /* переменные шаблона */ ];
// или $params = new TemplateParameters(/* ... */);

// отрисовка в вывод
$latte->render('template.latte', $params);
// или отрисовка в переменную
$output = $latte->renderToString('template.latte', $params);

Параметрами могут быть массивы, а ещё лучше объект, который даст проверку типов и подсказки в редакторе.

Примеры использования можно найти и в репозитории Latte examples.

Производительность и кеширование

Шаблоны Latte исключительно быстры, потому что Latte компилирует их прямо в PHP-код и кеширует на диске. Поэтому по сравнению с шаблонами на чистом PHP у них нет никаких дополнительных накладных расходов.

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

$latte->setAutoRefresh(false);

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

Кеш можно также сгенерировать заранее во время развёртывания (например, в скрипте деплоя) методом Engine::warmupCache(). Он заблаговременно компилирует указанный шаблон в кеш, чтобы первому посетителю не пришлось ждать: $latte->warmupCache('template.latte').

Способы расширения Latte

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

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

Параметры как класс

Лучше, чем передавать переменные в шаблон массивом, создать класс. Вы получите типобезопасную запись, удобные подсказки в IDE и способ зарегистрировать фильтры и функции.

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,
));

Отключение автоматического экранирования переменной

Если переменная содержит HTML-строку, вы можете пометить её так, чтобы Latte не экранировал её автоматически (и тем самым дважды). Это избавляет от необходимости указывать в шаблоне |noescape.

Проще всего обернуть строку в объект Latte\Runtime\Html:

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

Latte также не экранирует все объекты, реализующие интерфейс Latte\Runtime\HtmlStringable. Так что вы можете создать собственный класс, метод __toString() которого будет возвращать HTML-код, не подлежащий автоматическому экранированию:

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'),
];

Метод __toString должен возвращать корректный HTML и обеспечивать экранирование параметров, иначе может возникнуть XSS-уязвимость!

Как расширить Latte фильтрами, тегами и так далее

Как добавить в Latte собственный фильтр, функцию, тег и прочее? Узнайте в главе расширение Latte. Если вы хотите переиспользовать свои доработки в разных проектах или поделиться ими с другими, вам стоит создать расширение.

Любой код в шаблоне {php ...}

Внутри тега {do} можно писать только выражения PHP, поэтому вы не можете вставить, например, конструкции вроде if ... else или инструкции, заканчивающиеся точкой с запятой.

Но вы можете зарегистрировать расширение RawPhpExtension, которое добавляет тег {php ...}. С его помощью можно вставить любой PHP-код. Он не подчиняется правилам режима песочницы, поэтому его использование остаётся на ответственности автора шаблона.

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

Проверка сгенерированного кода

Latte компилирует шаблоны в PHP-код. Разумеется, он следит за тем, чтобы сгенерированный код был синтаксически корректен. Однако при использовании сторонних расширений или RawPhpExtension Latte не может гарантировать правильность получившегося файла. Кроме того, в PHP можно написать код, который синтаксически корректен, но запрещён (например, присваивание значения переменной $this) и вызывает PHP Compile Error. Если вы напишете такую операцию в шаблоне, она попадёт и в сгенерированный PHP-код. Поскольку запрещённых операций в PHP больше двух сотен, Latte не ставит целью их обнаруживать. Сам PHP укажет на них при отрисовке, и обычно это не проблема.

Но бывают ситуации, когда вы хотите знать уже во время компиляции шаблона, что в нём нет ошибок PHP Compile Error. Особенно когда шаблоны могут редактировать пользователи или когда вы используете песочницу. В таком случае поручите проверять шаблоны во время компиляции. Включить эту возможность можно методом Engine::enablePhpLinter(). Поскольку для проверки нужно вызвать бинарник PHP, передайте путь к нему в параметре:

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

try {
	$latte->compile('home.latte');
} catch (Latte\CompileException $e) {
	// перехватывает ошибки Latte, а также Compile Error в PHP
	echo 'Error: ' . $e->getMessage();
}

Национальные настройки

Latte позволяет задать локаль, которая влияет на форматирование чисел, дат и на сортировку. Устанавливается она методом setLocale(). Идентификатор локали следует стандарту языковых тегов IETF, который использует расширение PHP intl. Он состоит из кода языка и, возможно, кода страны, например en_US для английского в США, de_DE для немецкого в Германии и так далее.

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

Настройка локали влияет на фильтры localDate, sort, number и bytes.

Требуется расширение PHP intl. Настройка в Latte не влияет на глобальную настройку локали в PHP.

Строгий режим

В строгом режиме разбора Latte проверяет отсутствующие закрывающие HTML-теги, а также запрещает использование переменной $this. Чтобы включить его:

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

Чтобы генерировать шаблоны с заголовком declare(strict_types=1), поступите так:

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

Начиная с Latte 3.1 строгие типы включены по умолчанию. Отключить их можно через $latte->setFeature(Latte\Feature::StrictTypes, false).

Предупреждения о миграции

Latte 3.1 меняет поведение некоторых HTML-атрибутов. Например, значения null теперь убирают атрибут вместо вывода пустой строки. Чтобы легко найти места, где это изменение затрагивает ваши шаблоны, вы можете включить предупреждения о миграции:

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

После включения Latte проверяет отрисованные атрибуты и выдаёт пользовательское предупреждение (E_USER_WARNING), если вывод отличается от того, который получился бы в Latte 3.0. Столкнувшись с предупреждением, примените одно из решений:

  1. Если новый вывод верен для вашего случая (например, вы предпочитаете, чтобы атрибут исчезал при null), подавите предупреждение, добавив фильтр |accept
  2. Если вы хотите, чтобы атрибут отрисовывался пустым (например, title=""), а не пропадал, когда переменная равна null, задайте пустую строку как запасное значение: title={$val ?? ''}
  3. Если вам строго нужно старое поведение (например, вывод "1" для true вместо "true"), явно приведите значение к строке: data-foo={(string) $val}

Когда все предупреждения устранены, выключите предупреждения о миграции и удалите все фильтры |accept из шаблонов, потому что они больше не нужны.

Область видимости переменных цикла

По умолчанию переменные, определённые в цикле {foreach} (такие как $key и $value), остаются доступны и после его окончания, ровно как в самом PHP. Это может привести к непреднамеренной перезаписи переменных, когда переменная цикла называется так же, как уже существующая переменная шаблона.

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

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

Пример различия:

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

Без ScopedLoopVariables: выводит 1, 2, 2 (переменная перезаписана) С ScopedLoopVariables: выводит 1, 2, original (переменная восстановлена)

Это работает и с синтаксисом деструктуризации, например {foreach $array as [$a, $b]}.

Переменные цикла, использующие ссылки ({foreach $array as &$value}) или присваивание в свойство ({foreach $array as $obj->prop}), в область видимости не заключаются, потому что это сломало бы их назначение.

Автоматическое снятие отступов

Используя парные теги вроде {if}, {foreach} или {block}, вы часто делаете отступ вложенного содержимого ради читаемости. Однако по умолчанию этот отступ попадает в сгенерированный вывод. Возможность Dedent автоматически его убирает, поэтому вывод остаётся чистым независимо от того, насколько глубоко вложены ваши теги Latte:

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

Пример:

{if true}
	Hello
	World
{/if}

Без Dedent вывод содержал бы отступы (\tHello\n\tWorld\n). С Dedent отступы убираются, и вывод становится Hello\nWorld\n.

Более глубокий отступ внутри блока сохраняется относительно базового отступа:

{if true}
	Hello
		Indented
{/if}

Вывод: Hello\n\tIndented\n.

Отступы внутри блока должны быть единообразными (либо табуляции, либо пробелы). Если их смешать, Latte выбросит исключение Inconsistent indentation.

Перевод в шаблонах

Используйте расширение TranslatorExtension, чтобы добавить в шаблон {_...}, {translate} и фильтр translate. Они служат для перевода значений или частей шаблона на другие языки. Параметром служит callable, выполняющий перевод, или объект типа Nette\Localization\Translator (передайте null, чтобы отключить переводы):

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

	public function translate(string $original): string
	{
		// создаём $translated из $original в соответствии с $this->lang
		return $translated;
	}
}

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

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

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

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

Шаблон может передавать переводчику и дополнительные параметры через {_$original, foo: bar} или {translate foo: bar}, которые тот получает как массив $params:

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

Отладка и Tracy

Latte старается сделать разработку максимально приятной. Для отладки есть три тега: {dump}, {debugbreak} и {trace}.

Больше всего удобства вы получите, установив прекрасный инструмент отладки Tracy и включив плагин Latte:

// включает Tracy
Tracy\Debugger::enable();

$latte = new Latte\Engine;
// активирует расширение Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);

Теперь вы будете видеть все ошибки на аккуратном красном экране, включая ошибки в шаблонах с подсветкой строки и столбца (видео). Одновременно в правом нижнем углу, в так называемой панели Tracy Bar, появится вкладка Latte, где наглядно видны все отрисованные шаблоны и их взаимосвязи (с возможностью перейти в шаблон или в скомпилированный код), а также переменные:

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

Linter: проверка синтаксиса шаблонов

Инструмент Linter служит для проверки всех шаблонов. Его задача – просмотреть указанные файлы и убедиться, что в них нет синтаксических ошибок и ссылок на несуществующие теги, фильтры, функции, классы и подобные конструкции.

Linter запускается из командной строки:

vendor/bin/latte-lint <path>

Параметр --strict включает строгий режим. Параметр --debug выводит имя каждого обработанного файла и полные подробности исключения, что помогает при разборе проблем.

Если вы используете собственные теги, фильтры или другие расширения Latte, вам нужно создать свой вариант Linter, например custom-latte-lint. В этом скрипте вы регистрируете все необходимые расширения до того, как начнётся сама проверка шаблонов:

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

// укажите настоящий путь к файлу autoload.php
require __DIR__ . '/vendor/autoload.php';

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

$linter = new Latte\Tools\Linter;
$latte = $linter->getEngine();
// добавьте здесь свои расширения
$latte->addExtension(/* ... */);

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

Кроме того, вы можете передать в Linter собственный объект Latte\Engine:

$latte = new Latte\Engine;
// здесь настраиваем объект $latte
$linter = new Latte\Tools\Linter(engine: $latte);

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

Загрузка шаблонов из строки

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

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

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

Обработчик исключений

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

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

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

Автоматический поиск макета

С помощью тега {layout} шаблон определяет свой родительский шаблон. Можно также сделать так, чтобы макет искался автоматически, что упростит написание шаблонов: тег {layout} в них будет не нужен.

Добиться этого можно так:

// возвращает путь к файлу родительского шаблона
$finder = fn(Latte\Runtime\Template $template) => 'automatic.layout.latte';
$latte = new Latte\Engine;
$latte->addProvider('coreParentFinder', $finder);

Если у шаблона не должно быть макета, он укажет это тегом {layout none}.

версия: 3.x