コンパイラパスの作成

コンパイラパスは、Latte のテンプレートが抽象構文木(AST)に解析されたあと、最終的な PHP コードが生成されるに、テンプレートを解析・変更するための強力なしくみです。これによって高度なテンプレートの操作、最適化、(サンドボックスのような)セキュリティチェック、テンプレートの分析情報の収集ができます。このガイドでは、独自のコンパイラパスの作り方を順に見ていきます。

コンパイラパスとは何か

コンパイラパスの役割を理解するには、Latte のコンパイル過程をご覧ください。見てのとおり、コンパイラパスは重要な段階で動き、最初の解析と最終的なコード出力のあいだに深く介入できます。

その本質において、コンパイラパスは単なる PHP の callable(関数、静的メソッド、インスタンスメソッドなど)で、引数をひとつ受け取ります。テンプレートの AST のルートノードで、常に Latte\Compiler\Nodes\TemplateNode のインスタンスです。

コンパイラパスの主な目的は、たいてい次のいずれか、あるいは両方です。

  • 解析: AST を辿ってテンプレートについての情報を集める(定義されたブロックをすべて見つける、特定のタグの使用を調べる、あるセキュリティ上の制約が満たされているか確かめる、など)。
  • 変更: AST の構造やノードのプロパティを変える(HTML 属性を自動で追加する、特定のタグの組み合わせを最適化する、非推奨のタグを新しいものに置き換える、サンドボックスの規則を実装する、など)。

登録

コンパイラパスは ExtensiongetPasses() メソッドで登録します。このメソッドは連想配列を返し、キーはパスの一意な名前(内部での識別と順序づけに使われます)、値はパスのロジックを実装した PHP の callable です。

use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;

class MyExtension extends Extension
{
	public function getPasses(): array
	{
		return [
			'modificationPass' => $this->modifyTemplateAst(...),
			// ... ほかのパス ...
		];
	}

	public function modifyTemplateAst(TemplateNode $templateNode): void
	{
		// 実装...
	}
}

Latte の中核の拡張とあなたの独自拡張が登録したパスは順番に実行されます。あるパスが別のパスの結果や変更に依存する場合、順序は重要になり得ます。Latte には必要に応じてこの順序を制御するしくみがあります。詳しくは Extension::getPasses() のドキュメントをご覧ください。

AST の例

AST のイメージをつかみやすくするために、例を挙げます。これがもとのテンプレートです。

{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}

そしてこれが AST の形で表した姿です。

