Criando tags personalizadas
Esta página fornece um guia abrangente para criar tags personalizadas no Latte. Discutiremos tudo, desde tags simples até cenários mais complexos com conteúdo aninhado e necessidades específicas de parsing, com base na sua compreensão de como o Latte compila templates.
Tags personalizadas fornecem o mais alto nível de controle sobre a sintaxe do template e a lógica de renderização, mas também são o ponto de extensão mais complexo. Antes de decidir criar uma tag personalizada, sempre considere se não existe uma solução mais simples ou se uma tag adequada já existe no conjunto padrão. Use tags personalizadas apenas quando alternativas mais simples não forem suficientes para suas necessidades.
Compreendendo o processo de compilação
Para criar tags personalizadas de forma eficaz, é útil explicar como o Latte processa templates. Compreender este processo esclarece por que as tags são estruturadas dessa forma e como elas se encaixam no contexto mais amplo.
A compilação de um template no Latte, de forma simplificada, envolve estes passos principais:
- Análise Léxica: O lexer lê o código-fonte do template (arquivo
.latte
) e o divide em uma sequência de pequenas partes distintas chamadas tokens (por exemplo,{
,foreach
,$variable
,}
, texto HTML, etc.). - Parsing: O parser pega esse fluxo de tokens e constrói a partir dele uma estrutura de árvore significativa que representa a lógica e o conteúdo do template. Essa árvore é chamada de árvore de sintaxe abstrata (AST).
- Passos de Compilação: Antes de gerar o código PHP, o Latte executa passos de compilação. São funções que percorrem toda a AST e podem modificá-la ou coletar informações. Este passo é crucial para funcionalidades como segurança (Sandbox) ou otimização.
- Geração de Código: Finalmente, o compilador percorre a AST (potencialmente modificada) e gera o código da classe PHP correspondente. Este código PHP é o que realmente renderiza o template durante a execução.
- Caching: O código PHP gerado é armazenado em disco, o que torna as renderizações subsequentes muito rápidas, pois os passos 1–4 são pulados.
Na realidade, a compilação é um pouco mais complexa. O Latte tem dois lexers e parsers: um para o template HTML e outro para o código tipo PHP dentro das tags. E também o parsing não ocorre após a tokenização, mas o lexer e o parser rodam em paralelo em duas “threads” e se coordenam. Acredite, programar isso foi ciência de foguetes :-)
Todo o processo, desde o carregamento do conteúdo do template, passando pelo parsing, até a geração do arquivo final, pode ser sequenciado com este código, com o qual você pode experimentar e exibir resultados intermediários:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
A anatomia de uma tag
Criar uma tag personalizada totalmente funcional no Latte envolve várias partes interconectadas. Antes de mergulharmos na implementação, vamos entender os conceitos básicos e a terminologia, usando uma analogia com HTML e o Document Object Model (DOM).
Tags vs. Nós (Analogia com HTML)
Em HTML, escrevemos tags como <p>
ou <div>...</div>
. Essas tags são a
sintaxe no código-fonte. Quando o navegador analisa este HTML, ele cria uma representação na memória chamada Document
Object Model (DOM). No DOM, as tags HTML são representadas por nós (especificamente, nós Element
na
terminologia do DOM JavaScript). Trabalhamos programaticamente com esses nós (por exemplo, usando
document.getElementById(...)
do JavaScript, que retorna um nó Element). A tag é apenas uma representação textual
no arquivo de origem; o nó é uma representação de objeto na árvore lógica.
O Latte funciona de forma semelhante:
- No arquivo de template
.latte
, você escreve tags Latte, como{foreach ...}
e{/foreach}
. Esta é a sintaxe com a qual você, como autor do template, trabalha. - Quando o Latte analisa (parses) o template, ele constrói uma Árvore de Sintaxe Abstrata (AST). Esta árvore é composta por nós. Cada tag Latte, elemento HTML, pedaço de texto ou expressão no template se torna um ou mais nós nesta árvore.
- A classe base para todos os nós na AST é
Latte\Compiler\Node
. Assim como o DOM tem diferentes tipos de nós (Element, Text, Comment), a AST do Latte tem diferentes tipos de nós. Você encontraráLatte\Compiler\Nodes\TextNode
para texto estático,Latte\Compiler\Nodes\Html\ElementNode
para elementos HTML,Latte\Compiler\Nodes\Php\ExpressionNode
para expressões dentro de tags e, crucialmente para tags personalizadas, nós que herdam deLatte\Compiler\Nodes\StatementNode
.
Por que StatementNode
?
Elementos HTML (Html\ElementNode
) representam principalmente estrutura e conteúdo. Expressões PHP
(Php\ExpressionNode
) representam valores ou cálculos. Mas e as tags Latte como {if}
,
{foreach}
ou nossa própria {datetime}
? Essas tags executam ações, controlam o fluxo do
programa ou geram saída com base na lógica. São unidades funcionais que tornam o Latte um poderoso engine de templates,
não apenas uma linguagem de marcação.
Na programação, essas unidades que executam ações são frequentemente chamadas de “statements” (instruções).
Portanto, os nós que representam essas tags Latte funcionais normalmente herdam de
Latte\Compiler\Nodes\StatementNode
. Isso os distingue de nós puramente estruturais (como elementos HTML) ou nós que
representam valores (como expressões).
Os componentes chave
Vamos percorrer os principais componentes necessários para criar uma tag personalizada:
Função de parsing da tag
- Esta função PHP callable analisa a sintaxe da tag Latte (
{...}
) no template de origem. - Recebe informações sobre a tag (como seu nome, posição e se é um n:attribute) através do objeto Latte\Compiler\Tag.
- Sua ferramenta principal para analisar argumentos e expressões dentro dos delimitadores da tag é o objeto Latte\Compiler\TagParser, acessível via
$tag->parser
(este é um parser diferente daquele que analisa todo o template). - Para tags de par, usa
yield
para sinalizar ao Latte para analisar o conteúdo interno entre as tags de abertura e fechamento. - O objetivo final da função de parsing é criar e retornar uma instância da classe do nó, que é adicionada à AST.
- É costume (embora não obrigatório) implementar a função de parsing como um método estático (frequentemente chamado
create
) diretamente na classe do nó correspondente. Isso mantém a lógica de parsing e a representação do nó organizadas em um único pacote, permite o acesso a elementos privados/protegidos da classe, se necessário, e melhora a organização.
Classe do nó
- Representa a função lógica da sua tag na Árvore de Sintaxe Abstrata (AST).
- Contém informações analisadas (como argumentos ou conteúdo) como propriedades públicas. Essas propriedades frequentemente
contêm outras instâncias de
Node
(por exemplo,ExpressionNode
para argumentos analisados,AreaNode
para conteúdo analisado). - O método
print(PrintContext $context): string
gera o código PHP (uma instrução ou série de instruções) que executa a ação da tag durante a renderização do template. - O método
getIterator(): \Generator
expõe os nós filhos (argumentos, conteúdo) para travessia pelos passos de compilação. Deve fornecer referências (&
) para permitir que os passos potencialmente modifiquem ou substituam subnós. - Depois que todo o template é analisado na AST, o Latte executa uma série de passos de compilação. Esses passos percorrem toda a AST usando
o método
getIterator()
fornecido por cada nó. Eles podem inspecionar nós, coletar informações e até modificar a árvore (por exemplo, alterando propriedades públicas de nós ou substituindo nós inteiramente). Este design, que requer umgetIterator()
abrangente, é crucial. Ele permite que funcionalidades poderosas como Sandbox analisem e potencialmente alterem o comportamento de qualquer parte do template, incluindo suas próprias tags personalizadas, garantindo segurança e consistência.
Registro via extensão
- Você precisa informar o Latte sobre sua nova tag e qual função de parsing deve ser usada para ela. Isso é feito dentro de uma extensão Latte.
- Dentro da sua classe de extensão, você implementa o método
getTags(): array
. Este método retorna um array associativo onde as chaves são os nomes das tags (por exemplo,'mytag'
,'n:myattribute'
) e os valores são funções PHP callable representando suas respectivas funções de parsing (por exemplo,MyNamespace\DatetimeNode::create(...)
).
Resumo: A função de parsing da tag transforma o código-fonte do template da sua tag em um nó AST.
A classe do nó pode então transformar a si mesma em código PHP executável para o template compilado
e expõe seus subnós para os passos de compilação via getIterator()
. O registro via extensão
conecta o nome da tag à função de parsing e informa o Latte sobre ela.
Agora, vamos explorar como implementar esses componentes passo a passo.
Criando uma tag simples
Vamos começar a criar sua primeira tag Latte personalizada. Começaremos com um exemplo muito simples: uma tag chamada
{datetime}
que exibe a data e hora atuais. Inicialmente, esta tag não aceitará nenhum argumento, mas a
aprimoraremos posteriormente na seção “Parsing de argumentos da tag”. Ela também
não tem conteúdo interno.
Este exemplo o guiará pelos passos básicos: definir a classe do nó, implementar seus métodos print()
e
getIterator()
, criar a função de parsing e, finalmente, registrar a tag.
Objetivo: Implementar {datetime}
para exibir a data e hora atuais usando a função PHP
date()
.
Criação da classe do nó
Primeiro, precisamos de uma classe que represente nossa tag na Árvore de Sintaxe Abstrata (AST). Conforme discutido acima,
herdamos de Latte\Compiler\Nodes\StatementNode
.
Crie um arquivo (por exemplo, DatetimeNode.php
) e defina a classe:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Função de parsing da tag, chamada quando {datetime} é encontrado.
*/
public static function create(Tag $tag): self
{
// Nossa tag simples atualmente não aceita argumentos, então não precisamos analisar nada
$node = $tag->node = new self;
return $node;
}
/**
* Gera o código PHP que será executado durante a renderização do template.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Fornece acesso aos nós filhos para os passos de compilação do Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Quando o Latte encontra {datetime}
em um template, ele chama a função de parsing create()
. Sua
tarefa é retornar uma instância de DatetimeNode
.
O método print()
gera o código PHP que será executado durante a renderização do template. Chamamos
o método $context->format()
, que monta a string final do código PHP para o template compilado. O primeiro
argumento, 'echo date('Y-m-d H:i:s') %line;'
, é uma máscara na qual os parâmetros seguintes são inseridos.
O placeholder %line
diz ao método format()
para usar o segundo argumento, que é
$this->position
, e inserir um comentário como /* line 15 */
, que conecta o código PHP gerado de
volta à linha original do template, o que é crucial para a depuração.
A propriedade $this->position
é herdada da classe base Node
e é definida automaticamente pelo
parser do Latte. Ela contém um objeto Latte\Compiler\Position que indica onde a tag foi
encontrada no arquivo de origem .latte
.
O método getIterator()
é essencial para os passos de compilação. Ele deve fornecer todos os nós filhos, mas
nosso DatetimeNode
simples atualmente não tem argumentos nem conteúdo, portanto, nenhum nó filho. No entanto,
o método ainda deve existir e ser um gerador, ou seja, a palavra-chave yield
deve estar presente de alguma forma no
corpo do método.
Registro via extensão
Finalmente, vamos informar o Latte sobre a nova tag. Crie uma classe de extensão (por exemplo,
MyLatteExtension.php
) e registre a tag em seu método getTags()
.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Retorna a lista de tags fornecidas por esta extensão.
* @return array<string, callable> Mapa: 'nome-da-tag' => funcao-de-parsing
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Registre mais tags aqui posteriormente
];
}
}
Em seguida, registre esta extensão no Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Crie um template:
<p>Página gerada em: {datetime}</p>
Saída esperada: <p>Página gerada em: 2023-10-27 11:00:00</p>
Resumo desta fase
Criamos com sucesso uma tag personalizada básica {datetime}
. Definimos sua representação na AST
(DatetimeNode
), tratamos seu parsing (create()
), especificamos como ela deve gerar código PHP
(print()
), garantimos que seus filhos sejam acessíveis para travessia (getIterator()
) e a registramos
no Latte.
Na próxima seção, aprimoraremos esta tag para aceitar argumentos e demonstraremos como analisar expressões e gerenciar nós filhos.
Parsing de argumentos da tag
Nossa tag simples {datetime}
funciona, mas não é muito flexível. Vamos aprimorá-la para aceitar um argumento
opcional: uma string de formatação para a função date()
. A sintaxe desejada será
{datetime $format}
.
Objetivo: Modificar {datetime}
para aceitar uma expressão PHP opcional como argumento, que será usada
como string de formatação para date()
.
Apresentando o TagParser
Antes de modificarmos o código, é importante entender a ferramenta que usaremos: Latte\Compiler\TagParser. Quando o parser principal
do Latte (TemplateParser
) encontra uma tag Latte como {datetime ...}
ou um n:attribute, ele delega
o parsing do conteúdo dentro da tag (a parte entre {
e }
ou o valor do atributo) para um
TagParser
especializado.
Este TagParser
trabalha exclusivamente com os argumentos da tag. Sua tarefa é processar os tokens que
representam esses argumentos. Crucialmente, ele deve processar todo o conteúdo que lhe é fornecido. Se sua função de
parsing terminar, mas o TagParser
não tiver alcançado o final dos argumentos (verificado via
$tag->parser->isEnd()
), o Latte lançará uma exceção, pois isso indica que tokens inesperados foram
deixados dentro da tag. Por outro lado, se a tag requer argumentos, você deve chamar
$tag->expectArguments()
no início da sua função de parsing. Este método verifica se os argumentos estão
presentes e lança uma exceção útil se a tag foi usada sem nenhum argumento.
O TagParser
oferece métodos úteis para analisar diferentes tipos de argumentos:
parseExpression(): ExpressionNode
: Analisa uma expressão semelhante a PHP (variáveis, literais, operadores, chamadas de função/método, etc.). Lida com o açúcar sintático do Latte, como tratar strings alfanuméricas simples como strings entre aspas (por exemplo,foo
é analisado como se fosse'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Analisa uma expressão padrão ou uma string sem aspas. Strings sem aspas são sequências permitidas pelo Latte sem aspas, frequentemente usadas para coisas como caminhos de arquivo (por exemplo,{include ../file.latte}
). Se analisar uma string sem aspas, retorna umStringNode
.parseArguments(): ArrayNode
: Analisa argumentos separados por vírgula, potencialmente com chaves, como10, name: 'John', true
.parseModifier(): ModifierNode
: Analisa filtros como|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Analisa dicas de tipo PHP comoint
,?string
,array|Foo
.
Para necessidades de parsing mais complexas ou de nível inferior, você pode interagir diretamente com o fluxo de tokens via
$tag->parser->stream
. Este objeto fornece métodos para inspecionar e consumir tokens individuais:
$tag->parser->stream->is(...): bool
: Verifica se o token atual corresponde a algum dos tipos especificados (por exemplo,Token::Php_Variable
) ou valores literais (por exemplo,'as'
) sem consumi-lo. Útil para olhar à frente.$tag->parser->stream->consume(...): Token
: Consome o token atual e avança a posição do fluxo. Se tipos/valores de token esperados forem fornecidos como argumentos e o token atual não corresponder, lançaCompileException
. Use isso quando você espera um determinado token.$tag->parser->stream->tryConsume(...): ?Token
: Tenta consumir o token atual apenas se ele corresponder a um dos tipos/valores especificados. Se corresponder, consome o token e o retorna. Se não corresponder, deixa a posição do fluxo inalterada e retornanull
. Use isso para tokens opcionais ou ao escolher entre diferentes caminhos sintáticos.
Atualizando a função de parsing create()
Com esse entendimento, vamos modificar o método create()
em DatetimeNode
para analisar o argumento
de formato opcional usando $tag->parser
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
// Adicionamos uma propriedade pública para armazenar o nó da expressão de formato analisado
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Verificamos se existem alguns tokens
if (!$tag->parser->isEnd()) {
// Analisamos o argumento como uma expressão semelhante a PHP usando TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... os métodos print() e getIterator() serão atualizados a seguir ...
}
Adicionamos uma propriedade pública $format
. Em create()
, agora usamos
$tag->parser->isEnd()
para verificar se existem argumentos. Se sim,
$tag->parser->parseExpression()
processa os tokens para a expressão. Como o TagParser
deve
processar todos os tokens de entrada, o Latte lançará automaticamente um erro se o usuário escrever algo inesperado após a
expressão de formato (por exemplo, {datetime 'Y-m-d', unexpected}
).
Atualizando o método print()
Agora, vamos modificar o método print()
para usar a expressão de formato analisada armazenada em
$this->format
. Se nenhum formato foi fornecido ($this->format
é null
), devemos usar
uma string de formatação padrão, por exemplo, 'Y-m-d H:i:s'
.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node imprime a representação do código PHP de $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
Na variável $formatNode
, armazenamos o nó AST que representa a string de formatação para a função PHP
date()
. Usamos o operador de coalescência nula (??
) aqui. Se o usuário forneceu um argumento no
template (por exemplo, {datetime 'd.m.Y'}
), então a propriedade $this->format
contém o nó
correspondente (neste caso, um StringNode
com o valor 'd.m.Y'
), e este nó é usado. Se o usuário
não forneceu um argumento (escreveu apenas {datetime}
), a propriedade $this->format
é
null
, e em vez disso criamos um novo StringNode
com o formato padrão 'Y-m-d H:i:s'
. Isso
garante que $formatNode
sempre contenha um nó AST válido para o formato.
Na máscara 'echo date(%node) %line;'
, um novo placeholder %node
é usado, que diz ao método
format()
para pegar o primeiro argumento seguinte (que é nosso $formatNode
), chamar seu método
print()
(que retornará sua representação de código PHP) e inserir o resultado na posição do placeholder.
Implementando getIterator()
para subnós
Nosso DatetimeNode
agora tem um nó filho: a expressão $format
. Devemos expor este nó filho
aos passos de compilação, fornecendo-o no método getIterator()
. Lembre-se de fornecer uma referência
(&
) para permitir que os passos potencialmente substituam o nó.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Por que isso é crucial? Imagine um passo Sandbox que precisa verificar se o argumento $format
não contém uma
chamada de função proibida (por exemplo, {datetime dangerousFunction()}
). Se getIterator()
não
fornecer $this->format
, o passo Sandbox nunca veria a chamada dangerousFunction()
dentro do
argumento da nossa tag, criando uma potencial falha de segurança. Ao fornecê-lo, permitimos que o Sandbox (e outros passos)
inspecionem e potencialmente modifiquem o nó da expressão $format
.
Usando a tag aprimorada
A tag agora lida corretamente com o argumento opcional:
Formato padrão: {datetime}
Formato personalizado: {datetime 'd.m.Y'}
Usando variável: {datetime $userDateFormatPreference}
{* Isso causaria um erro após analisar 'd.m.Y', pois ", foo" é inesperado *}
{* {datetime 'd.m.Y', foo} *}
A seguir, veremos a criação de tags de par, que processam o conteúdo entre elas.
Tratando tags de par
Até agora, nossa tag {datetime}
era auto-fechada (conceitualmente). Ela não tinha conteúdo entre as tags
de abertura e fechamento. No entanto, muitas tags úteis operam em um bloco de conteúdo de template. Estas são chamadas de
tags de par. Exemplos incluem {if}...{/if}
, {block}...{/block}
ou uma tag personalizada que
criaremos agora: {debug}...{/debug}
.
Esta tag nos permitirá incluir informações de depuração em nossos templates que devem ser visíveis apenas durante o desenvolvimento.
Objetivo: Criar uma tag de par {debug}
cujo conteúdo é renderizado apenas quando um sinalizador
específico de “modo de desenvolvimento” está ativo.
Apresentando os providers
Às vezes, suas tags precisam acessar dados ou serviços que não são passados diretamente como parâmetros de template. Por exemplo, determinar se a aplicação está em modo de desenvolvimento, acessar o objeto do usuário ou obter valores de configuração. O Latte fornece um mecanismo chamado provedores (Providers) para este propósito.
Os provedores são registrados em sua extensão
usando o método getProviders()
. Este método retorna um array associativo onde as chaves são os nomes pelos quais
os provedores serão acessíveis no código de tempo de execução do template, e os valores são os dados ou objetos reais.
Dentro do código PHP gerado pelo método print()
da sua tag, você pode acessar esses provedores através de uma
propriedade especial do objeto $this->global
. Como esta propriedade é compartilhada entre todas as extensões, é
uma boa prática prefixar os nomes dos seus provedores para evitar possíveis conflitos de nomes com provedores principais
do Latte ou provedores de outras extensões de terceiros. Uma convenção comum é usar um prefixo curto e único relacionado ao
seu fornecedor ou nome da extensão. Para nosso exemplo, usaremos o prefixo app
e o sinalizador de modo de
desenvolvimento estará disponível como $this->global->appDevMode
.
A palavra-chave yield
para parsing de conteúdo
Como dizemos ao parser do Latte para processar o conteúdo entre {debug}
e {/debug}
? É aqui
que entra a palavra-chave yield
.
Quando yield
é usado na função create()
, a função se torna um gerador PHP. Sua execução é pausada e o controle
retorna ao TemplateParser
principal. O TemplateParser
então continua a analisar o conteúdo do
template até encontrar a tag de fechamento correspondente ({/debug}
em nosso caso).
Assim que a tag de fechamento é encontrada, o TemplateParser
retoma a execução da nossa função
create()
logo após a instrução yield
. O valor retornado pela instrução yield
é um array contendo dois elementos:
- Um
AreaNode
representando o conteúdo analisado entre as tags de abertura e fechamento. - Um objeto
Tag
representando a tag de fechamento (por exemplo,{/debug}
).
Vamos criar a classe DebugNode
e seu método create
utilizando yield
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Propriedade pública para armazenar o conteúdo interno analisado
public AreaNode $content;
/**
* Função de parsing para a tag de par {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // observe o tipo de retorno
{
$node = $tag->node = new self;
// Pausar o parsing, obter o conteúdo interno e a tag final quando {/debug} for encontrado
[$node->content, $endTag] = yield;
return $node;
}
// ... print() e getIterator() serão implementados a seguir ...
}
Nota: $endTag
é null
se a tag for usada como um n:attribute, ou seja,
<div n:debug>...</div>
.
Implementando print()
para renderização condicional
O método print()
agora precisa gerar código PHP que, em tempo de execução, verifique o provedor
appDevMode
e execute o código para o conteúdo interno apenas se o sinalizador for verdadeiro.
public function print(PrintContext $context): string
{
// Gera uma instrução PHP 'if' que verifica o provedor em tempo de execução
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Se estiver em modo de desenvolvimento, imprime o conteúdo interno
%node
}
XX,
$this->position, // Para o comentário %line
$this->content, // O nó contendo a AST do conteúdo interno
);
}
Isso é simples. Usamos PrintContext::format()
para criar uma instrução PHP if
padrão. Dentro do
if
, colocamos o placeholder %node
para $this->content
. O Latte chamará recursivamente
$this->content->print($context)
para gerar o código PHP para a parte interna da tag, mas apenas se
$this->global->appDevMode
avaliar como verdadeiro em tempo de execução.
Implementando getIterator()
para o conteúdo
Assim como com o nó de argumento no exemplo anterior, nosso DebugNode
agora tem um nó filho:
AreaNode $content
. Precisamos expô-lo fornecendo-o em getIterator()
:
public function &getIterator(): \Generator
{
// Fornece uma referência ao nó de conteúdo
yield $this->content;
}
Isso permite que os passos de compilação desçam para o conteúdo da nossa tag {debug}
, o que é importante
mesmo que o conteúdo seja renderizado condicionalmente. Por exemplo, o Sandbox precisa analisar o conteúdo independentemente
de appDevMode
ser verdadeiro ou falso.
Registro e uso
Registre a tag e o provedor em sua extensão:
class MyLatteExtension extends Extension
{
// Assumimos que $isDevelopmentMode é determinado em algum lugar (por exemplo, da configuração)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Registro da nova tag
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Registro do provedor
];
}
}
// Ao registrar a extensão:
$isDev = true; // Determine isso com base no ambiente da sua aplicação
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
E seu uso no template:
<p>Conteúdo normal visível sempre.</p>
{debug}
<div class="debug-panel">
ID do usuário atual: {$user->id}
Hora da requisição: {=time()}
</div>
{/debug}
<p>Outro conteúdo normal.</p>
Integração de n:attributes
O Latte oferece uma notação abreviada conveniente para muitas tags de par: n:attributes. Se você tem uma tag de par como
{tag}...{/tag}
e deseja que seu efeito seja aplicado diretamente a um único elemento HTML, muitas vezes pode
escrevê-la de forma mais concisa como um atributo n:tag
nesse elemento.
Para a maioria das tags de par padrão que você define (como nossa {debug}
), o Latte habilitará automaticamente
a versão do atributo n:
correspondente. Você não precisa fazer nada extra durante o registro:
{* Uso padrão da tag de par *}
{debug}<div>Informação de depuração</div>{/debug}
{* Uso equivalente com n:attribute *}
<div n:debug>Informação de depuração</div>
Ambas as versões renderizarão o <div>
apenas se $this->global->appDevMode
for
verdadeiro. Os prefixos inner-
e tag-
também funcionam como esperado.
Às vezes, a lógica da sua tag pode precisar se comportar de maneira ligeiramente diferente dependendo se ela é usada como
uma tag de par padrão ou como um n:attribute, ou se um prefixo como n:inner-tag
ou n:tag-tag
é usado.
O objeto Latte\Compiler\Tag
, passado para sua função de parsing create()
, fornece essas
informações:
$tag->isNAttribute(): bool
: Retornatrue
se a tag estiver sendo analisada como um n:attribute$tag->prefix: ?string
: Retorna o prefixo usado com o n:attribute, que pode sernull
(não é um n:attribute),Tag::PrefixNone
,Tag::PrefixInner
ouTag::PrefixTag
Agora que entendemos tags simples, parsing de argumentos, tags de par, provedores e n:attributes, vamos abordar um cenário
mais complexo envolvendo tags aninhadas dentro de outras tags, usando nossa tag {debug}
como ponto de partida.
Tags intermediárias
Algumas tags de par permitem ou até exigem que outras tags apareçam dentro delas antes da tag de fechamento final.
Estas são chamadas de tags intermediárias. Exemplos clássicos incluem {if}...{elseif}...{else}...{/if}
ou
{switch}...{case}...{default}...{/switch}
.
Vamos estender nossa tag {debug}
para suportar uma cláusula opcional {else}
, que será renderizada
quando a aplicação não estiver em modo de desenvolvimento.
Objetivo: Modificar {debug}
para suportar uma tag intermediária opcional {else}
. A sintaxe
final deve ser {debug} ... {else} ... {/debug}
.
Parsing de tags intermediárias com yield
Já sabemos que yield
pausa a função de parsing create()
e retorna o conteúdo analisado junto com
a tag final. No entanto, yield
oferece mais controle: você pode fornecer a ele um array de nomes de tags
intermediárias. Quando o parser encontra qualquer uma dessas tags especificadas no mesmo nível de aninhamento (ou
seja, como filhos diretos da tag pai, não dentro de outros blocos ou tags dentro dela), ele também para o parsing.
Quando o parsing para devido a uma tag intermediária, ele para de analisar o conteúdo, retoma o gerador
create()
e passa de volta o conteúdo parcialmente analisado e a tag intermediária em si (em vez da tag
final). Nossa função create()
pode então processar esta tag intermediária (por exemplo, analisar seus argumentos,
se houver) e usar yield
novamente para analisar a próxima parte do conteúdo até a tag final ou outra
tag intermediária esperada.
Vamos modificar DebugNode::create()
para esperar {else}
:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Conteúdo para a parte {debug}
public AreaNode $thenContent;
// Conteúdo opcional para a parte {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield e esperar por {/debug} ou {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Verificar se a tag onde paramos era {else}
if ($nextTag?->name === 'else') {
// Yield novamente para analisar o conteúdo entre {else} e {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() e getIterator() serão atualizados a seguir ...
}
Agora, yield ['else']
diz ao Latte para parar o parsing não apenas para {/debug}
, mas também para
{else}
. Se {else}
for encontrado, $nextTag
conterá o objeto Tag
para
{else}
. Em seguida, usamos yield
novamente sem argumentos, o que significa que agora esperamos apenas a
tag final {/debug}
, e armazenamos o resultado em $node->elseContent
. Se {else}
não foi
encontrado, $nextTag
seria o Tag
para {/debug}
(ou null
, se usado como
n:attribute) e $node->elseContent
permaneceria null
.
Implementando print()
com {else}
O método print()
precisa refletir a nova estrutura. Ele deve gerar uma instrução PHP if/else
baseada no provedor devMode
.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Código para o ramo 'then' (conteúdo {debug})
} else {
%node // Código para o ramo 'else' (conteúdo {else})
}
XX,
$this->position, // Número da linha para a condição 'if'
$this->thenContent, // Primeiro placeholder %node
$this->elseContent ?? new NopNode, // Segundo placeholder %node
);
}
Esta é uma estrutura PHP if/else
padrão. Usamos %node
duas vezes; format()
substitui
os nós fornecidos sequencialmente. Usamos ?? new NopNode
para evitar erros se $this->elseContent
for
null
– NopNode
simplesmente não imprime nada.
Implementando getIterator()
para ambos os conteúdos
Agora temos potencialmente dois nós filhos de conteúdo ($thenContent
e $elseContent
). Precisamos
fornecer ambos, se existirem:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Usando a tag aprimorada
A tag agora pode ser usada com a cláusula opcional {else}
:
{debug}
<p>Exibindo informações de depuração porque devMode está LIGADO.</p>
{else}
<p>Informações de depuração estão ocultas porque devMode está DESLIGADO.</p>
{/debug}
Tratando estado e aninhamento
Nossos exemplos anteriores ({datetime}
, {debug}
) eram relativamente sem estado dentro de seus
métodos print()
. Eles imprimiam conteúdo diretamente ou realizavam uma verificação condicional simples baseada em
um provedor global. No entanto, muitas tags precisam gerenciar alguma forma de estado durante a renderização ou envolvem
a avaliação de expressões do usuário que devem ser executadas apenas uma vez por desempenho ou correção. Além disso,
precisamos considerar o que acontece quando nossas tags personalizadas são aninhadas.
Vamos ilustrar esses conceitos criando uma tag {repeat $count}...{/repeat}
. Esta tag repetirá seu conteúdo
interno $count
vezes.
Objetivo: Implementar {repeat $count}
, que repete seu conteúdo um número especificado de vezes.
A necessidade de variáveis temporárias e únicas
Imagine que o usuário escreva:
{repeat rand(1, 5)} Conteúdo {/repeat}
Se gerássemos ingenuamente um loop for
PHP desta forma em nosso método print()
:
// Código gerado simplificado, INCORRETO
for ($i = 0; $i < rand(1, 5); $i++) {
// imprimir conteúdo
}
Isso estaria errado! A expressão rand(1, 5)
seria reavaliada a cada iteração do loop, levando a um
número imprevisível de repetições. Precisamos avaliar a expressão $count
uma vez antes do início do
loop e armazenar seu resultado.
Geraremos código PHP que primeiro avalia a expressão de contagem e a armazena em uma variável temporária de tempo de
execução. Para evitar colisões com variáveis definidas pelo usuário do template e variáveis internas do Latte
(como $ʟ_...
), usaremos a convenção de prefixar nossas variáveis temporárias com $__
(sublinhado
duplo).
O código gerado ficaria assim:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// imprimir conteúdo
}
Agora, considere o aninhamento:
{repeat $countA} {* Loop externo *}
{repeat $countB} {* Loop interno *}
...
{/repeat}
{/repeat}
Se as tags {repeat}
externa e interna gerassem código usando os mesmos nomes de variáveis temporárias
(por exemplo, $__count
e $__i
), o loop interno sobrescreveria as variáveis do loop externo, quebrando
a lógica.
Precisamos garantir que as variáveis temporárias geradas para cada instância da tag {repeat}
sejam
únicas. Conseguimos isso usando PrintContext::generateId()
. Este método retorna um inteiro único durante a
fase de compilação. Podemos anexar este ID aos nomes de nossas variáveis temporárias.
Então, em vez de $__count
, geraremos $__count_1
para a primeira tag repeat, $__count_2
para a segunda, etc. Da mesma forma, para o contador do loop, usaremos $__i_1
, $__i_2
, etc.
Implementando RepeatNode
Vamos criar a classe do nó.
<?php
namespace App\Latte;
use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class RepeatNode extends StatementNode
{
public ExpressionNode $count;
public AreaNode $content;
/**
* Função de parsing para {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // garante que $count seja fornecido
$node = $tag->node = new self;
// Analisa a expressão de contagem
$node->count = $tag->parser->parseExpression();
// Obtém o conteúdo interno
[$node->content] = yield;
return $node;
}
/**
* Gera um loop 'for' PHP com nomes de variáveis únicos.
*/
public function print(PrintContext $context): string
{
// Geração de nomes de variáveis únicos
$id = $context->generateId();
$countVar = '$__count_' . $id; // ex. $__count_1, $__count_2, etc.
$iteratorVar = '$__i_' . $id; // ex. $__i_1, $__i_2, etc.
return $context->format(
<<<'XX'
// Avalia a expressão de contagem *uma vez* e armazena
%raw = (int) (%node);
// Loop usando a contagem armazenada e variável de iteração única
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Renderiza o conteúdo interno
}
XX,
$countVar, // %0 - Variável para armazenar a contagem
$this->count, // %1 - Nó da expressão para a contagem
$iteratorVar, // %2 - Nome da variável de iteração do loop
$this->position, // %3 - Comentário com número da linha para o próprio loop
$this->content // %4 - Nó do conteúdo interno
);
}
/**
* Fornece os nós filhos (expressão de contagem e conteúdo).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
O método create()
analisa a expressão $count
necessária usando parseExpression()
.
Primeiro, $tag->expectArguments()
é chamado. Isso garante que o usuário forneceu algo após
{repeat}
. Embora $tag->parser->parseExpression()
falhasse se nada fosse fornecido, a mensagem de
erro poderia ser sobre sintaxe inesperada. Usar expectArguments()
fornece um erro muito mais claro, indicando
especificamente que faltam argumentos para a tag {repeat}
.
O método print()
gera o código PHP responsável por executar a lógica de repetição em tempo de execução.
Ele começa gerando nomes únicos para as variáveis PHP temporárias que precisará.
O método $context->format()
é chamado com um novo placeholder %raw
, que insere a string
bruta fornecida como o argumento correspondente. Aqui, ele insere o nome da variável única armazenado em
$countVar
(por exemplo, $__count_1
). E quanto a %0.raw
e %2.raw
? Isso
demonstra placeholders posicionais. Em vez de apenas %raw
, que pega o próximo argumento bruto
disponível, %2.raw
pega explicitamente o argumento no índice 2 (que é $iteratorVar
) e insere seu
valor de string bruto. Isso nos permite reutilizar a string $iteratorVar
sem passá-la várias vezes na lista de
argumentos para format()
.
Esta chamada format()
cuidadosamente construída gera um loop PHP eficiente e seguro que lida corretamente com a
expressão de contagem e evita colisões de nomes de variáveis, mesmo quando as tags {repeat}
estão aninhadas.
Registro e uso
Registre a tag em sua extensão:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Registro da tag repeat
];
}
}
Use-a no template, incluindo aninhamento:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Loop interno</td>
{/repeat}
</tr>
{/repeat}
Este exemplo demonstra como lidar com estado (contadores de loop) e potenciais problemas de aninhamento usando variáveis
temporárias prefixadas com $__
e tornadas únicas com IDs de PrintContext::generateId()
.
n:attributes puros
Enquanto muitos n:attributes
como n:if
ou n:foreach
servem como atalhos convenientes
para suas contrapartes em tags de par ({if}...{/if}
, {foreach}...{/foreach}
), o Latte também permite
definir tags que existem apenas na forma de n:attribute. Estes são frequentemente usados para modificar atributos ou
comportamento do elemento HTML ao qual estão anexados.
Exemplos padrão integrados no Latte incluem n:class
,
que ajuda a construir dinamicamente o atributo class
, e n:attr
, que pode definir múltiplos atributos arbitrários.
Vamos criar nosso próprio n:attribute puro: n:confirm
, que adiciona um diálogo de confirmação JavaScript antes
de executar uma ação (como seguir um link ou enviar um formulário).
Objetivo: Implementar n:confirm="'Tem certeza?'"
, que adiciona um manipulador onclick
para
prevenir a ação padrão se o usuário cancelar o diálogo de confirmação.
Implementando ConfirmNode
Precisamos de uma classe Node e uma função de parsing.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
class ConfirmNode extends StatementNode
{
public ExpressionNode $message;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->message = $tag->parser->parseExpression();
return $node;
}
/**
* Gera o código do atributo 'onclick' com escaping correto.
*/
public function print(PrintContext $context): string
{
// Garante o escaping correto para os contextos de atributo JavaScript e HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
O método print()
gera código PHP que, eventualmente, durante a renderização do template, imprimirá
o atributo HTML onclick="..."
. Lidar com contextos aninhados (JavaScript dentro de um atributo HTML) requer escaping
cuidadoso. O filtro LR\Filters::escapeJs(%node)
é chamado em tempo de execução e escapa a mensagem corretamente
para uso dentro do JavaScript (a saída seria como "Sure?"
). Em seguida, o filtro
LR\Filters::escapeHtmlAttr(...)
escapa caracteres que são especiais em atributos HTML, então isso mudaria a saída
para return confirm("Sure?")
. Este escaping de tempo de execução em duas etapas garante que a
mensagem seja segura para JavaScript e que o código JavaScript resultante seja seguro para incorporação no atributo HTML
onclick
.
Registro e uso
Registre o n:attribute em sua extensão. Não se esqueça do prefixo n:
na chave:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // Registro de n:confirm
];
}
}
Agora você pode usar n:confirm
em links, botões ou elementos de formulário:
<a href="delete.php?id=123" n:confirm='"Realmente deseja excluir o item {$id}?"'>Excluir</a>
HTML gerado:
<a href="delete.php?id=123" onclick="return confirm("Realmente deseja excluir o item 123?")">Excluir</a>
Quando o usuário clica no link, o navegador executa o código onclick
, exibe o diálogo de confirmação e
só navega para delete.php
se o usuário clicar em “OK”.
Este exemplo demonstra como um n:attribute puro pode ser criado para modificar o comportamento ou atributos de seu elemento
HTML hospedeiro, gerando código PHP apropriado em seu método print()
. Lembre-se do duplo escaping que muitas vezes
é necessário: uma vez para o contexto de destino (JavaScript neste caso) e novamente para o contexto do atributo HTML.
Tópicos avançados
Embora as seções anteriores cubram os conceitos básicos, aqui estão alguns tópicos mais avançados que você pode encontrar ao criar tags Latte personalizadas.
Modos de saída de tags
O objeto Tag
passado para sua função create()
tem uma propriedade outputMode
. Esta
propriedade influencia como o Latte trata espaços em branco e indentação ao redor, especialmente quando a tag é usada em sua
própria linha. Você pode modificar esta propriedade em sua função create()
.
Tag::OutputKeepIndentation
(Padrão para a maioria das tags como{=...}
): O Latte tenta preservar a indentação antes da tag. Novas linhas após a tag são geralmente preservadas. Isso é adequado para tags que imprimem conteúdo inline.Tag::OutputRemoveIndentation
(Padrão para tags de bloco como{if}
,{foreach}
): O Latte remove a indentação inicial e potencialmente uma nova linha seguinte. Isso ajuda a manter o código PHP gerado mais limpo e evita linhas em branco extras na saída HTML causadas pela própria tag. Use isso para tags que representam estruturas de controle ou blocos que não devem adicionar espaços em branco por si só.Tag::OutputNone
(Usado por tags como{var}
,{default}
): Semelhante aRemoveIndentation
, mas sinaliza mais fortemente que a tag em si não produz saída direta, potencialmente afetando o tratamento de espaços em branco ao redor dela de forma ainda mais agressiva. Adequado para tags declarativas ou de configuração.
Escolha o modo que melhor se adapta ao propósito da sua tag. Para a maioria das tags estruturais ou de controle,
OutputRemoveIndentation
geralmente é apropriado.
Acessando tags pai/mais próximas
Às vezes, o comportamento de uma tag precisa depender do contexto em que é usada, especificamente em qual(is) tag(s) pai ela
reside. O objeto Tag
passado para sua função create()
fornece o método
closestTag(array $classes, ?callable $condition = null): ?Tag
exatamente para este propósito.
Este método pesquisa para cima na hierarquia das tags atualmente abertas (incluindo elementos HTML representados internamente
durante o parsing) e retorna o objeto Tag
do ancestral mais próximo que corresponde a critérios específicos. Se
nenhum ancestral correspondente for encontrado, retorna null
.
O array $classes
especifica que tipo de tags ancestrais você está procurando. Ele verifica se o nó associado
da tag ancestral ($ancestorTag->node
) é uma instância desta classe.
function create(Tag $tag)
{
// Procura a tag ancestral mais próxima cujo nó é uma instância de ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Podemos acessar a instância ForeachNode em si:
$foreachNode = $foreachTag->node;
}
}
Observe $foreachTag->node
: Isso funciona apenas porque é uma convenção no desenvolvimento de tags Latte
atribuir imediatamente o nó criado a $tag->node
dentro do método create()
, como sempre
fizemos.
Às vezes, apenas comparar o tipo do nó não é suficiente. Você pode precisar verificar uma propriedade específica da tag
ancestral potencial ou seu nó. O segundo argumento opcional para closestTag()
é um callable que recebe o objeto
Tag
ancestral potencial e deve retornar se é uma correspondência válida.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Condição: o bloco deve ser dinâmico
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Usar closestTag()
permite criar tags que são conscientes do contexto e impõem o uso adequado dentro da
estrutura do seu template, levando a templates mais robustos e compreensíveis.
Placeholders PrintContext::format()
Usamos frequentemente PrintContext::format()
para gerar código PHP nos métodos print()
de nossos
nós. Ele aceita uma string de máscara e argumentos subsequentes que substituem os placeholders na máscara. Aqui está um resumo
dos placeholders disponíveis:
%node
: O argumento deve ser uma instância deNode
. Chama o métodoprint()
do nó e insere a string de código PHP resultante.%dump
: O argumento é qualquer valor PHP. Exporta o valor para código PHP válido. Adequado para escalares, arrays, null.$context->format('echo %dump;', 'Hello')
→echo 'Hello';
$context->format('$arr = %dump;', [1, 2])
→$arr = [1, 2];
%raw
: Insere o argumento diretamente no código PHP de saída sem qualquer escaping ou modificação. Use com cautela, principalmente para inserir fragmentos de código PHP pré-gerados ou nomes de variáveis.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: O argumento deve ser umExpression\ArrayNode
. Imprime os itens do array formatados como argumentos para uma chamada de função ou método (separados por vírgula, lida com argumentos nomeados se presentes).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: O argumento deve ser um objetoPosition
(geralmente$this->position
). Insere um comentário PHP/* line X */
indicando o número da linha da fonte.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Gera código PHP que em tempo de execução escapa a expressão interna usando as regras de escaping conscientes do contexto atuais.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: O argumento deve ser umModifierNode
. Gera código PHP que aplica os filtros especificados noModifierNode
ao conteúdo interno, incluindo escaping consciente do contexto, a menos que desabilitado com|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Semelhante a%modify
, mas destinado a modificar blocos de conteúdo capturado (frequentemente HTML).
Você pode referenciar explicitamente argumentos por seu índice (baseado em zero): %0.node
, %1.dump
,
%2.raw
, etc. Isso permite reutilizar um argumento várias vezes na máscara sem passá-lo repetidamente para
format()
. Veja o exemplo da tag {repeat}
, onde %0.raw
e %2.raw
foram
usados.
Exemplo de parsing de argumentos complexos
Embora parseExpression()
, parseArguments()
, etc., cubram muitos casos, às vezes você precisa de
lógica de parsing mais complexa usando o TokenStream
de nível inferior disponível via
$tag->parser->stream
.
Objetivo: Criar uma tag {embedYoutube $videoID, width: 640, height: 480}
. Queremos analisar o ID do vídeo
obrigatório (string ou variável) seguido por pares chave-valor opcionais para as dimensões.
<?php
namespace App\Latte;
class YoutubeNode extends StatementNode
{
public ExpressionNode $videoId;
public ?ExpressionNode $width = null;
public ?ExpressionNode $height = null;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
// Analisar o ID do vídeo obrigatório
$node->videoId = $tag->parser->parseExpression();
// Analisar pares chave-valor opcionais
$stream = $tag->parser->stream; // Obter o fluxo de tokens
while ($stream->tryConsume(',')) { // Requer separação por vírgula
// Esperar um identificador 'width' ou 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Esperar o separador de dois pontos
$value = $tag->parser->parseExpression(); // Analisar a expressão do valor
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Argumento desconhecido '$key'. Esperado 'width' ou 'height'.", $keyToken->position);
}
}
return $node;
}
}
Este nível de controle permite definir sintaxes muito específicas e complexas para suas tags personalizadas, interagindo diretamente com o fluxo de tokens.
Usando AuxiliaryNode
O Latte fornece nós “auxiliares” genéricos para situações especiais durante a geração de código ou dentro de passos
de compilação. São eles AuxiliaryNode
e Php\Expression\AuxiliaryNode
.
Considere AuxiliaryNode
como um nó contêiner flexível que delega suas funcionalidades principais – geração
de código e exposição de nós filhos – aos argumentos fornecidos em seu construtor:
- Delegação de
print()
: O primeiro argumento do construtor é uma closure PHP. Quando o Latte chama o métodoprint()
em umAuxiliaryNode
, ele executa esta closure fornecida. A closure recebe oPrintContext
e quaisquer nós passados no segundo argumento do construtor, permitindo que você defina lógica de geração de código PHP completamente personalizada em tempo de execução. - Delegação de
getIterator()
: O segundo argumento do construtor é um array de objetosNode
. Quando o Latte precisa percorrer os filhos de umAuxiliaryNode
(por exemplo, durante os passos de compilação), seu métodogetIterator()
simplesmente fornece os nós listados neste array.
Exemplo:
$node = new AuxiliaryNode(
// 1. Esta closure se torna o corpo de print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Estes nós são fornecidos pelo método getIterator() e passados para a closure acima
[$argumentNode1, $argumentNode2]
);
O Latte fornece dois tipos distintos com base em onde você precisa inserir o código gerado:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Use isso quando precisar gerar um pedaço de código PHP que representa uma expressão.Latte\Compiler\Nodes\AuxiliaryNode
: Use isso para fins mais gerais, quando precisar inserir um bloco de código PHP representando uma ou mais instruções.
Uma razão importante para usar AuxiliaryNode
em vez de nós padrão (como StaticMethodCallNode
)
dentro do seu método print()
ou passo de compilação é controlar a visibilidade para os passos de compilação
subsequentes, especialmente aqueles relacionados à segurança, como o Sandbox.
Considere um cenário: Seu passo de compilação precisa envolver uma expressão fornecida pelo usuário
($userExpr
) em uma chamada para uma função auxiliar específica e confiável
myInternalSanitize($userExpr)
. Se você criar um nó padrão
new FunctionCallNode('myInternalSanitize', [$userExpr])
, ele será totalmente visível para a travessia da AST. Se um
passo Sandbox for executado posteriormente e myInternalSanitize
não estiver em sua lista de permissões,
o Sandbox pode bloquear ou modificar essa chamada, potencialmente quebrando a lógica interna da sua tag, mesmo que
você, o autor da tag, saiba que essa chamada específica é segura e necessária. Você pode, portanto, gerar a chamada
diretamente dentro da closure do AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... dentro de print() ou passo de compilação ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Geração direta de código PHP
$userExpr,
),
// IMPORTANTE: Ainda passe o nó da expressão do usuário original aqui!
[$userExpr],
);
Neste caso, o passo Sandbox vê o AuxiliaryNode
, mas não analisa o código PHP gerado por sua closure.
Ele não pode bloquear diretamente a chamada myInternalSanitize
gerada dentro da closure.
Embora o próprio código PHP gerado esteja oculto dos passos, as entradas para esse código (nós representando dados
ou expressões do usuário) ainda devem ser atravessáveis. É por isso que o segundo argumento do construtor
AuxiliaryNode
é crucial. Você deve passar um array contendo todos os nós originais (como
$userExpr
no exemplo acima) que sua closure usa. O getIterator()
do AuxiliaryNode
fornecerá esses nós, permitindo que passos de compilação como o Sandbox os analisem em busca de problemas
potenciais.
Melhores práticas
- Propósito claro: Certifique-se de que sua tag tenha um propósito claro e necessário. Não crie tags para tarefas que podem ser facilmente resolvidas com filtros ou funções.
- Implemente
getIterator()
corretamente: Sempre implementegetIterator()
e forneça referências (&
) para todos os nós filhos (argumentos, conteúdo) que foram analisados do template. Isso é essencial para os passos de compilação, segurança (Sandbox) e potenciais otimizações futuras. - Propriedades públicas para nós: Torne públicas as propriedades que contêm nós filhos para que os passos de compilação possam modificá-los, se necessário.
- Use
PrintContext::format()
: Utilize o métodoformat()
para gerar código PHP. Ele lida com aspas, escapa corretamente os placeholders e adiciona comentários de número de linha automaticamente. - Variáveis temporárias (
$__
): Ao gerar código PHP de tempo de execução que precisa de variáveis temporárias (por exemplo, para armazenar subtotais, contadores de loop), use a convenção de prefixo$__
para evitar colisões com variáveis do usuário e variáveis internas do Latte$ʟ_
. - Aninhamento e IDs únicos: Se sua tag pode ser aninhada ou precisa de estado específico da instância em tempo de
execução, use
$context->generateId()
dentro do seu métodoprint()
para criar sufixos únicos para suas variáveis temporárias$__
. - Provedores para dados externos: Use provedores (registrados via
Extension::getProviders()
) para acessar dados ou serviços em tempo de execução ($this->global->…) em vez de codificar valores ou depender de estado global. Use prefixos de fornecedor para nomes de provedores. - Considere n:attributes: Se sua tag de par opera logicamente em um único elemento HTML, o Latte provavelmente
fornecerá suporte automático a
n:attribute
. Tenha isso em mente para conveniência do usuário. Se estiver criando uma tag modificadora de atributo, considere se umn:attribute
puro é a forma mais apropriada. - Testes: Escreva testes para suas tags, cobrindo tanto o parsing de diferentes entradas sintáticas quanto a correção da saída do código PHP gerado.
Seguindo estas diretrizes, você pode criar tags personalizadas poderosas, robustas e sustentáveis que se integram perfeitamente com o engine de templates Latte.
Estudar as classes de nós que fazem parte do Latte é a melhor maneira de aprender todos os detalhes sobre o processo de parsing.