エクステンションの作成

エクステンションは、カスタムタグ、フィルタ、関数、プロバイダなどを定義できる再利用可能なクラスです。

Latteでカスタマイズしたものを別のプロジェクトで再利用したり、他の人と共有したりするときにエクステンションを作成します。 また、プロジェクトのテンプレートで使用したい特定のタグやフィルタをすべて含む、各ウェブプロジェクト用の拡張機能を作成することも便利です。

拡張機能クラス

Extensionは、Latte\Extension を継承したクラスです。 Latteへの登録は、addExtension() を用いて(あるいは設定ファイルによって)行います。

$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);

複数の拡張機能を登録し、それらが同じ名前のタグ、フィルタ、または関数を定義している場合、最後に追加された拡張機能が優先されます。これは、拡張機能がネイティブタグ/フィルタ/関数をオーバーライドできることも意味しています。

クラスに変更を加え、自動リフレッシュがオフになっていない場合、Latteは自動的にテンプレートを再コンパイルします。

クラスは以下のメソッドのいずれかを実装することができます。

abstract class Extension
{
	/**
	 * Initializes before template is compiler.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Returns a list of parsers for Latte tags.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Returns a list of compiler passes.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Returns a list of |filters.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Returns a list of functions used in templates.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Returns a list of providers.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Returns a value to distinguish multiple versions of the template.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Initializes before template is rendered.
	 */
	public function beforeRender(Template $template): void;
}

拡張機能がどのようなものであるかについては、組み込みのCoreExtension をご覧ください。

beforeCompile (Latte\Engine $engine)void

テンプレートがコンパイルされる前に呼び出されます。このメソッドは、例えばコンパイルに関連する初期化などに使用することができます。

getTags(): array

テンプレートがコンパイルされたときに呼び出されます。タグ解析関数である タグ名 ⇒ callable の連想配列を返します。

public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}

n:baz タグは純粋なn:attributeを表し、すなわち属性としてのみ記述可能なタグである。

foobar のタグの場合、Latte はそれらがペアであるかどうかを自動的に認識し、ペアであればn:inner-foon:tag-foo の接頭辞を持つ変種を含めて n:attribute を使って自動的に記述することができます。

このようなn:attributeの実行順序はgetTags() が返す配列の中の順序で決まります.従って、n:foon:bar の前に実行されます。 <div n:bar="..." n:foo="...">.

複数の拡張子にわたってn:属性の順序を決定する必要がある場合には、order() ヘルパーメソッドを使用して下さい。before xorafter パラメータはどのタグがタグの前と後に順序付けられるかを決定します。

public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}

getPasses(): array

テンプレートがコンパイルされるときに呼び出されます。ASTを走査し修正するいわゆるコンパイラパスを表す関数である、連想配列 name pass ⇒ callable を返します。

ここでも、order() ヘルパー・メソッドが使えます。before またはafter パラメータの値には、before/after all の意味を持つ * を指定することができます。

public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}

beforeRender (Latte\Engine $engine)void

これは各テンプレートのレンダリングの前に呼び出されます。このメソッドは、例えば、レンダリング中に使用される変数を初期化するために使用することができます。

getFilters(): array

テンプレートがレンダリングされる前に呼び出されます。フィルタを連想配列で返します フィルタ名 ⇒ callable.

public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}

getFunctions(): array

テンプレートがレンダリングされる前に呼び出されます。関数を連想配列で返します 関数名 ⇒ callable.

public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}

getProviders(): array

テンプレートがレンダリングされる前に呼び出されます。プロバイダの配列を返します。プロバイダは通常、ランタイムにタグを使用するオブジェクトです。これらのプロバイダーは、$this->global->... を介してアクセスします。

public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}

getCacheKey (Latte\Engine $engine)mixed

テンプレートがレンダリングされる前に呼び出されます。戻り値はコンパイルされたテンプレートファイルの名前に含まれるハッシュを持つキーの一部になります。したがって、異なる戻り値に対して、Latteは異なるキャッシュファイルを生成します。