Latte\Compiler\Nodes\TemplateNode(
   Latte\Compiler\Nodes\FragmentNode(
      - Latte\Essential\Nodes\ForeachNode(
           expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
              object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
              name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
           content: Latte\Compiler\Nodes\FragmentNode(
              - Latte\Compiler\Nodes\TextNode('  ')
              - Latte\Compiler\Nodes\Html\ElementNode('li')(
                   content: Latte\Compiler\Nodes\PrintNode(
                      expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
                         object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
                         name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\ModifierNode(
                         filters:
                            - Latte\Compiler\Nodes\Php\FilterNode('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\FragmentNode(
               - Latte\Compiler\Nodes\TextNode('no items found')
            )
        )
   )
)

NodeTraverser による AST の走査

複雑な AST の構造を辿る再帰関数を手で書くのは面倒でミスを招きます。Latte にはそのための専用の道具、Latte\Compiler\NodeTraverser があります。このクラスは Visitor デザインパターンを実装しており、AST の走査を体系的で扱いやすいものにします。

基本的な使い方は、NodeTraverser のインスタンスを作り、その traverse() メソッドに AST のルートノードと 1 つか 2 つの「ビジター」callable を渡すことです。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;

(new NodeTraverser)->traverse(
	$templateNode,

	// 'enter' ビジター: ノードに入るとき(子より前)に呼ばれます
	enter: function (Node $node) {
		echo "Entering node of type: " . $node::class . "\n";
		// ここでノードを調べられます
		if ($node instanceof Nodes\TextNode) {
			// echo "Found text: " . $node->content . "\n";
		}
	},

	// 'leave' ビジター: ノードを出るとき(子のあと)に呼ばれます
	leave: function (Node $node) {
		echo "Leaving node of type: " . $node::class . "\n";
		// 子の処理が終わったあとの操作をここで行えます
	},
);

必要に応じて、enter ビジターだけ、leave ビジターだけ、あるいは両方を渡せます。

enter(Node $node): この関数は、走査器がそのノードの子を訪れるに、各ノードに対して実行されます。次の用途に便利です。

  • 木を下りながら情報を集める。
  • 子を処理するに判断する(子をスキップすると決めるなど。走査の最適化を参照)。
  • 子が訪れられる前にノードを変更する(あまり一般的ではありません)。

leave(Node $node): この関数は、そのノードのすべての子(とその部分木全体)が完全に訪れ終わった(入って出た)あとに、各ノードに対して実行されます。次の用途に最もよく使われます。

  • 子の処理が終わったあとにノードを置き換える。
  • AST からノードを取り除く。
  • 部分木全体から集めた情報をまとめる。

enterleave のビジターは、走査の流れに影響を与える値を返すこともできます。null(または何も返さない)なら通常どおり走査を続け、Node のインスタンスを返せば現在のノードを置き換え、NodeTraverser::RemoveNodeNodeTraverser::StopTraversal のような特別な定数を返せば流れを変えます。詳しくは以降の節で説明します。

走査のしくみ

NodeTraverser は内部で、すべての Node クラスが実装しなければならない getIterator() メソッドを使います(カスタムタグの作成で触れたとおりです)。getIterator() が yield する子を反復し、それらに対して再帰的に traverse() を呼ぶことで、イテレータから辿れる木の中のすべてのノードについて、enterleave のビジターが正しい深さ優先の順序で呼ばれるようにします。ここからも、独自タグのノードで getIterator() を正しく実装することが、コンパイラパスが正しく動くために絶対に欠かせないと分かります。

テンプレートの中で {do} タグ(Latte\Essential\Nodes\DoNode で表されます)が何回使われているかを数える簡単なパスを書いてみましょう。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;

function countDoTags(TemplateNode $templateNode): void
{
	$count = 0;
	(new NodeTraverser)->traverse(
		$templateNode,
		enter: function (Node $node) use (&$count): void {
			if ($node instanceof DoNode) {
				$count++;
			}
		},
		// この用途では 'leave' ビジターは不要です
	);

	echo "Found {do} tag $count times.\n";
}

$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);

この例では、出会った各ノードの型を調べるために enter ビジターだけが必要でした。

次に、これらのビジターを使って実際に AST を変更する方法を見ていきます。

AST の変更

コンパイラパスの主な目的のひとつは、抽象構文木を変更することです。これによって、PHP コードが生成される前に、テンプレートの構造そのものに対して強力な変換や最適化、規則の強制ができます。NodeTraverserenterleave のビジターの中でこれを行ういくつかの方法を提供します。

重要な注意: AST の変更には注意が必要です。不適切な変更、たとえば不可欠なノードを取り除いたり、互換性のない型のノードに置き換えたりすると、コード生成時のエラーや、実行時の思わぬ振る舞いにつながります。変更を行うパスは必ず入念にテストしてください。

ノードのプロパティの変更

木を変更する最も簡単な方法は、走査中に出会ったノードの公開プロパティを直接変えることです。すべてのノードは、解析された引数、内容、属性を公開プロパティに保持しています。

例: 静的なテキストノード(TextNode。Latte タグの外側にある素の HTML やテキストを表します)をすべて見つけ、その内容を*AST の中で直接*大文字にするパスを作ってみましょう。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;

function uppercaseStaticText(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// TextNode には先に処理すべき子がないので 'enter' が使えます
		enter: function (Node $node) {
			// このノードは静的なテキストブロックか?
			if ($node instanceof TextNode) {
				// そうです。公開プロパティ 'content' を直接変更します。
				$node->content = mb_strtoupper(html_entity_decode($node->content));
			}
			// 何も返す必要はありません。変更はその場で行われます。
		},
	);
}

この例では、enter ビジターが現在の $nodeTextNode かどうかを調べます。そうであれば、mb_strtoupper() を使って公開プロパティ $content を直接更新します。これにより、PHP コードの生成に、AST に保持された静的テキストの内容がそのまま変わります。オブジェクトを直接変更しているので、ビジターから何も返す必要はありません。

効果: テンプレートに <p>Hello</p>{= $var }<span>World</span> が含まれていた場合、このパスのあと AST は <p>HELLO</p>{= $var }<span>WORLD</span> のようなものを表します。$var の内容には影響しません。

ノードの置き換え

より強力な変更の手法は、ノードをまったく別のものに置き換えることです。これは enterleave のビジターから新しい Node のインスタンスを返すことで行います。すると NodeTraverser が、親ノードの構造の中でもとのノードを返されたノードに差し替えます。

例: PHP_VERSION 定数(ConstantFetchNode で表されます)の使用をすべて見つけ、コンパイル時に検出された実際の PHP バージョンを含む文字列リテラル(StringNode)に直接置き換えるパスを作ってみましょう。これはコンパイル時最適化の一種です。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;

function inlinePhpVersion(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// 置き換えには 'leave' がよく使われます。子(あれば)が先に
		// 処理されることが保証されます。ここでは 'enter' でも動きます。
		leave: function (Node $node) {
			// このノードは定数へのアクセスで、その名前は 'PHP_VERSION' か?
			if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
				// 現在の PHP バージョンを保持する新しい StringNode を作ります
				$newNode = new StringNode(PHP_VERSION);

				// 任意ですが良い習慣: 位置情報をコピーします
				$newNode->position = $node->position;

				// 新しい StringNode を返します。走査器はもとの
				// ConstantFetchNode をこの $newNode に置き換えます。
				return $newNode;
			}
			// Node を返さなければ、もとの $node がそのまま残ります。
		},
	);
}

