開発者向けの実践

インストール

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 のキャッシュがまだ存在しないために全部が同時にキャッシュを生成しようとする状況のことです。CPU が跳ね上がります。Latte は賢いので、同時リクエストが複数あるとき、キャッシュを生成するのは最初のスレッドだけで、ほかは待ってからそれを使います。

配置のとき(たとえばデプロイスクリプトの中)に Engine::warmupCache() メソッドでキャッシュをあらかじめ作っておくこともできます。指定したテンプレートを前もってキャッシュにコンパイルするので、最初の訪問者が待たずに済みます: $latte->warmupCache('template.latte')

Latte を拡張する方法

Latte は、単純なヘルパーからまったく新しい言語構造まで、いくつもの方法でカスタマイズできます。Latte の拡張のページで詳しく扱っていますが、ここでざっと見ておきましょう。

  • カスタムフィルタ: テンプレートの出力でデータを整形・変換するためのもの({$var|myFilter} など)。
  • カスタム関数: テンプレートの式の中から呼ぶ独自のロジックのためのもの({myFunction($arg)} など)。
  • カスタムタグ: まったく新しい言語構造のためのもの({mytag}...{/mytag}n:mytag)。
  • コンパイラパス: 解析と PHP コード生成のあいだでテンプレートの AST を書き換える関数(最適化やセキュリティチェックなど)。
  • カスタムローダー: Latte がテンプレートファイルを見つけて読み込む方法を変えるためのもの。

拡張を複数のプロジェクトで再利用したり、ほかの人と共有したりしたいなら、Latte Extensionクラスにまとめましょう。

クラスとしてのパラメータ

変数を配列としてテンプレートに渡すより、クラスを作るほうが優れています。型安全な書き方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 のコンパイルエラーを引き起こすコードが書けます。そうした操作をテンプレートに書けば、生成される PHP コードにも入ってしまいます。PHP には 200 を超える禁止された操作があるので、Latte はそれらの検出を目指してはいません。レンダリング時に PHP 自身が指摘してくれますし、たいていはそれで問題ありません。

とはいえ、テンプレートに PHP のコンパイルエラーが含まれていないことを、コンパイルの時点で知りたい場面もあります。とくにテンプレートをユーザーが編集できる場合や、サンドボックスを使う場合です。そうしたときは、コンパイル時にテンプレートをチェックさせましょう。この機能は Engine::enablePhpLinter() メソッドで有効にできます。チェックのために PHP バイナリを呼び出す必要があるので、そのパスをパラメータとして渡してください。

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

try {
	$latte->compile('home.latte');
} catch (Latte\CompileException $e) {
	// Latte のエラーと PHP のコンパイルエラーの両方を捕まえます
	echo 'Error: ' . $e->getMessage();
}

ロケール

Latte ではロケールを設定でき、数値や日付の書式、並べ替えに影響します。設定には setLocale() メソッドを使います。ロケールの識別子は PHP の intl 拡張が使う IETF 言語タグの標準に従います。言語コードと、場合によっては国コードから成ります。たとえばアメリカ英語なら en_US、ドイツのドイツ語なら de_DE です。

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

ロケールの設定は localDatesortnumberbytes のフィルタに影響します。

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 は出力される属性をチェックし、Latte 3.0 が生成したはずの出力と異なる場合にユーザー警告(E_USER_WARNING)を発します。警告に出会ったら、次のいずれかの方法で対処してください。

  1. 新しい出力があなたの用途にとって正しい場合(たとえば null のとき属性が消えてほしい場合)は、|accept フィルタを付けて警告を抑制します
  2. 変数が null のときに属性を削除するのではなく空(title="" など)として出力したい場合は、フォールバックとして空文字列を渡します: title={$val ?? ''}
  3. どうしても以前の振る舞いが必要な場合(たとえば true に対して "true" ではなく "1" を出力したい場合)は、値を明示的に文字列にキャストします: 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
	{
		// $this->lang に応じて $original から $translated を作ります
		return $translated;
	}
}

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

トランスレーターはテンプレートのレンダリング時、つまり実行時に呼ばれます。しかし Latte は、テンプレートのコンパイル時にすべての静的なテキストを翻訳できます。各文字列が一度だけ翻訳され、その結果がコンパイル済みファイルに書き込まれるので、性能が上がります。この場合、キャッシュディレクトリには言語ごとに複数のコンパイル済みテンプレートができます。そのためには、第 2 パラメータに言語を指定するだけです。

$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} の 3 つのタグがあります。

最も快適なのは、素晴らしいデバッグツール Tracyをインストールして Latte のプラグインを有効にすることです。

// Tracy を有効にします
Tracy\Debugger::enable();

$latte = new Latte\Engine;
// Tracy の拡張を有効にします
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);

これですべてのエラーが見やすい赤い画面に表示されるようになり、テンプレートのエラーも行と列のハイライト付きで示されます(動画)。同時に、画面右下のいわゆる Tracy バーに Latte のタブが現れ、レンダリングされたすべてのテンプレートとその関係(テンプレートやコンパイル済みコードへ飛べます)、そして変数が一目で分かります。

Latte はテンプレートを読みやすい PHP コードにコンパイルするので、IDE で快適にステップ実行できます。

Linter: テンプレート構文の検証

Linter ツールはすべてのテンプレートを検証するために使います。指定したファイルを走査し、構文エラーがないこと、存在しないタグ・フィルタ・関数・クラスなどへの参照がないことを確かめるのが目的です。

Linter はコマンドラインから実行します。

vendor/bin/latte-lint <path>

--strict パラメータで厳格モードを有効にできます。--debug パラメータは処理した各ファイルの名前と例外の詳細を出力するので、原因を探るときに役立ちます。

カスタムタグ、フィルタ、そのほかの Latte 拡張を使っている場合は、custom-latte-lint のような独自の Linter を作る必要があります。このスクリプトでは、実際にテンプレートを検証する前に必要な拡張をすべて登録します。

#!/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);

あるいは、独自の Latte\Engine オブジェクトを Linter に渡すこともできます。

$latte = new Latte\Engine;
// ここで $latte オブジェクトを設定します
$linter = new Latte\Tools\Linter(engine: $latte);

こうしてできあがったカスタムの linter は標準のツールと同じように使えますが、あなたの拡張をすべて把握したうえで動きます。

文字列からのテンプレートの読み込み

テストなどの目的で、ファイルではなく文字列からテンプレートを読み込みたいですか。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