Vytváříme Extension

Rozšíření (extension) je znovupoužitelná třída, která může definovat vlastní značky, filtry, funkce, providery a další prvky pro Latte. Vytváříme je, když chceme své úpravy Latte použít v různých projektech nebo je sdílet s komunitou.

Rozšíření je užitečné vytvořit i pro každý webový projekt. Může obsahovat všechny specifické značky a filtry, které chcete v šablonách projektu využívat.

Třída rozšíření

Rozšíření je třída dědící od Latte\Extension. Do Latte se registruje pomocí addExtension() nebo konfiguračním souborem:

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

Pokud zaregistrujete více rozšíření definujících stejně pojmenované tagy, filtry nebo funkce, platí poslední přidané. To znamená, že vaše rozšíření může přepisovat nativní značky/filtry/funkce.

Kdykoliv provedete změnu ve třídě rozšíření a není vypnutý auto-refresh, Latte automaticky překompiluje vaše šablony.

Třída může implementovat kteroukoliv z následujících metod:

abstract class Extension
{
	/**
	 * Inicializace před kompilací šablony.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Vrací seznam parserů pro značky Latte.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Vrací seznam průchodů kompilátoru.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Vrací seznam |filtrů.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Vrací seznam funkcí použitých v šablonách.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Vrací seznam providerů.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Vrací hodnotu pro rozlišení více verzí šablony.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Inicializace před vykreslením šablony.
	 */
	public function beforeRender(Template $template): void;
}

Pro představu, jak rozšíření vypadá, se podívejte na vestavěné CoreExtension.

Nyní si podrobněji rozebereme jednotlivé metody:

beforeCompile (Latte\Engine $engine)void

Tato metoda se volá před kompilací šablony. Můžete ji využít například pro inicializace související s kompilací.

getTags(): array

Volá se při kompilaci šablony. Vrací asociativní pole název tagu ⇒ callable, což jsou parsovací funkce tagu.

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

Značka n:baz představuje ryzí n:atribut, tj. jde o značku, kterou lze zapisovat pouze jako atribut.

Latte automaticky rozpozná, zda jsou značky foo a bar párové. Pokud ano, bude je možné zapisovat i pomocí n:atributů, včetně variant s prefixy n:inner-foo a n:tag-foo.

Pořadí provádění n:atributů je dáno jejich pořadím v poli vráceném getTags(). Tedy n:foo se provede vždy před n:bar, i kdyby byly atributy v HTML značce uvedeny v opačném pořadí.

Pro stanovení pořadí n:atributů napříč více rozšířeními použijte pomocnou metodu order(). Parametry before a after určují, před nebo za kterými značkami se daná značka zařadí:

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

getPasses(): array

Volá se při kompilaci šablony. Vrací asociativní pole název pass ⇒ callable, což jsou funkce představující tzv. průchody kompilátoru, které procházejí a modifikují AST.

Opět je možné využít pomocnou metodu order(). Hodnotou parametrů before nebo after může být '*' s významem před/za všemi.

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

beforeRender (Latte\Engine $engine)void

Volá se před každým vykreslením šablony. Metodu lze využít například pro inicializaci proměnných používaných při vykreslování.

getFilters(): array

Volá se před vykreslením šablony. Vrací filtry jako asociativní pole název filtru ⇒ callable.

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

getFunctions(): array

Volá se před vykreslením šablony. Vrací funkce jako asociativní pole název funkce ⇒ callable.

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

getProviders(): array

Volá se před vykreslením šablony. Vrací pole tzv. providerů, což jsou zpravidla objekty, které za běhu využívají tagy. Přistupují k nim přes $this->global->....

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

getCacheKey (Latte\Engine $engine)mixed

Volá se před vykreslením šablony. Vrácená hodnota se stane součástí klíče, jehož hash je obsažen v názvu souboru se zkompilovanou šablonou. Pro různé vrácené hodnoty tedy Latte vygeneruje různé soubory v cache.

