Latte 2 から 3 への移行

Latte 3 はコンパイラが完全に書き直され、文法が形式的にきちんと定義されました。Latte 2 とできる限り一致するようになっていますが、いくつかの構文には少し手直しが必要です。

実際のところ、テンプレートの大多数は何も変更せずに、Latte 2 でも Latte 3 でも同じように動きます。では、非互換をどう見つければよいのでしょうか。

まず、移行版の Latte 2.11 をインストールしてください。

このバージョンは新機能を持たず、新しい Latte が対応しないと分かっているケースについて E_USER_DEPRECATED で警告し、さらに重要なことに、どう直せばよいかを教えてくれます。すべてのテンプレートを調べて互換性を確かめるには、コンソールから実行する Linter ツールが使えます。

vendor/bin/latte-lint <path>

考えられる非互換を解消したら、Latte 3.0 にアップグレードしてください。そしてもう一度 Linter を実行し、新しい厳格なパーサーがすべてのテンプレートを本当に理解できるか確かめましょう。

API の変更

API の変更はカスタムタグの追加にだけ関わります。それ以外の API はバージョン 2 と同じで、テンプレートのレンダリング、パラメータの受け渡し、フィルタの登録の方法は変わりません。

例外は、いわゆる動的フィルタ Engine::addFilter(null, ...) です。これは現在、addFilter() メソッドを使うクラスによって登録されるフィルタが担当します。もとの Engine::addFilterLoader() メソッドは移行のための手段として残っていますが、非推奨です。

カスタムタグを追加するための API はまったく異なるので、Latte 2 向けに作られたアドオンは動きません。アドオンの更新 も参照してください。

構文の変更

変更点は次のとおりです。

  • フィルタのパラメータ区切りにはカンマを使います。以前の |filter: arg : arg|filter: arg, arg になります
  • {label foo}...{/label} タグは常にペアです。ペアでない場合は {label /} と書きます
  • 逆に {_'text'} タグは常に単独で、ペアの {_}...{/} は新しい {translate}...{/translate} に置き換わりました
  • {block foo-$var} のような疑似文字列は引用符で {block "foo-$var"} と書くか、波かっこを足して {block foo-{$var}} と書く必要があります
  • これは属性にも当てはまります。つまり n:block="foo-$var" ではなく n:block="foo-{$var}" を使います
  • Latte 3 ではフィルタの大文字小文字を区別する必要があります
  • {do ...}{php ...} タグには式しか書けません。任意の PHP を使うには RawPhpExtension を登録してください

さらに細かいケースもあります。

  • n:inner-xxxn:tag-xxxn:ifcontent の属性は空要素の HTML 要素には使えません
  • n:inner-snippet 属性は inner- を付けずに書かなければなりません
  • </script></style> のタグは閉じなければなりません
  • マジック変数 $iterations は削除されました($iterator と混同しないでください)
  • {includeblock file.latte} タグは {include file.latte with blocks} または {import} に置き換えてください
  • {include "abc"} は、"abc" にピリオドが含まれていてファイルだと明らかな場合を除き、{include file "abc"} と書くべきです

アドオンの更新

パーサーの全面的な書き直しにより、カスタムタグの書き方は完全に変わりました。Latte 用のカスタムタグを作っているなら、バージョン 3 向けに書き直す必要があります。ドキュメントをご覧ください。

タグを追加する他者製のアドオンを使っている場合は、作者が Latte 3 向けのバージョンを出すのを待つ必要があります。バージョン 3.1 の nette/applicationnette/cachingnette/forms ライブラリと Texy はすでに更新されており、Latte 2 と 3 の両方で動きます。

nette/application

Nette を通常どおり使っている場合、この拡張は自動的に設定されるので、何も変える必要はありません。

Latte 2 向けの古いコード:

$latte->onCompile[] = function ($latte) {
	Nette\Bridges\ApplicationLatte\UIMacros::install($latte->getCompiler());
};

$latte->addProvider('uiControl', $control);
$latte->addProvider('uiPresenter', $control->getPresenter());

Latte 3 向けの新しいコード:

$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension($control));

UIExtension は n:href{link}{control}{snippet} などを追加します。つまりスニペット用のタグは Latte 本体から nette/application ライブラリに移りました。Latte 3 では、プレゼンターの templatePrepareFilters() メソッドはもう呼ばれません。

nette/forms

Nette を通常どおり使っている場合、この拡張は自動的に設定されるので、何も変える必要はありません。

Latte 2 向けの古いコード:

$latte->onCompile[] = function ($latte) {
	Nette\Bridges\FormsLatte\FormMacros::install($latte->getCompiler());
};

Latte 3 向けの新しいコード:

$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

nette/caching

Nette を通常どおり使っている場合、この拡張は自動的に設定されるので、何も変える必要はありません。

Latte 2 向けの古いコード:

$latte->onCompile[] = function ($latte) {
	$latte->getCompiler()->addMacro('cache', new Nette\Bridges\CacheLatte\CacheMacro);
};

$latte->addProvider('cacheStorage', $cacheStorage);

Latte 3 向けの新しいコード:

$latte->addExtension(new Nette\Bridges\CacheLatte\CacheExtension($cacheStorage));

Tracy

Tracy 用のパネルも、今では拡張として有効にします。

Latte 2 向けの古いコード:

$latte = new Latte\Engine;
Latte\Bridges\Tracy\LattePanel::initialize($latte);

Latte 3 向けの新しいコード:

$latte = new Latte\Engine;
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);

翻訳

TranslatorExtension は、翻訳タグ {_'text'}、新しいペアタグ {translate}...{/translate}、そして |translate フィルタを追加します。

Latte 2 向けの古いコード:

$latte->addFilter('translate', [$translator, 'translate']);

Latte 3 向けの新しいコード:

$latte->addExtension(new Latte\Essential\TranslatorExtension($translator));

プレゼンターでは、$template->setTranslator($translator) メソッドでテンプレートにトランスレーターを設定すると自動的に有効になります。これがないと翻訳タグは使えないので、拡張を手動で、あるいは設定ファイルで登録する必要があります。

設定ファイル

Latte 2 では、設定ファイルlatte › macros セクションで新しいタグを登録できました。バージョン 3 では、この方法で拡張そのものを追加します。

latte:
	extensions:
		- App\Templating\LatteExtension
		- Latte\Essential\TranslatorExtension(@Nette\Localization\Translator)

Latte のアドオンを開発していますか?

ひとつのライブラリで Latte の両方のバージョンに同時に対応できます。バージョンの判定には Latte\Engine::VERSION 定数を使い、onCompile[]addMacro() の利用を新しい addExtension() と分けるのがよいでしょう。

if (version_compare(Latte\Engine::VERSION, '3', '<')) {
	// Latte 2 の初期化
	$this->latte->onCompile[] = function ($latte) {
		$latte->addMacro(/* ... */);
	};
} else {
	// Latte 3 の初期化
	$this->latte->addExtension(/* ... */);
}

例として、Latte 2 向けの次のコードを Latte 3 向けに書き直してみましょう。

// Latte 2 向けの古いコード
$this->latte->onCompile[] = function (Latte\Engine $latte) {
	$set = new Latte\Macros\MacroSet($latte->getCompiler());
	$set->addMacro('foo', 'echo %escape(MyClass:myFunc(%node.word, %node.array))');
};

Latte 3 は拡張で拡張します。foo タグを追加するごく単純な拡張は次のようになります。

// Latte 3 向けの新しいコード
class FooExtension extends Latte\Extension
{
	public function getTags(): array
	{
		return [
			'foo' => [FooNode::class, 'create'], // FooNode クラスはこのあと追加します
		];
	}
}

// 登録
$this->latte->addExtension(new FooExtension);

新しいコンパイラはより堅牢で、以前のような近道がないため、マクロを書くのに少し多くの行数がかかります。たとえば Latte 2 のように PHP コードの文字列を直接渡すことはできず、代わりに関数を作ります。Latte 2 では関数がこのような形だったことを思い出してください。

// Latte 2
$set->addMacro('foo', function (Latte\MacroNode $node, Latte\PhpWriter $writer) {
	return $writer->write('echo %escape(MyClass:myFunc(%node.word, %node.array))');
});

とはいえ Latte 3 のやり方もほとんど同じで、MacroNodeLatte\Compiler\TagPhpWriterLatte\Compiler\PrintContext になっただけです。ただし何より重要なのは、中間の段階がひとつ増えたことです。関数は PHP コードを直接返すのではなく、ノード、つまり StatementNode の子を返し、それが AST ツリーの一部になります。そしてこのノードは、PHP コードを返す print(Latte\Compiler\PrintContext $context): string メソッドを持ちます。

// Latte 3
class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		return $context->format('echo ...'); // PHP コードを返します
	}
}

さらに、$context->format() のマスクにはもう %node.*** の略記がありません。先にタグの内容を解析することが前提になっています。そこでパーサーを使って内容を変数(サブノード)に解析し、それから出力します。

use Latte\Compiler\Nodes\Php\Expression\ArrayNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;

class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	public ExpressionNode $subject;
	public ArrayNode $args;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = new self;
		// タグの内容を解析します
		$node->subject = $tag->parser->parseUnquotedStringOrExpression();
		$tag->parser->stream->tryConsume(',');
		$node->args = $tag->parser->parseArguments();
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		return $context->format(
			'echo %escape(MyClass:myFunc(%node, %node));',
			$this->subject,
			$this->args,
		);
	}
}

最後に、走査のときにサブノードを辿れるよう、getIterator() メソッドを追加します。

class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	...

	public function &getIterator(): \Generator
	{
		yield $this->subject;
		yield $this->args;
	}
}
バージョン: 3.x