ここでは leave ビジターが PHP_VERSION に対応する ConstantFetchNode を特定します。そしてコンパイル時PHP_VERSION 定数の値を含む、まったく新しい StringNode を作ります。この $newNode を返すことで、AST の中のもとの ConstantFetchNode を置き換えるよう走査器に伝えます。

効果: テンプレートに {= PHP_VERSION } が含まれていて、コンパイルが PHP 8.2.1 で走った場合、このパスのあとの AST は実質的に {= '8.2.1' } を表します。

置き換えに enterleave のどちらを選ぶか:

  • 新しいノードの生成が古いノードの子の処理結果に依存する場合、あるいは単に置き換え前に子が訪れられることを保証したい場合(よくある慣習)は leave を使います。
  • 子が訪れられるにノードを置き換えたい場合は enter を使います。

ノードの削除

ビジターから特別な定数 NodeTraverser::RemoveNode を返すと、ノードを AST からまるごと取り除けます。

例: 出力からすべての HTML コメント(<!-- ... -->)を取り除いてみましょう。Latte のコメント {* ... *} はこの方法では狙えません。パーサーがその内容を捨て、専用のコメントノードではなく空の NopNode に置き換えるからです。一方 HTML コメントは Html\CommentNode ノードとして保持されるので、ここで取り除けます。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Html\CommentNode;

function removeHtmlComments(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// コメントを取り除くのに子の情報は不要なので 'enter' で十分です
		enter: function (Node $node) {
			if ($node instanceof CommentNode) {
				// このノードを AST から取り除くよう走査器に伝えます
				return NodeTraverser::RemoveNode;
			}
		},
	);
}

注意: RemoveNode は慎重に使ってください。不可欠な内容を含むノードや構造に関わるノードを取り除くと(たとえばループの内容ノードを取り除くと)、テンプレートが壊れたり、生成されるコードが不正になったりします。本当に省略可能で自己完結したノード(コメントやデバッグ用のタグなど)や、空の構造ノード(たとえば空の FragmentNode は、場合によっては後始末のパスで安全に取り除けます)に対して使うのが最も安全です。

プロパティの変更、ノードの置き換え、ノードの削除。この 3 つの方法が、コンパイラパスの中で AST を操作するための基本的な道具になります。

走査の最適化

テンプレートの AST はかなり大きくなることがあり、何千ものノードを含む場合もあります。パスが木の特定の部分だけに関心があるなら、すべてのノードを訪れるのは無駄で、コンパイル性能に影響しかねません。NodeTraverser には走査を最適化する手段があります。

子のスキップ

ある種類のノードに出会ったら、その子孫に探しているノードがあり得ないと分かっているなら、その子を訪れないよう走査器に伝えられます。これは enter ビジターから定数 NodeTraverser::DontTraverseChildren を返すことで行います。走査の経路から枝をまるごと刈り取ることで、とくにタグの中に複雑な PHP の式があるテンプレートでは、かなりの時間を節約できます。

走査の停止

パスが何か(特定のノード型、条件の成立)の最初の出現だけを見つければよいなら、それを見つけた時点で走査全体を完全に止められます。これは enterleave のビジターから定数 NodeTraverser::StopTraversal を返すことで実現します。traverse() メソッドはそれ以上ノードを訪れるのをやめます。非常に大きな木の中で最初の一致だけが必要なときに、とても効果的です。

便利な NodeHelpers クラス

NodeTraverser は細かい制御を提供しますが、Latte には便利なユーティリティクラス Latte\Compiler\NodeHelpers もあります。これはよくある探索と解析の作業のために NodeTraverser を包んだもので、定型コードを減らせます。

find (Node $startNode, callable $filter)array