Latteはどのように動くのか?

カスタムタグやコンパイラパスの定義方法を理解するためには、Latteがどのように動作しているかを理解することが不可欠です。

Latteのテンプレートコンパイルは簡単に言うと以下のような仕組みになっています。

  • まず、レクサーがテンプレートのソースコードを処理しやすいように小さな断片(トークン)にトークン化します。
  • 次に、パーサがトークンのストリームを意味のあるノードツリー(抽象構文木、AST)に変換します。
  • 最後に、コンパイラはASTからテンプレートをレンダリングするPHPクラスを 生成 して、それをキャッシュします。

実は、コンパイルはもう少し複雑です。Latteは2つ**のレキサとパーサを持っています。1つはHTMLテンプレート用、もう1つはタグの中にあるPHPのようなコード用のレキサです。また、トークン化の後にパーシングが実行されるわけではなく、レキサーとパーサーが2つの「スレッド」で並行して実行され、協調しているのです。ロケットサイエンスですね :-)

さらに、すべてのタグは独自のパーシング ルーチンを持っています。パーサーはタグに遭遇すると、そのパース関数を呼び出します(Extension::getTags()を返します)。 その仕事は、タグの引数と、ペアタグの場合は内部のコンテンツを解析することです。それはASTの一部となるnodeを返します。詳細については、タグ解析関数を参照してください。

パーサーが作業を終えると、テンプレートを表す完全なASTができあがります。ルート・ノードはLatte\Compiler\Nodes\TemplateNode です。ツリー内の個々のノードは、タグだけでなく、HTML要素、その属性、タグの内部で使用されるすべての式などを表します。

この後、いわゆるコンパイラー・パスが登場します。これは、ASTを修正する関数(Extension::getPasses()によって返されます)です。

テンプレートのコンテンツの読み込みから、パース、結果のファイルの生成までの全プロセスは、このコードでシーケンス化することができ、実験して中間結果をダンプすることができます。

$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);

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\Essential\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')
            )
        )
   )
)

カスタムタグ

新しいタグを定義するには、3つのステップが必要です。

- タグのパース関数を定義する (タグをノードにパースする役割を果たす)

タグのパース関数

タグのパース処理は、そのパース関数(Extension::getTags()によって返されるもの)によって処理されます。この関数の仕事は、タグの中にあるすべての引数を解析し、チェックすることです(これを行うにはTagParserを使用します)。 さらに、タグがペアである場合、TemplateParserに依頼して内部のコンテンツを解析して返します。 この関数はノードを生成して返します.ノードは通常,Latte\Compiler\Nodes\StatementNode の子であり,これがASTの一部となります.

各ノードに対してクラスを作成し,パース関数を静的ファクトリとしてその中にエレガントに配置します.例として、おなじみの{foreach} タグを作ってみましょう。

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// a parsing function that just creates a node for now
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// code will be added later
	}

	public function &getIterator(): \Generator
	{
		// code will be added later
	}
}

解析関数create() はオブジェクトLatte\Compiler\Tag に渡され、タグの基本情報(クラシックタグか n:attribute か、どの行にあるかなど)を持ち、主に$tag->parser にあるLatte\Compiler\TagParser にアクセスします。

タグが引数を持たなければならない場合は、$tag->expectArguments() を呼び出して引数の存在をチェックします。$tag->parser オブジェクトのメソッドはそれらをパースするために利用できます。

  • parseExpression(): ExpressionNode PHP 風の式 (例:10 + 3) に対応します。
  • parseUnquotedStringOrExpression(): ExpressionNode 式または引用符で囲まれていない文字列に対して
  • parseArguments(): ArrayNode 配列の内容 (例:10, true, foo => bar)
  • parseModifier(): ModifierNode 修飾子に対して (例:|upper|truncate:10)
  • parseType(): expressionNode typehint 用 (例:int|string またはFoo\Bar[])

