Создание пользовательских фильтров
Фильтры – мощный инструмент для форматирования и изменения
данных прямо в шаблонах 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-содержимого.