この静的メソッドは、$startNode(自身を含む)から始まる部分木の中で、$filter コールバックを満たすすべてのノードを見つけます。一致したノードの配列を返します。

例: テンプレート全体からすべての変数ノード(VariableNode)を見つけます。

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;

function findAllVariables(TemplateNode $templateNode): array
{
	return NodeHelpers::find(
		$templateNode,
		fn($node) => $node instanceof VariableNode,
	);
}

findFirst (Node $startNode, callable $filter)?Node

find と似ていますが、$filter コールバックを満たす最初のノードを見つけた時点で走査を止めます。見つかった Node オブジェクト、あるいは一致するノードがなければ null を返します。実質的には NodeTraverser::StopTraversal の便利なラッパーです。

例: {parameters} ノードを見つけます。

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;

function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
	return NodeHelpers::findFirst(
		$templateNode->head, // 効率のため head の部分だけを探します
		fn($node) => $node instanceof ParametersNode,
	);
}

clone (Latte\Compiler\Node $node)Node

この静的メソッドは、ノードとその部分木全体のディープコピーを作ります。AST の枝を複製する必要があるとき、たとえばもとのノードをそのままにして変更したコピーを挿入したいときに便利です。

use Latte\Compiler\NodeHelpers;

$copy = NodeHelpers::clone($node);

toValue (ExpressionNode $node, bool $constants = false)mixed

この静的メソッドは、ExpressionNodeコンパイル時に評価し、それに対応する PHP の値を返そうとします。確実に動くのは単純なリテラルのノード(StringNodeIntegerNodeFloatNodeBooleanNodeNullNode)と、そうした評価可能な要素だけを含む ArrayNode のインスタンスに限られます。

$constantstrue にすると、defined() を確認して constant() を使い、ConstantFetchNodeClassConstantFetchNode の解決も試みます。

ノードが変数、関数呼び出し、そのほか動的な要素を含む場合はコンパイル時に評価できず、このメソッドは InvalidArgumentException を投げます。

使いどころ: コンパイル時に判断を下すために、タグの引数の静的な値を取得する。

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;

function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
	try {
		$value = NodeHelpers::toValue($argumentNode);
		return is_string($value) ? $value : null;
	} catch (\InvalidArgumentException $e) {
		// 引数は静的なリテラル文字列ではありませんでした
		return null;
	}
}

toText (?Node $node): ?string

この静的メソッドは、単純なノードから素のテキスト内容を取り出すのに便利です。主に次のものに対して働きます。

  • TextNode: その $content を返します。
  • FragmentNode: すべての子に対する toText() の結果を連結します。テキストに変換できない子がある場合(たとえば PrintNode を含む場合)は null を返します。
  • NopNode: 空文字列を返します。
  • そのほかのノード型: null を返します。

使いどころ: コンパイラパスでの解析のために、HTML 属性の値や単純な HTML 要素の静的なテキスト内容を取得する。

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;

function getStaticAttributeValue(AttributeNode $attr): ?string
{
	// $attr->value はふつう AreaNode(FragmentNode や TextNode など)です
	return NodeHelpers::toText($attr->value);
}

// パスでの使用例:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
//     $nameAttrValue = $node->getAttribute('name');
//     if ($nameAttrValue === 'description') { ... }
// }

NodeHelpers は、よくある AST の走査と解析の作業に対する既製の解を提供して、コンパイラパスを簡潔にしてくれます。

実践的な例

AST の走査と変更の考え方を、いくつかの実践的な問題に当てはめてみましょう。これらの例は、コンパイラパスでよく使われるパターンを示します。

<img> への loading="lazy" の自動追加

現代のブラウザは loading="lazy" 属性による画像のネイティブな遅延読み込みに対応しています。loading 属性をまだ持たないすべての <img> タグに、この属性を自動で追加するパスを作ってみましょう。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;

function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// ノードを直接変更し、この判断に子は関係しないので
		// 'enter' が使えます。
		enter: function (Node $node) {
			// 'img' という名前の HTML 要素か?
			if ($node instanceof Html\ElementNode && $node->name === 'img') {
				// 'loading' 属性がすでにあるか確認します(大文字小文字を区別しません)
				foreach ($node->attributes->children as $attrNode) {
					if ($attrNode instanceof Html\AttributeNode
						&& $attrNode->name instanceof Nodes\TextNode // 静的な属性名
						&& strtolower($attrNode->name->content) === 'loading'
					) {
						return; // すでにあるので何もしません
					}
				}

				// 属性が空でなければ空白を前に足します
				if ($node->attributes->children) {
					$node->attributes->children[] = new Nodes\TextNode(' ');
				}

				// 新しい属性ノード loading="lazy" を作ります
				$node->attributes->children[] = new Html\AttributeNode(
					name: new Nodes\TextNode('loading'),
					value: new Nodes\TextNode('lazy'),
					quote: '"',
				);
				// 変更はその場で行われるので、返す必要はありません。
			}
		},
	);
}

