Создание пользовательских фильтров

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

Что такое фильтры?

Фильтры в Latte – это по сути функции PHP, предназначенные специально для преобразования входного значения в выходное. Они применяются с помощью записи через вертикальную черту (|) в выражениях шаблона ({...}).

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

{* Вместо сложного PHP для обрезки: *}
{$article->text|truncate:100}

{* Вместо кода форматирования даты: *}
{$event->startTime|date:'Y-m-d H:i'}

{* Применение нескольких преобразований: *}
{$product->name|lower|capitalize}

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

Учёт контекста: ключевая сильная сторона фильтров Latte – способность учитывать контекст. Это значит, что фильтр может понимать, с содержимым какого типа он работает (HTML, JavaScript, простой текст и так далее), и применять подходящую логику или экранирование, что критично для безопасности и корректности, особенно при генерации HTML.

Интеграция с логикой приложения: как и у пользовательских функций, PHP-callable за фильтром может быть замыканием, статическим методом или методом экземпляра. Это позволяет фильтрам при необходимости обращаться к сервисам или данным приложения, хотя их основное назначение остаётся прежним – преобразовать входное значение.

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

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

Создание и регистрация фильтров

Есть несколько способов определить и зарегистрировать пользовательские фильтры в Latte.

Прямая регистрация через addFilter()

Самый простой способ добавить фильтр – использовать метод addFilter() прямо на объекте Latte\Engine. Вы указываете имя фильтра (как он будет использоваться в шаблоне) и соответствующий PHP-callable.

$latte = new Latte\Engine;

// Простой фильтр без аргументов
$latte->addFilter('initial', fn(string $s): string => mb_substr($s, 0, 1) . '.');

// Фильтр с необязательным аргументом
$latte->addFilter('shortify', function (string $s, int $len = 10): string {
	return mb_substr($s, 0, $len);
});

// Фильтр, обрабатывающий массив
$latte->addFilter('sum', fn(array $numbers): int|float => array_sum($numbers));

Использование в шаблоне:

{$name|initial}                 {* Выводит 'J.', если $name равно 'John' *}
{$description|shortify}         {* Использует длину по умолчанию 10 *}
{$description|shortify:50}      {* Использует длину 50 *}
{$prices|sum}                   {* Выводит сумму элементов массива $prices *}

Передача аргументов:

Значение слева от вертикальной черты (|) всегда передаётся в функцию фильтра как первый аргумент. Все параметры, указанные после двоеточия (:) в шаблоне, передаются как последующие аргументы.

{$text|shortify:30}
// Вызывает функцию PHP shortify($text, 30)

Регистрация через расширение

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

namespace App\Templating;

use Latte\Extension;

class MyLatteExtension extends Extension
{
	public function getFilters(): array
	{
		return [
			'initial' => $this->initial(...),
			'shortify' => $this->shortify(...),
		];
	}

	public function initial(string $s): string
	{
		return mb_substr($s, 0, 1) . '.';
	}

	public function shortify(string $s, int $len = 10): string
	{
		return mb_substr($s, 0, $len);
	}
}

// Регистрация
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);

Такой подход держит логику фильтров упакованной, а регистрацию делает простой.

Фильтры через класс с атрибутами

Ещё один изящный способ определить фильтры – использовать методы вашего класса параметров шаблона. Достаточно добавить к методу атрибут #[Latte\Attributes\TemplateFilter].

use Latte\Attributes\TemplateFilter;

class TemplateParameters
{
	public function __construct(
		public string $description,
		// другие параметры...
	) {}

	#[TemplateFilter]
	public function shortify(string $s, int $len = 10): string
	{
		return mb_substr($s, 0, $len);
	}
}

// Передаём объект в шаблон
$params = new TemplateParameters(description: '...');
$latte->render('template.latte', $params);

Latte автоматически обнаружит и зарегистрирует методы, помеченные этим атрибутом, когда объект TemplateParameters будет передан в шаблон. Имя фильтра в шаблоне совпадёт с именем метода (в данном случае shortify).

{* Используем фильтр, определённый в классе параметров *}
{$description|shortify:50}

Контекстные фильтры

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

Контекстный фильтр определяется так же, как обычный, но его первый параметр должен быть объявлен с типом Latte\Runtime\FilterInfo. Latte автоматически распознаёт такую сигнатуру и при вызове фильтра передаёт объект FilterInfo. Последующие параметры получают аргументы фильтра как обычно.

use Latte\Runtime\FilterInfo;
use Latte\ContentType;

$latte->addFilter('money', function (FilterInfo $info, float $amount): string {
	// 1. Проверяем тип входного содержимого (необязательно, но рекомендуется)
	//    Разрешаем null (ввод из переменной) или простой текст. Отклоняем применение к HTML и т. п.
	if (!in_array($info->contentType, [null, ContentType::Text], true)) {
		$actualType = $info->contentType ?? 'mixed';
		throw new \RuntimeException(
			"Filter |money used in incompatible content type $actualType. Expected text or null."
		);
	}

	// 2. Выполняем преобразование
	$formatted = number_format($amount, 2, '.', ',') . ' EUR';
	$htmlOutput = '<i>' . htmlspecialchars($formatted) . '</i>'; // Обеспечиваем правильное экранирование!

	// 3. Объявляем тип выходного содержимого
	$info->contentType = ContentType::Html;

	// 4. Возвращаем результат
	return $htmlOutput;
});

$info->contentType – это строковая константа из Latte\ContentType (например, ContentType::Html, ContentType::Text, ContentType::JavaScript и так далее) или null, если фильтр применяется к переменной ({$var|filter}). Вы можете читать её, чтобы проверить входной контекст, и записывать в неё, чтобы объявить тип выходного контекста.

Установив тип содержимого в HTML, вы говорите Latte, что строка, возвращённая вашим фильтром, – безопасный HTML. Тогда Latte не применит к этому результату своё автоматическое экранирование. Это принципиально важно, если ваш фильтр генерирует HTML-разметку.

Если ваш фильтр генерирует HTML, вы отвечаете за правильное экранирование всех входных данных, используемых внутри этого HTML (как в вызове htmlspecialchars($formatted) выше). Иначе можно создать XSS-уязвимость. Если фильтр возвращает только простой текст, устанавливать $info->contentType не нужно.

Фильтры на блоках

Фильтры, применяемые к блокам с типом содержимого, отличным от текста (обычно HTML), должны быть контекстными. Дело в том, что у содержимого блока есть определённый тип, о котором фильтр должен знать. Обычный, неконтекстный фильтр можно применить только к блоку, содержимое которого – простой текст.

{block heading|money}1000{/block}
{* Фильтр 'money' получает '1000' как второй аргумент,
   а $info->contentType будет ContentType::Html *}

Контекстные фильтры дают мощный контроль над тем, как обрабатываются данные в зависимости от контекста, открывают доступ к продвинутым возможностям и обеспечивают правильное экранирование, особенно при генерации HTML-содержимого.

версия: 3.x