Jak Latte funguje?

Pro pochopení toho, jak definovat vlastní tagy nebo compiler passes, je nezbytné porozumět, jak funguje Latte pod kapotou.

Kompilace šablon v Latte probíhá zjednodušeně takto:

  1. Lexer tokenizuje zdrojový kód šablony na menší části (tokeny) pro snadnější zpracování.
  2. Parser převede proud tokenů na smysluplný strom uzlů (abstraktní syntaktický strom, AST).
  3. Překladač vygeneruje z AST třídu PHP, která vykresluje šablonu, a uloží ji do cache.

Ve skutečnosti je kompilace o něco složitější. Latte má dva lexery a parsery: jeden pro HTML šablonu a druhý pro PHP-like kód uvnitř tagů. A také parsování neprobíhá až po tokenizaci, ale lexer i parser běží paralelně ve dvou „vláknech“ a koordinují se. Je to raketová věda :-)

Každý tag má svou parsovací rutinu. Když parser narazí na tag, zavolá jeho parsovací funkci (vrací je Extension::getTags()). Jejich úkolem je naparsovat argumenty značky a v případě párových značek i vnitřní obsah. Vrací uzel, který se stane součástí AST. Podrobně v části Parsovací funkce tagu.

Po dokončení parsování máme kompletní AST reprezentující šablonu. Kořenovým uzlem je Latte\Compiler\Nodes\TemplateNode. Jednotlivé uzly uvnitř stromu reprezentují nejen tagy, ale i HTML elementy, jejich atributy, všechny výrazy použité uvnitř značek atd.

Poté přicházejí na řadu tzv. Průchody kompilátoru, což jsou funkce (vrací je Extension::getPasses()), které modifikují AST.

Celý proces od načtení obsahu šablony přes parsování až po vygenerování výsledného souboru můžete sekvenčně vykonat tímto kódem:

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

Příklad AST

Pro lepší představu o podobě AST přidáváme ukázku. Toto je zdrojová šablona:

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

A toto její reprezentace v podobě 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')
            )
        )
   )
)

Vlastní tagy

K definování nové značky jsou zapotřebí tři kroky:

  1. Definování parsovací funkce tagu (zodpovědná za parsování tagu do uzlu)
  2. Vytvoření třídy uzlu (zodpovědné za generování PHP kódu a procházení AST)
  3. Registrace tagu pomocí Extension::getTags()

Parsovací funkce tagu

Parsování tagů má na starosti jeho parsovací funkce (ta, kterou vrací Extension::getTags()). Jejím úkolem je:

  1. Naparsovat a zkontrolovat případné argumenty uvnitř značky (k tomu využívá TagParser).
  2. Pokud je značka párová, požádat TemplateParser o naparsování a vrácení vnitřního obsahu.
  3. Vytvořit a vrátit uzel, který je zpravidla potomkem Latte\Compiler\Nodes\StatementNode, a který se stane součástí AST.

Pro každý uzel vytváříme třídu a parsovací funkci do ní umístíme jako statickou továrnu. Jako příklad si vytvoříme známý tag {foreach}:

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// parsovací funkce, která zatím pouze vytváří uzel
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// kód doplníme později
	}

	public function &getIterator(): \Generator
	{
		// kód doplníme později
	}
}

Parsovací funkci create() se předává objekt Latte\Compiler\Tag, který nese základní informace o tagu (jestli jde o klasický tag nebo n:attribut, na jakém řádku se nachází, apod.) a hlavně zpřístupňuje Latte\Compiler\TagParser v $tag->parser.