解説:

  • enter ビジターは img という名前の Html\ElementNode ノードを探します。
  • 既存の属性($node->attributes->children)を反復して、loading 属性がすでにあるか確認します。
  • なければ、loading="lazy" を表す新しい Html\AttributeNode を作り、(必要なら空白を前に付けて)追加します。

関数呼び出しのチェック

コンパイラパスは Latte のサンドボックスの土台です。本物のサンドボックスは洗練されていますが、禁止された関数呼び出しをチェックするという基本的な原理は示せます。

目標: テンプレートの式の中で、危険になり得る shell_exec 関数の使用を防ぐ。

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;

function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
	$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // 単純なリスト

	(new NodeTraverser)->traverse(
		$templateNode,
		enter: function (Node $node) use ($forbiddenFunctions) {
			// 直接の関数呼び出しノードか?
			if ($node instanceof Php\Expression\FunctionCallNode
				&& $node->name instanceof Php\NameNode
				&& isset($forbiddenFunctions[strtolower((string) $node->name)])
			) {
				throw new SecurityViolationException(
					"Function {$node->name}() is not allowed.",
					$node->position,
				);
			}
		},
	);
}

解説:

  • 禁止する関数名のリストを定義します。
  • enter ビジターが FunctionCallNode を探します。
  • 関数名($node->name)が静的な NameNode であれば、その小文字表現を禁止リストと照合します。
  • 禁止された関数が見つかったら Latte\SecurityViolationException を投げます。これはセキュリティ規則の違反をはっきり示し、コンパイルを止めます。

これらの例は、NodeTraverser を使うコンパイラパスが、テンプレートの AST の構造と直接やり取りすることで、解析、自動的な変更、セキュリティ制約の強制に使えることを示しています。

ベストプラクティス

コンパイラパスを書くときは、堅牢で保守しやすく効率的な拡張を作るために、次の指針を心に留めておいてください。

  • 順序が大事: パスが実行される順序を意識しましょう。あなたのパスが別のパス(Latte の中核のパスや別の独自パス)が作った AST の構造に依存する場合、あるいはほかのパスがあなたの変更に依存する可能性がある場合は、Extension::getPasses() が提供する順序づけのしくみで依存関係(before/after)を定義してください。詳しくは Extension::getPasses() のドキュメントをご覧ください。
  • 単一責任: パスはひとつのはっきり定義された仕事を行うようにしましょう。複雑な変換では、ロジックを複数のパスに分けることを検討してください。たとえば解析用のパスと、その結果にもとづく変更用のパスです。分かりやすさとテストのしやすさが向上します。
  • パフォーマンス: コンパイラパスはテンプレートのコンパイル時間を増やします(とはいえ、これはテンプレートが変わるまで一度きりです)。可能なら、パスの中で計算量の大きい処理は避けましょう。AST の特定の部分を訪れる必要がないと分かっているなら、NodeTraverser::DontTraverseChildrenNodeTraverser::StopTraversal といった走査の最適化を活用してください。
  • NodeHelpers を使う: 特定のノードを探す、単純な式を静的に評価するといったよくある作業では、独自の NodeTraverser ロジックを書く前に、Latte\Compiler\NodeHelpers に適したメソッドがないか確認しましょう。時間を節約でき、定型コードも減らせます。
  • エラー処理: パスがテンプレートの AST の中にエラーや不正な状態を見つけたら、分かりやすいメッセージと該当する Position オブジェクト(ふつうは $node->position)を添えて Latte\CompileException(セキュリティ上の問題なら Latte\SecurityViolationException)を投げてください。テンプレートの開発者に役立つ手がかりになります。
  • べき等性(できれば): 理想としては、同じ AST に対してパスを何度実行しても、一度実行したのと同じ結果になるべきです。常に実現できるとは限りませんが、実現できればデバッグやパス間の相互作用の理解が楽になります。たとえば、変更を行うパスでは、その変更がすでに適用されていないかを確認してから適用するようにしましょう。

これらの実践に従えば、コンパイラパスを強力かつ確実に活用して Latte の機能を広げ、より安全で、より最適化された、あるいは機能豊かなテンプレート処理に貢献できます。

バージョン: 3.x