と、トークンを直接操作する低レベルのLatte\Compiler\TokenStream があります。

  • $tag->parser->stream->consume(...): Token
  • $tag->parser->stream->tryConsume(...): ?Token

Latte は PHP の構文を少しずつ拡張しています。例えば、修飾子を追加したり、三項演算子を短くしたり、 単純な英数字の文字列を引用符なしで書けるようにしたりしています。これが、PHPの代わりにPHP-likeという言葉を使う理由です。したがって、parseExpression() メソッドはfoo'foo' のようにパースします。 さらに、unquoted-string は、引用符で囲む必要がなく、同時に英数字である必要もない文字列の特殊なケースです。例えば、{include ../file.latte} タグのファイルへのパスがこれにあたります。これをパースするには、parseUnquotedStringOrExpression() メソッドを使用します。

Latteの一部であるノードクラスを勉強することは,パース処理の細かな部分をすべて学ぶのに最適な方法です.

{foreach} タグに戻りましょう。このタグでは、expression + 'as' + second expression という形式の引数を想定して、次のように解析しています。

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;

class ForeachNode extends StatementNode
{
	public ExpressionNode $expression;
	public ExpressionNode $value;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->expression = $tag->parser->parseExpression();
		$tag->parser->stream->consume('as');
		$node->value = $parser->parseExpression();
		return $node;
	}
}

変数$expression$value に書き込んだ式は、サブノードを表しています。

サブノードを持つ変数は public として定義し、必要であれば以降の処理ステップで変更できるようにする。また、トラバースのために利用可能にすることも必要である。

私たちのようなペアのタグの場合、メソッドはTemplateParserにタグの内部コンテンツを解析させなければなりません。これはyield で処理され、[inner content, end tag] というペアが返されます。内部コンテンツを変数$node->content に格納します。

public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}

yield キーワードによってcreate() メソッドが終了し、TemplateParser に制御が戻されます。その後、create() に制御が戻され、中断したところから続行されます。yield, メソッドを使用すると、自動的にGenerator が返されます。

また、yield にタグ名の配列を渡して、終了タグの前に出現した場合にパースを停止させることもできます。これは {foreach}...{else}...{/foreach}の実装に役立ちます。{else} が発生した場合、それ以降の内容を$node->elseContent にパースします。

public AreaNode $content;
public ?AreaNode $elseContent = null;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $nextTag] = yield ['else'];
	if ($nextTag?->name === 'else') {
		[$node->elseContent] = yield;
	}

	return $node;
}

ノードを返すとタグのパースが完了する。

PHPコードの生成

各ノードはprint() メソッドを実装する必要があります。テンプレートの指定された部分をレンダリングするPHPコード(ランタイムコード)を返します。パラメータとしてオブジェクトLatte\Compiler\PrintContext が渡されます。このオブジェクトには、 結果のコードの組み立てを簡略化する便利なメソッドformat() があります。

format(string $mask, ...$args) メソッドは、マスクに以下のプレースホルダーを受け付けます。

  • %node はノードを表示します。
  • %dump PHP に値をエクスポートします。
  • %raw は、テキストを変換せずに直接挿入します。
  • %args は、関数呼び出しの引数として ArrayNode をプリントします。
  • %line は行番号付きのコメントを出力します。
  • %escape(...) 内容をエスケープします。
  • %modify(...) モディファイアを適用する
  • %modifyContent(...) ブロックにモディファイアを適用

print() 関数は次のようになります (簡単のためにelse ブランチは無視します)。

public function print(Latte\Compiler\PrintContext $context): string
{
	return $context->format(
		<<<'XX'
			foreach (%node as %node) %line {
				%node
			}

			XX,
		$this->expression,
		$this->value,
		$this->position,
		$this->content,
	);
}

変数$this->positionLatte\Compiler\Node クラスですでに定義されており、パーサーによって設定されます。この変数には、ソースコード内のタグの位置を行と列の番号で表したLatte\Compiler\Position オブジェクトが含まれます。