Pokud značka musí mít argumenty, zkontrolujeme jejich existenci zavoláním $tag->expectArguments(). Pro jejich parsování jsou k dispozici metody objektu $tag->parser:

  • parseExpression(): ExpressionNode pro PHP-like výraz (např. 10 + 3)
  • parseUnquotedStringOrExpression(): ExpressionNode pro výraz nebo unquoted-řetězec
  • parseArguments(): ArrayNode obsah pole (např. 10, true, foo => bar)
  • parseModifier(): ModifierNode pro modifikátor (např |upper|truncate:10)
  • parseType(): ExpressionNode pro typehint (např. int|string nebo Foo\Bar[])

a dále nízkoúrovňový Latte\Compiler\TokenStream operující přímo s tokeny:

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

Latte mírně rozšiřuje syntaxi PHP, například o modifikátory, zkrácené ternání operátory, nebo umožňuje jednoduché alfanumerické řetězce psát bez uvozovek. Proto používáme termín PHP-like místo PHP. Metoda parseExpression() tedy naparsuje např. foo jako 'foo'. Vedle toho unquoted-řetězec je speciálním případem řetězce, který také nemusí být v uvozovkách, ale zároveň nemusí být ani alfanumerický. Jde třeba o cestu k souboru ve značce {include ../file.latte}. K jeho naparsování slouží metoda parseUnquotedStringOrExpression().

Studium tříd uzlů, které jsou součástí Latte, je nejlepší způsob, jak se naučit všechny podrobnosti o procesu parsování.

Vraťmě se ke značce {foreach}. V ní očekáváme argumenty ve tvaru výraz + 'as' + druhý výraz a naparsujeme je následujícícm způsobem:

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;
	}
}

Výrazy, které jsme zapsali do proměnných $expression a $value, představují poduzly.

Proměnné s poduzly definujte jako public, aby je bylo možné případně modifikovat v dalších krocích zpracování. Zároveň je nutné je zpřístupnit pro procházení.

U párových značek, jako je ta naše, musí metoda ještě nechat TemplateParser naparsovat její vnitřek. Tohle obstará yield, který vrací dvojici [vnitří obsah, koncová značka]. Vnitřní obsah uložíme do proměnné $node->content.

public AreaNode $content;

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

Klíčové slovo yield způsobí, že se metoda create() přeruší, řízení se vrátí zpátky k TemplateParser, který pokračuje v parsování obsahu dokud nenarazí na koncovou značku. Poté předá řízení zpět do create(), která pokračuje od místa, kde skončila. Užitím yield metoda automaticky vrací Generator.

Do yield lze také předat pole názvů značek, u kterých chceme parsování zastavit, pokud se vyskytnou dříve než koncová značka. To nám pomůže implemenotovat konstrukci {foreach}...{else}...{/foreach}. Pokud se objeví {else}, obsah za ní naparsujeme do $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;
}

Vrácením uzlu $node je parsování tagu dokončeno.

Generování PHP kódu

Každý uzel musí implementovat metodu print(). Vrací PHP kód vykreslující danou část šablony (runtime kód). Jako parametr se jí předává objekt Latte\Compiler\PrintContext, který má užitečnou metodu format() zjednodušující sestavení výsledného kódu.

Metoda format(string $mask, ...$args) akceptuje v masce tyto placeholdery:

  • %node vypisuje Node
  • %dump vyexportuje hodnotu do PHP
  • %raw vloží přímo text bez jakékoliv transformace
  • %args vypíše ArrayNode jako argumenty volání funkce
  • %line vypíše komentář s číslem řádku
  • %escape(...) escapuje obsah
  • %modify(...) aplikuje modifikátor
  • %modifyContent(...) aplikuje modifikátor pro bloky

Naše funkce print() by mohla vypadat takto (pro jednoduchost zanedbáváme else větev):

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,
	);
}

Proměnnou $this->position definuje už třída Latte\Compiler\Node a nastavuje ji parser. Obsahuje objekt Latte\Compiler\Position s pozicí tagu ve zdrojovém kódu v podobě čísla řádku a sloupce.

