カスタムフィルタの作成

フィルタは、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() を使用した直接登録

フィルタを追加する最も簡単な方法は、Latte\Engine オブジェクトで直接 addFilter() メソッドを使用することです。フィルタ名(テンプレートで使用される名前)と対応する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}                 {* $name が 'John' の場合 'J.' を出力 *}
{$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が未知のフィルタ名を要求する単一のcallableです。ローダーはフィルタのPHP callableまたは null を返します。

$latte = new Latte\Engine;

// ローダーは動的にフィルタcallableを作成/取得できます
$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);

TemplateParameters オブジェクトがテンプレートに渡されると、Latteはこの属性でマークされたメソッドを自動的に認識して登録します。テンプレート内のフィルタ名は、メソッド名(この場合は 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->contentTypeLatte\ContentType の文字列定数(例:ContentType::HtmlContentType::TextContentType::JavaScript など)またはフィルタが変数({$var|filter})に適用される場合は null です。この値を読み取り、入力コンテキストを確認し、書き込みして出力コンテキストタイプを宣言できます。

コンテンツタイプをHTMLに設定することで、フィルタによって返される文字列が安全なHTMLであることをLatteに伝えます。Latteは、この結果にデフォルトの自動エスケープを適用しません。これは、フィルタがHTMLマークアップを生成する場合に不可欠です。

フィルタがHTMLを生成する場合、そのHTMLで使用される入力データを適切にエスケープする責任があります(上記の htmlspecialchars($formatted) の呼び出しの場合のように)。これを怠ると、XSS脆弱性が生じる可能性があります。フィルタがプレーンテキストのみを返す場合は、$info->contentType を設定する必要はありません。

ブロック上のフィルタ

ブロックに適用されるすべてのフィルタは、コンテキスト認識型でなければなりません。これは、ブロックのコンテンツには定義されたコンテンツタイプ(通常はHTML)があり、フィルタがそれを認識する必要があるためです。

{block heading|money}1000{/block}
{* フィルタ 'money' は2番目の引数として '1000' を受け取ります
   そして $info->contentType は ContentType::Html になります *}

コンテキストフィルタは、データがそのコンテキストに基づいてどのように処理されるかを強力に制御し、高度な機能を可能にし、特にHTMLコンテンツを生成する場合に適切なエスケープ動作を保証します。

バージョン: 3.0