ランタイムコードは、補助変数を使用することができます。テンプレート自身が使用する変数との衝突を避けるために、それらの変数の前に$ʟ__ という文字を付けるのが慣例となっています。

また、実行時に任意の値を使用することができ、それらはExtension::getProviders()メソッドを使用してプロバイダの形でテンプレートに渡されます。これらの値には$this->global->... を使ってアクセスします。

ASTのトラバース

ASTツリーを深くトラバースするためには、getIterator() メソッドを実装する必要があります。これによって、サブノードへのアクセスが可能になる。

public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}

getIterator() は参照を返すことに注意。これは、ノードビジターが個々のノードを他のノードに置き換えることができるようにするものである。

ノードがサブノードを持つ場合、このメソッドを実装し、すべてのサブノードを利用できるようにする必要がある。さもなければ、セキュリティホールができてしまう。例えば、サンドボックスモードではサブノードを制御できず、許可されていないコンストラクトがその中で呼び出されないようにすることができない。

子ノードがない場合でも、メソッド本体にyield キーワードが存在しなければならないので、次のように記述する。

public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}

補助ノード.toc-auxiliarynode

Latte用の新しいタグを作成する場合は、ASTツリーでそれを表現する専用のノードクラスを作成することをお勧めします(上記の例のForeachNode クラスを参照)。このクラスでは、print() メソッドのボディと、getIterator() メソッドによってアクセス可能になったノードのリストをコンストラクタのパラメータとして渡すことができます:

// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode

$node = new AuxiliaryNode(
	// body of the print() method:
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// nodes accessed via getIterator() and also passed into the print() method:
	[$argNode],
);

コンパイラのパス

コンパイラパスは、ASTを変更したり、ASTの情報を収集するための関数です。これらはExtension::getPasses()メソッドによって返されます。

ノードトラバーサ

AST を扱う最も一般的な方法は、Latte\Compiler\NodeTraverser を使うことです。

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

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);

enter* 関数 (すなわち visitor) は、ノードが最初に遭遇したとき、そのサブノードが処理される前に呼び出される。leave*関数は、すべてのサブノードが訪問された後に呼び出される。 よくあるパターンは、enterで何らかの情報を収集し、それに基づいてleaveで修正を行うというものである。leave*が呼ばれた時点で、ノード内のすべてのコードはすでに訪問され、必要な情報が収集されている。

ASTを変更するには?最も簡単な方法は、単にノードのプロパティを変更することである。もう一つの方法は、新しいノードを返すことで、ノードを完全に置き換えることである。例:次のコードは,ASTのすべての整数を文字列に変更する(例えば,42は'42' に変更される).

use Latte\Compiler\Nodes\Php;

$ast = (new NodeTraverser)->traverse(
	$ast,
	leave: function (Node $node) {
		if ($node instanceof Php\Scalar\IntegerNode) {
            return new Php\Scalar\StringNode((string) $node->value);
        }
	},
);

ASTは数千のノードを含むことがあり,そのすべてを走査するのは時間がかかる場合がある.場合によっては,完全な探索を避けることも可能である.

ツリー内のすべてのHtml\ElementNode を探す場合、一度Php\ExpressionNode を見てしまうと、その子ノードをすべてチェックする意味がないことがわかります。なぜなら、HTML は式の中に入れることができないからです。この場合、トラバーサーにクラス・ノードに再帰しないように指示することができます。

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);

特定の1つのノードだけを探している場合は、そのノードを見つけた後に探索を完全に中断することも可能です。

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);

ノードヘルパー

クラスLatte\Compiler\NodeHelpers は、特定のコールバックなどを満たす AST ノードを見つけることができるメソッドをいくつか提供します。いくつかの例を示します。

use Latte\Compiler\NodeHelpers;

// finds all HTML element nodes
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// finds first text node
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// converts PHP value node to real value
$value = NodeHelpers::toValue($node);

// converts static textual node to string
$text = NodeHelpers::toText($node);
version: 3.0