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

Фильтры — это мощные инструменты для форматирования и изменения данных непосредственно в шаблонах 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-объект, стоящий за фильтром, может быть замыканием (closure), статическим методом или методом экземпляра. Это позволяет фильтрам получать доступ к сервисам приложения или данным, если это необходимо, хотя их основной целью остается преобразование входного значения.

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

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

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

Существует несколько способов определения и регистрации пользовательских фильтров в Latte.

Прямая регистрация с помощью addFilter()

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

$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\Latte;

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 App\Latte\MyLatteExtension);

Этот подход сохраняет логику вашего фильтра инкапсулированной и упрощает регистрацию.

Использование загрузчика фильтров

Latte позволяет регистрировать загрузчик фильтров с помощью addFilterLoader(). Это единственный вызываемый объект, который Latte запросит для любого неизвестного имени фильтра во время компиляции. Загрузчик возвращает вызываемый PHP-объект фильтра или null.

$latte = new Latte\Engine;

// Загрузчик может динамически создавать/получать вызываемые объекты фильтров
$latte->addFilterLoader(function (string $name): ?callable {
	if ($name === 'myLazyFilter') {
		// Представьте здесь сложную инициализацию...
		$service = get_some_expensive_service();
		return fn($value) => $service->process($value);
	}
	return null;
});

Этот метод был в основном предназначен для ленивой загрузки фильтров с очень сложной инициализацией. Однако современные практики внедрения зависимостей (dependency injection) обычно справляются с ленивыми сервисами более эффективно.

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

Фильтры, использующие класс с атрибутами

Еще один элегантный способ определения фильтров — использование методов в вашем классе параметров шаблона. Просто добавьте атрибут #[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(
			"Фильтр |money используется в несовместимом типе контента $actualType. Ожидался text или 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.0