Runtime kód může využívat pomocné proměnné. Aby nedošlo ke kolizi s proměnnými, které používá samotná šablona, je zvykem je prefixovat znaky $ʟ__.

Může také za běhu využívat libovolné hodnoty, které si do šablony předá v podobě tzv. providerů metodou Extension::getProviders(). K nim přistupuje pomocí $this->global->....

Procházení AST

Aby bylo možné AST strom procházet do hloubky, je nutné implementovat metodu getIterator(). Ta zpřístupní poduzly:

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

Všimněte si, že getIterator() vrací reference. Právě díky tomu mohou node visitors jednotlivé uzly měnit za jiné.

Pokud má uzel poduzly, je nezbytné tuto metodu implementovat a všechny poduzly zpřístupnit. Jinak by mohla vzniknout bezpečnostní díra. Například režim sandboxu by nebyl schopen kontrolovat poduzly a zajistit, aby v nich nebyly volány nepovolené konstrukce.

Pokud uzel nemá žádné poduzly, implementujte metodu takto:

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

AuxiliaryNode

Pokud vytváříte nový tag pro Latte, je žádoucí, abyste pro něj vytvořili vlastní třídu uzlu, která jej bude reprezentovat v AST stromu (viz třída ForeachNode v příkladu výše). V některých případech se vám může hodit pomocná triviální třída uzlu AuxiliaryNode, které tělo metody print() a seznam uzlů, které zpřístupňuje metoda getIterator(), předáme jako parametry konstruktoru:

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

$node = new AuxiliaryNode(
	// tělo metody print():
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// uzly zpřístupněné přes getIterator() a také předané do metody print():
	[$argNode],
);

Průchody kompilátoru

Průchody kompilátoru jsou funkce, které modifikují AST nebo sbírají v nich informace. Vrací je metoda Extension::getPasses().

Node Traverser

Nejběžnějším způsobem práce s AST je použití Latte\Compiler\NodeTraverser:

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

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

Funkce enter (tj. node visitor) je volána při prvním setkání s uzlem, ještě před zpracováním jeho poduzlů. Funkce leave je volána po návštěvě všech poduzlů. Běžným postupem je, že funkce enter se používá ke shromáždění některých informací a poté funkce leave na jejich základě provede úpravy. V době, kdy je volána funkce leave, bude již veškerý kód uvnitř uzlu navštíven a potřebné informace shromážděny.

Jak AST modifikovat? Nejjednodušším způsobem je jednoduše měnit vlastnosti uzlů. Druhým způsobem je uzel zcela nahradit vrácením uzlu nového. Příklad: následující kód změní všechna celá čísla v AST na řetězce (např. 42 se změní na '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 může snadno obsahovat tisíce uzlů a procházení všech uzlů může být časově náročné. V některých případech je možné se úplnému procházení vyhnout.

Pokud ve stromu hledáte všechny uzly Html\ElementNode, pak víte, že jakmile jednou uvidíte uzel Php\ExpressionNode, nemá smysl kontrolovat také všechny jeho podřízené uzly, protože HTML nemůže být uvnitř výrazů. V takovém případě můžete traverseru přikázat, aby do uzlu třídy neprováděl rekurzi:

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

Pokud hledáte pouze jeden konkrétní uzel, je také možné po jeho nalezení procházení zcela přerušit.

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

Pomocníci pro uzly

Třída Latte\Compiler\NodeHelpers poskytuje některé metody, které mohou najít uzly AST, které splňují určitou podmínku atd. Několik příkladů:

use Latte\Compiler\NodeHelpers;

// najde všechny uzly prvků HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// najde první textový uzel
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// převede uzel PHP na skutečnou hodnotu
$value = NodeHelpers::toValue($node);

// převede statický textový uzel na řetězec
$text = NodeHelpers::toText($node);

Tímto způsobem můžete efektivně procházet a manipulovat s AST stromem ve vašich rozšířeních pro Latte.

verze: 3.0