Özel Etiketler Oluşturma
Bu sayfa, Latte'de özel etiketler oluşturmak için kapsamlı bir rehber sunar. Latte'nin şablonları nasıl derlediğine dair bilginizin üzerine inşa ederek, basit etiketlerden iç içe içerik ve özel ayrıştırma ihtiyaçları olan daha karmaşık senaryolara kadar her şeyi ele alacağız.
Özel etiketler, şablon söz dizimi ve render mantığı üzerinde en yüksek düzeyde denetim sağlar, ancak aynı zamanda en karmaşık genişletme noktasıdır. Özel bir etiket oluşturmaya karar vermeden önce, her zaman daha basit bir çözüm olup olmadığını veya standart sette uygun bir etiketin zaten bulunup bulunmadığını değerlendirin. Özel etiketleri yalnızca daha basit alternatifler ihtiyaçlarınızı karşılamadığında kullanın.
Derleme Sürecini Anlamak
Özel etiketleri etkili bir şekilde oluşturmak için Latte'nin şablonları nasıl işlediğini açıklamak yararlıdır. Bu süreci anlamak, etiketlerin neden böyle yapılandırıldığını ve büyük resme nasıl oturduğunu netleştirir.
Latte'de şablon derleme, basitleştirilmiş haliyle şu temel adımlardan oluşur:
- Sözcüksel çözümleme (lexing): Lexer, şablonun kaynak kodunu (
.lattedosyası) okur ve onu token adı verilen küçük, ayrı parçalar dizisine böler (örneğin{,foreach,$variable,}, HTML metni vb.). - Ayrıştırma (parsing): Ayrıştırıcı bu token akışını alır ve şablonun mantığını ve içeriğini temsil eden anlamlı bir ağaç yapısı kurar. Bu ağaca Soyut Söz Dizimi Ağacı (AST) denir.
- Compiler pass'ler: PHP kodu üretmeden önce Latte compiler pass'leri çalıştırır. Bunlar tüm AST'yi gezen, onu değiştirebilen veya bilgi toplayabilen fonksiyonlardır. Bu adım, güvenlik (Sandbox) veya optimizasyonlar gibi özellikler için hayati önem taşır.
- Kod üretimi: Son olarak derleyici, (muhtemelen değiştirilmiş) AST'yi gezer ve buna karşılık gelen PHP sınıf kodunu üretir. Şablonu çalıştırıldığında gerçekten render eden şey bu PHP kodudur.
- Önbellekleme: Üretilen PHP kodu diskte önbelleğe alınır; böylece 1–4. adımlar atlandığı için sonraki render'lar çok hızlı olur.
Aslında derleme biraz daha karmaşıktır. Latte'nin iki lexer'ı ve ayrıştırıcısı vardır: biri HTML şablonu, diğeri etiketlerin içindeki PHP benzeri kod için. Ayrıca ayrıştırma, token'lara ayırma işleminden sonra çalışmaz; lexer ve ayrıştırıcı iki “iş parçacığında” paralel çalışır ve birbirleriyle eşgüdüm sağlar. Bana, David Grudl'a inanın: bunu programlamak roket bilimi gibi hissettirdi :-)
Şablon içeriğinin yüklenmesinden ayrıştırmaya, oradan sonuç dosyasının üretilmesine kadar tüm süreç, üzerinde deneyler yapabileceğiniz ve ara sonuçları dökebileceğiniz şu kodla sıralanabilir:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Bir Etiketin Anatomisi
Latte'de tam işlevsel bir özel etiket oluşturmak birbiriyle bağlantılı birkaç parça içerir. Uygulamaya geçmeden önce, HTML ve Belge Nesne Modeli'ne (DOM) benzetme yaparak temel kavramları ve terminolojiyi anlayalım.
Etiketler ve Node'lar (HTML ile Benzetme)
HTML'de <p> veya <div>...</div> gibi etiketler yazarız. Bu etiketler kaynak
koddaki söz dizimidir. Bir tarayıcı bu HTML'i ayrıştırdığında, bellekte Belge Nesne Modeli (DOM) adı verilen bir
temsil oluşturur. DOM'da HTML etiketleri node'larla (JavaScript DOM terminolojisinde özellikle Element
node'larıyla) temsil edilir. Bu node'larla programatik olarak etkileşime gireriz (örneğin JavaScript'te
document.getElementById(...) bir Element node'u döndürür). Etiket yalnızca kaynak dosyadaki metinsel temsildir;
node ise mantıksal ağaçtaki nesne temsilidir.
Latte de benzer şekilde çalışır:
- Bir
.latteşablon dosyasında{foreach ...}ve{/foreach}gibi Latte etiketleri yazarsınız. Şablon yazarı olarak etkileşime girdiğiniz söz dizimi budur. - Latte şablonu ayrıştırdığında bir Soyut Söz Dizimi Ağacı (AST) kurar. Bu ağaç Node'lardan oluşur. Şablondaki her Latte etiketi, HTML elemanı, metin parçası veya ifade bu ağaçta bir ya da daha fazla node'a dönüşür.
- AST'deki tüm node'ların temel sınıfı
Latte\Compiler\Node'dur. DOM'un farklı node türleri (Element, Text, Comment) olduğu gibi, Latte'nin AST'sinin de çeşitli node türleri vardır. Statik metin içinLatte\Compiler\Nodes\TextNode, HTML elemanları içinLatte\Compiler\Nodes\Html\ElementNode, etiketlerin içindeki ifadeler içinLatte\Compiler\Nodes\Php\ExpressionNodeve özel etiketler açısından en önemlisiLatte\Compiler\Nodes\StatementNode'dan türeyen node'larla karşılaşacaksınız.
Neden StatementNode?
HTML elemanları (Html\ElementNode) öncelikle yapıyı ve içeriği temsil eder. PHP ifadeleri
(Php\ExpressionNode) değerleri veya hesaplamaları temsil eder. Peki ya {if}, {foreach} ya
da bizim özel {datetime} etiketimiz gibi Latte etiketleri? Bu etiketler eylem gerçekleştirir, program
akışını denetler veya mantığa dayalı çıktı üretir. Latte'yi yalnızca bir işaretleme dili değil, güçlü bir şablon
motoru yapan işlevsel birimlerdir.
Programlamada eylem gerçekleştiren bu tür birimlere genellikle “deyim” (statement) denir. Bu nedenle, işlevsel Latte
etiketlerini temsil eden node'lar tipik olarak Latte\Compiler\Nodes\StatementNode'dan türetilir. Bu, onları salt
yapısal node'lardan (HTML elemanları gibi) veya değer temsil eden node'lardan (ifadeler gibi) ayırır.
Temel Bileşenler
Özel bir etiket oluşturmak için gereken ana bileşenleri gözden geçirelim:
Etiket Ayrıştırma Fonksiyonu
- Bu PHP callable'ı, şablon kaynağındaki Latte etiketi söz dizimini (
{...}) ayrıştırır. - Etiketle ilgili bilgileri (adı, konumu ve n:attribute olup olmadığı gibi) bir Latte\Compiler\Tag nesnesi aracılığıyla, ana Latte\Compiler\TemplateParser'ı ise ikinci
argüman olarak alır. Tam imzası
callable(Tag, TemplateParser): (Node|\Generator|void)şeklindedir. - Etiket sınırlayıcıları içindeki argümanları ve ifadeleri ayrıştırmak için başlıca aracı,
$tag->parserüzerinden erişilen Latte\Compiler\TagParser nesnesidir (bu, tüm şablonu ayrıştırandan farklı bir ayrıştırıcıdır). - Çiftli etiketlerde, başlangıç ve bitiş etiketleri arasındaki iç içeriği ayrıştırması için Latte'ye sinyal vermek
üzere
yieldkullanır. - Ayrıştırma fonksiyonunun nihai amacı, AST'ye eklenecek bir Node sınıfı örneği oluşturup döndürmektir.
- Ayrıştırma fonksiyonunu, karşılık gelen Node sınıfının içinde doğrudan statik bir metot (genellikle
createadıyla) olarak yazmak alışılmış bir uygulamadır (zorunlu değildir). Bu, ayrıştırma mantığını ve node'un temsilini derli toplu bir arada tutar, gerektiğinde private/protected sınıf öğelerine erişime izin verir ve düzeni iyileştirir.
Node Sınıfı
- Etiketinizin Soyut Söz Dizimi Ağacı (AST) içindeki mantıksal işlevini temsil eder.
- Ayrıştırılan bilgileri (argümanlar veya içerik gibi) public özellikler olarak tutar. Bu özellikler genellikle başka
Nodeörnekleri içerir (örneğin ayrıştırılmış argümanlar içinExpressionNode, ayrıştırılmış içerik içinAreaNode). print(PrintContext $context): stringmetodu, şablon render edilirken etiketin eylemini gerçekleştiren PHP kodunu (bir deyimi veya deyimler dizisini) üretir.getIterator(): \Generatormetodu, alt node'ları (argümanlar, içerik) compiler pass'ler tarafından gezilebilir kılar. Pass'lerin alt node'ları değiştirebilmesi veya başkasıyla takas edebilmesi için referans (&) yield etmelidir.- Şablonun tamamı AST'ye ayrıştırıldıktan sonra Latte bir dizi compiler pass çalıştırır. Bu pass'ler, her node'un sağladığı
getIterator()metodunu kullanarak tüm AST'yi gezer. Node'ları inceleyebilir, bilgi toplayabilir ve hatta ağacı değiştirebilirler (örneğin node'ların public özelliklerini değiştirerek veya node'ları tamamen başkasıyla takas ederek). Kapsamlı birgetIterator()gerektiren bu tasarım hayati önemdedir. Sandbox gibi güçlü özelliklerin, özel etiketleriniz de dahil olmak üzere şablonun herhangi bir parçasını analiz edip davranışını değiştirebilmesini sağlar; bu da güvenliği ve tutarlılığı garanti eder.
Extension Aracılığıyla Kayıt
- Latte'ye yeni etiketinizi ve onun için hangi ayrıştırma fonksiyonunun kullanılacağını bildirmeniz gerekir. Bu, bir Latte Extension içinde gerçekleşir.
- Extension sınıfınızın içinde
getTags(): arraymetodunu uygularsınız. Bu metot, anahtarların etiket adları (örneğin'mytag','n:myattribute'), değerlerin ise bunlara karşılık gelen ayrıştırma fonksiyonlarını temsil eden PHP callable'ları (örneğinMyNamespace\DatetimeNode::create(...)) olduğu ilişkisel bir dizi döndürür.
Özetle: Etiket ayrıştırma fonksiyonu, etiketinizin şablon kaynak kodunu bir AST Node'una
dönüştürür. Node sınıfı ise kendisini derlenmiş şablon için çalıştırılabilir PHP koduna
nasıl dönüştüreceğini bilir ve alt node'larını getIterator() aracılığıyla compiler pass'lere
açar. Extension aracılığıyla kayıt, etiket adını ayrıştırma fonksiyonuna bağlar ve onu Latte'ye tanıtır.
Şimdi bu bileşenlerin adım adım nasıl uygulanacağına bakalım.
Basit Bir Etiket Oluşturma
İlk özel Latte etiketinizi oluşturmaya girişelim. Çok basit bir örnekle başlayacağız: geçerli tarih ve saati
çıktılayan {datetime} adlı bir etiket. Başlangıçta bu etiket hiçbir argüman kabul etmeyecek, ancak
daha sonra Etiket Argümanlarını Ayrıştırma bölümünde onu
geliştireceğiz. İç içeriği de yoktur.
Bu örnek sizi temel adımlarda yönlendirecek: Node sınıfını tanımlamak, print() ve
getIterator() metotlarını uygulamak, ayrıştırma fonksiyonunu oluşturmak ve son olarak etiketi kaydetmek.
Hedef: PHP'nin date() fonksiyonunu kullanarak geçerli tarih ve saati çıktılayan {datetime}
etiketini uygulamak.
Node Sınıfının Oluşturulması
Öncelikle, etiketimizi Soyut Söz Dizimi Ağacı'nda (AST) temsil edecek bir sınıfa ihtiyacımız var. Yukarıda
anlatıldığı gibi Latte\Compiler\Nodes\StatementNode'dan türetiyoruz.
Bir dosya oluşturun (örneğin DatetimeNode.php) ve sınıfı tanımlayın:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* {datetime} bulunduğunda çağrılan etiket ayrıştırma fonksiyonu.
*/
public static function create(Tag $tag): self
{
// Etiketimiz içerik çıktılıyor, bu yüzden çevresindeki girintiyi koru
$tag->outputMode = $tag::OutputKeepIndentation;
// Basit etiketimiz şu an argüman almıyor, dolayısıyla bir şey ayrıştırmamıza gerek yok
$node = $tag->node = new self;
return $node;
}
/**
* Şablon render edilirken çalıştırılacak PHP kodunu üretir.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Latte'nin compiler pass'leri için alt node'lara erişim sağlar.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Latte bir şablonda {datetime} ile karşılaştığında, etiket ayrıştırma fonksiyonu
create()'i çağırır. Bu fonksiyonun görevi bir DatetimeNode örneği döndürmektir. Ayrıca
$tag->outputMode değerini OutputKeepIndentation olarak ayarlıyoruz; çünkü bir etiket varsayılan
OutputNone modunda çalıştığından (Etiket Çıktı Modları bölümünde
açıklanıyor), şablonun ilk metninden önce yer alan bir etiket çıktısını main() yerine üretilen
prepare() metodunda verebilirdi. Bu modu ayarlamak, çıktının etiketin durduğu yerde oluşmasını sağlar.
print() metodu, şablon render edilirken çalıştırılacak PHP kodunu üretir. Derlenmiş şablon için sonuç
PHP kodu dizesini birleştiren $context->format() metodunu çağırıyoruz. İlk argüman olan
'echo date('Y-m-d H:i:s') %line;', sonraki parametrelerin yerine konduğu maskedir. %line yer tutucusu,
format() metoduna kendisinden sonraki argümanı (yani $this->position) almasını ve üretilen PHP
kodunu özgün şablon satırına bağlayan /* pos 15:1 */ gibi bir yorum eklemesini söyler; bu da hata ayıklama
için hayati önemdedir.
$this->position özelliği temel Node sınıfından miras alınır ve Latte'nin
ayrıştırıcısı tarafından otomatik olarak ayarlanır. Etiketin kaynak .latte dosyasındaki yerini gösteren bir
Latte\Compiler\Range nesnesi tutar (bu,
Position sınıfının bayt cinsinden bir length ile genişletilmiş bir alt sınıfıdır). Çiftli
etiketlerde bu aralık açılış etiketinden kapanış etiketine kadar uzanır; ayrıca StatementNode torunları,
kendisini oluşturan her etiketin (açılış, {else}/{case} gibi ara ve kapanış) Range
değerini listeleyen $this->tagRanges özelliğini sunar.
getIterator() metodu compiler pass'ler için hayati önemdedir. Tüm alt node'ları yield etmelidir, ancak basit
DatetimeNode'umuzun şu an argümanı veya içeriği, dolayısıyla alt node'u yoktur. Yine de metodun var olması
ve bir generator olması gerekir; yani yield anahtar sözcüğü metot gövdesinde bir şekilde bulunmalıdır.
Extension Aracılığıyla Kayıt
Son olarak Latte'yi yeni etiketten haberdar edin. Bir Extension sınıfı oluşturun (örneğin
MyLatteExtension.php) ve etiketi onun getTags() metodunda kaydedin.
<?php
namespace App\Templating;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Bu extension'ın sağladığı etiketlerin listesini döndürür.
* @return array<string, callable> Eşleme: 'etiket-adı' => ayrıştırma-fonksiyonu
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Daha fazla etiketi buraya kaydedin
];
}
}
Ardından bu extension'ı Latte Engine'e kaydedin:
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
Şablonu oluşturun:
<p>Sayfanın oluşturulma zamanı: {datetime}</p>
Beklenen çıktı: <p>Sayfanın oluşturulma zamanı: 2023-10-27 11:00:00</p>
Bu Aşamanın Özeti
{datetime} adlı basit bir özel etiketi başarıyla oluşturduk. AST'deki temsilini tanımladık
(DatetimeNode), ayrıştırmasını ele aldık (create()), PHP kodunu nasıl üreteceğini belirledik
(print()), alt öğelerinin gezilebilir olmasını sağladık (getIterator()) ve etiketi Latte'ye
kaydettik.
Bir sonraki bölümde bu etiketi argüman kabul edecek şekilde geliştirecek, böylece ifadelerin nasıl ayrıştırılacağını ve alt node'ların nasıl yönetileceğini göstereceğiz.
Etiket Argümanlarını Ayrıştırma
Basit {datetime} etiketimiz çalışıyor ama pek esnek değil. Onu isteğe bağlı bir argüman kabul edecek
şekilde geliştirelim: date() fonksiyonu için bir biçim dizesi. İstenen söz dizimi
{datetime $format} olacak.
Hedef: {datetime} etiketini, date() için biçim dizesi olarak kullanılacak isteğe bağlı
bir PHP ifadesini argüman olarak kabul edecek şekilde değiştirmek.
TagParser ile Tanışma
Kodu değiştirmeden önce kullanacağımız aracı, Latte\Compiler\TagParser'ı anlamak önemlidir.
Latte'nin ana ayrıştırıcısı (TemplateParser) {datetime ...} gibi bir Latte etiketiyle ya da bir
n:attribute ile karşılaştığında, etiketin içindeki içeriğin ({ ile } arasındaki
kısmın veya niteliğin değerinin) ayrıştırılmasını özelleşmiş bir TagParser'a devreder.
Bu TagParser yalnızca etiketin argümanları üzerinde çalışır. Görevi, bu argümanları temsil eden
token'ları tüketmektir. Önemli olan şu: kendisine verilen içeriğin tamamını ayrıştırmak zorundadır.
Ayrıştırma fonksiyonunuz bittiğinde TagParser argümanların sonuna ulaşmamışsa
($tag->parser->isEnd() ile denetlenir), Latte bir istisna fırlatır; çünkü bu, etiketin içinde beklenmeyen
token'ların kaldığını gösterir. Bunun tersine, bir etiket argüman gerektiriyorsa, ayrıştırma fonksiyonunuzun
başında $tag->expectArguments() çağırmalısınız. Bu metot argümanların var olup olmadığını denetler
ve etiket argümansız kullanılmışsa yardımcı bir istisna fırlatır.
TagParser, farklı türde argümanları ayrıştırmak için kullanışlı metotlar sunar:
parseExpression(): ExpressionNode: PHP benzeri bir ifadeyi (değişkenler, sabit değerler, operatörler, fonksiyon/metot çağrıları vb.) ayrıştırır. Latte'nin söz dizimi kolaylıklarını da işler; örneğin basit alfanümerik dizeleri tırnak içindeymiş gibi ele alır (foo,'foo'yazılmış gibi ayrıştırılır).parseUnquotedStringOrExpression(): ExpressionNode: Ya standart bir ifadeyi ya da tırnaksız bir dizeyi ayrıştırır. Tırnaksız dizeler, Latte'nin tırnak olmadan izin verdiği dizilerdir ve genellikle dosya yolları gibi şeyler için kullanılır (örneğin{include ../file.latte}). Tırnaksız bir dize ayrıştırırsa birStringNodedöndürür.parseArguments(): ArrayNode:10, name: 'John', truegibi, gerektiğinde anahtarlı olabilen virgülle ayrılmış argümanları ayrıştırır.parseModifier(): ModifierNode:|upper|truncate:10gibi filtreleri ayrıştırır.parseType(): ?SuperiorTypeNode:int,?string,array|Foogibi PHP tür bildirimlerini ayrıştırır.
Daha karmaşık veya düşük seviyeli ayrıştırma ihtiyaçları için $tag->parser->stream üzerinden token akışıyla doğrudan etkileşime
girebilirsiniz. Bu nesne, tek tek token'ları incelemek ve tüketmek için metotlar sağlar:
$tag->parser->stream->is(...): bool: Geçerli token'ın belirtilen türlerden (örneğinToken::Php_Variable) veya sabit değerlerden (örneğin'as') herhangi biriyle eşleşip eşleşmediğini, onu tüketmeden denetler. İleriye bakmak için kullanışlıdır.$tag->parser->stream->consume(...): Token: Geçerli token'ı tüketir ve akış konumunu ileri taşır. Argüman olarak beklenen token türleri/değerleri verilmişse ve geçerli token bunlarla eşleşmiyorsa, birCompileExceptionfırlatır. Belirli bir token beklediğinizde bunu kullanın.$tag->parser->stream->tryConsume(...): ?Token: Geçerli token'ı yalnızca belirtilen tür/değerlerden biriyle eşleşiyorsa tüketmeye çalışır. Eşleşirse token'ı tüketir ve döndürür. Eşleşmezse akış konumunu değiştirmeden bırakır venulldöndürür. İsteğe bağlı token'lar için veya farklı söz dizimi yolları arasında seçim yaparken kullanın.
Ayrıştırma Fonksiyonu create()'i Güncelleme
Bu bilgilerle, DatetimeNode içindeki create() metodunu, isteğe bağlı biçim argümanını
$tag->parser ile ayrıştıracak şekilde değiştirelim.
<?php
namespace App\Templating;
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
{
// Ayrıştırılan biçim ifadesi node'unu tutacak public bir özellik ekle
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Herhangi bir token var mı diye denetle
if (!$tag->parser->isEnd()) {
// Argümanı TagParser ile PHP benzeri bir ifade olarak ayrıştır.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... print() ve getIterator() metotları bir sonraki adımda güncellenecek ...
}
$format public özelliğini ekledik. create() içinde artık argüman olup olmadığını
denetlemek için $tag->parser->isEnd() kullanıyoruz. Varsa, $tag->parser->parseExpression()
ifadenin token'larını tüketir. TagParser girdi token'larının tümünü tüketmek zorunda olduğundan,
kullanıcı biçim ifadesinden sonra beklenmedik bir şey yazarsa (örneğin {datetime 'Y-m-d', unexpected}) Latte
otomatik olarak hata fırlatır.
print() Metodunu Güncelleme
Şimdi print() metodunu, $this->format içinde saklanan ayrıştırılmış biçim ifadesini
kullanacak şekilde değiştirin. Biçim verilmemişse ($this->format değeri null ise) varsayılan
bir biçim dizesi, örneğin 'Y-m-d H:i:s', kullanmalıyız.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node, $formatNode'un PHP kodu temsilini yazdırır.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
$formatNode değişkeninde, PHP'nin date() fonksiyonu için biçim dizesini temsil eden AST node'unu
saklıyoruz. Burada null birleştirme operatörünü (??) kullanıyoruz. Kullanıcı şablonda bir argüman vermişse
(örneğin {datetime 'd.m.Y'}), $this->format özelliği ilgili node'u tutar (bu durumda
'd.m.Y' değerine sahip bir StringNode) ve o node kullanılır. Kullanıcı argüman vermemişse
(yalnızca {datetime} yazmışsa), $this->format özelliği null olur ve bunun yerine
varsayılan 'Y-m-d H:i:s' biçimiyle yeni bir StringNode oluştururuz. Böylece $formatNode
her zaman biçim için geçerli bir AST node'u içerir.
'echo date(%node) %line;' maskesinde, format() metoduna kendisinden sonraki ilk argümanı (yani
$formatNode'umuzu) almasını, onun print() metodunu çağırmasını (bu, node'un PHP kodu temsilini
döndürür) ve sonucu yer tutucunun bulunduğu konuma yerleştirmesini söyleyen yeni %node yer tutucusu
kullanılıyor.
Alt Node'lar İçin getIterator() Uygulaması
DatetimeNode'umuzun artık bir alt node'u var: $format ifadesi. Bu alt node'u
getIterator() metodunda yield ederek compiler pass'lere erişilebilir kılmak zorundayız. Pass'lerin
node'u başkasıyla takas edebilmesi için referans (&) yield etmeyi unutmayın.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Bu neden bu kadar önemli? $format argümanının yasaklı bir fonksiyon çağrısı içerip içermediğini
denetlemesi gereken bir Sandbox pass'i düşünün (örneğin {datetime dangerousFunction()}).
getIterator() metodu $this->format'ı yield etmezse, Sandbox pass'i etiketimizin argümanı
içindeki dangerousFunction() çağrısını asla göremez ve olası bir güvenlik açığı doğar. Onu yield ederek
Sandbox'ın (ve diğer pass'lerin) $format ifade node'unu incelemesine ve gerekirse değiştirmesine izin
veriyoruz.
Geliştirilmiş Etiketin Kullanımı
Etiket artık isteğe bağlı bir argümanı doğru şekilde ele alıyor:
Varsayılan biçim: {datetime}
Özel biçim: {datetime 'd.m.Y'}
Değişken kullanımı: {datetime $userDateFormatPreference}
{* Bu, 'd.m.Y' ayrıştırıldıktan sonra hataya yol açar çünkü ", foo" beklenmiyor *}
{* {datetime 'd.m.Y', foo} *}
Sırada, aralarındaki içeriği işleyen çiftli etiketler oluşturmaya bakacağız.
Çiftli Etiketleri Ele Alma
Şimdiye kadar {datetime} etiketimiz (kavramsal olarak) kendiliğinden kapanan bir etiketti. Başlangıç
ve bitiş etiketi arasında herhangi bir içeriği yok. Ancak birçok yararlı etiket bir şablon içeriği bloğu üzerinde
çalışır. Bunlara çiftli etiketler denir. Örnek olarak {if}...{/if}, {block}...{/block} veya
birazdan oluşturacağımız özel etiket verilebilir: {debug}...{/debug}.
Bu etiket, şablonlarımıza yalnızca geliştirme sırasında görünmesi gereken hata ayıklama bilgileri koymamıza olanak tanıyacak.
Hedef: İçeriği yalnızca belirli bir “geliştirme modu” bayrağı etkinse render edilen çiftli bir
{debug} etiketi oluşturmak.
Provider'lar ile Tanışma
Bazen etiketlerinizin, şablon parametresi olarak doğrudan aktarılmayan verilere veya servislere erişmesi gerekir. Örneğin uygulamanın geliştirme modunda olup olmadığını belirlemek, bir kullanıcı nesnesine erişmek veya yapılandırma değerlerini almak. Latte bunun için Provider adlı bir mekanizma sunar.
Provider'lar, Extension içinde
getProviders() metoduyla kaydedilir. Bu metot, anahtarların provider'ların şablonun çalışma zamanı kodunda
erişilebilir olacağı adlar, değerlerin ise gerçek veriler veya nesneler olduğu ilişkisel bir dizi döndürür.
Etiketinizin print() metodunun ürettiği PHP kodunun içinde bu provider'lara özel $this->global
nesne özelliği üzerinden erişebilirsiniz. Bu özellik tüm extension'lar arasında paylaşıldığından, Latte'nin çekirdek
provider'larıyla veya üçüncü taraf extension'ların provider'larıyla ad çakışması yaşamamak için provider
adlarınıza önek koymak iyi bir uygulamadır. Yaygın bir yaklaşım, üretici veya extension adınızla ilişkili kısa ve
benzersiz bir önek kullanmaktır. Örneğimizde app önekini kullanalım; geliştirme modu bayrağı
$this->global->appDevMode olarak erişilebilir olacak.
İçeriği Ayrıştırmak İçin yield Anahtar
Sözcüğü
Latte'nin ayrıştırıcısına {debug} ile {/debug} arasındaki içeriği işlemesini nasıl
söyleriz? İşte burada yield anahtar sözcüğü devreye girer.
create() fonksiyonunda yield kullanıldığında, fonksiyon bir PHP Generator'ı hâline gelir. Çalışması duraklar
ve denetim ana TemplateParser'a döner. TemplateParser daha sonra şablon içeriğini ayrıştırmaya,
ilgili kapanış etiketiyle (bizim durumumuzda {/debug}) karşılaşana kadar devam eder.
Kapanış etiketi bulununca TemplateParser, create() fonksiyonumuzun çalışmasını tam olarak
yield deyiminin ardından sürdürür. yield'in döndürdüğü değer, iki öğe içeren bir
dizidir:
- Başlangıç ve bitiş etiketleri arasında ayrıştırılan içeriği temsil eden bir
AreaNode. - Kapanış etiketini temsil eden
Tagnesnesi (örneğin{/debug}).
DebugNode sınıfını ve yield kullanan create metodunu oluşturalım.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Ayrıştırılan iç içeriği saklayacak public özellik
public AreaNode $content;
/**
* Çiftli {debug} ... {/debug} etiketi için ayrıştırma fonksiyonu.
*/
public static function create(Tag $tag): \Generator // dönüş türüne dikkat edin
{
$node = $tag->node = new self;
// Ayrıştırmayı duraklat, {/debug} bulununca iç içeriği ve bitiş etiketini al
[$node->content, $endTag] = yield;
return $node;
}
// ... print() ve getIterator() bir sonraki adımda uygulanacak ...
}
Not: Etiket n:attribute olarak, yani <div n:debug>...</div> şeklinde kullanılırsa
$endTag değeri null olur.
Çiftli bir etiket {debug/} (veya <div n:debug/>) gibi eğik çizgiyle de kapatılabilir. Bu
durumda iç içeriği olmaz: generator [$emptyFragmentNode, $startTag] alır; burada ikinci öğe null
değil, başlangıç etiketinin kendisidir.
Koşullu Render İçin print() Uygulaması
print() metodunun artık çalışma zamanında appDevMode provider'ını denetleyen ve iç içeriğin
kodunu yalnızca bayrak doğruysa çalıştıran PHP kodu üretmesi gerekiyor.
public function print(PrintContext $context): string
{
// Çalışma zamanında provider'ı denetleyen bir PHP 'if' deyimi üret
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Geliştirme modundaysak iç içeriği yazdır
%node
}
XX,
$this->position, // %line yorumu için
$this->content, // İç içeriğin AST'sini tutan node
);
}
Bu basit bir iş. Standart bir PHP if deyimi oluşturmak için PrintContext::format() kullanıyoruz.
if'in içine $this->content için %node yer tutucusunu yerleştiriyoruz. Latte, etiketin
iç kısmı için PHP kodu üretmek üzere özyinelemeli olarak $this->content->print($context) çağıracak,
ancak bu kod yalnızca çalışma zamanında $this->global->appDevMode doğru olarak değerlendirilirse
çalışacak.
İçerik İçin getIterator() Uygulaması
Önceki örnekteki argüman node'unda olduğu gibi, DebugNode'umuzun artık bir alt node'u var:
AreaNode $content. Onu getIterator() içinde yield ederek gezilebilir kılmalıyız:
public function &getIterator(): \Generator
{
// İçerik node'una referansı yield et
yield $this->content;
}
Bu, compiler pass'lerin {debug} etiketimizin içeriğine inebilmesini sağlar; içerik koşullu render edilse bile
bu önemlidir. Örneğin Sandbox'ın, appDevMode doğru olsun ya da olmasın içeriği analiz etmesi gerekir.
Kayıt ve Kullanım
Etiketi ve provider'ı extension'ınızda kaydedin:
class MyLatteExtension extends Extension
{
// $isDevelopmentMode'un bir yerde belirlendiğini varsayıyoruz (örneğin yapılandırmadan)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Yeni etiketi kaydet
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Provider'ı kaydet
];
}
}
// Extension'ı kaydederken:
$isDev = true; // Bunu uygulamanızın ortamına göre belirleyin
$latte->addExtension(new MyLatteExtension($isDev));
Ve şablonda kullanın:
<p>Her zaman görünen normal içerik.</p>
{debug}
<div class="debug-panel">
Geçerli kullanıcı ID: {$user->id}
İstek zamanı: {=time()}
</div>
{/debug}
<p>Daha fazla normal içerik.</p>
n:attribute Entegrasyonu
Latte, birçok çiftli etiket için kullanışlı bir kısayol sunar: n:attribute'lar. {tag}...{/tag} gibi çiftli bir
etiketiniz varsa ve etkisinin doğrudan tek bir HTML elemanına uygulanmasını istiyorsanız, çoğu zaman bunu o eleman
üzerinde bir n:tag niteliği olarak daha kısa yazabilirsiniz.
Tanımladığınız çoğu standart çiftli etiket için (bizim {debug} etiketimiz gibi) Latte, karşılık gelen
n: nitelik sürümünü otomatik olarak etkinleştirir. Kayıt sırasında fazladan bir şey yapmanız gerekmez:
{* Standart çiftli etiket kullanımı *}
{debug}<div>Hata ayıklama bilgisi</div>{/debug}
{* n:attribute ile eşdeğer kullanım *}
<div n:debug>Hata ayıklama bilgisi</div>
Her ikisi de <div> elemanını yalnızca $this->global->appDevMode doğruysa render eder.
inner- ve tag- önekleri de beklendiği gibi çalışır.
Bazen etiketinizin mantığının, standart çiftli etiket olarak mı yoksa n:attribute olarak mı kullanıldığına ya da
n:inner-tag veya n:tag-tag gibi bir önek kullanılıp kullanılmadığına göre biraz farklı
davranması gerekebilir. create() ayrıştırma fonksiyonunuza aktarılan Latte\Compiler\Tag nesnesi bu
bilgiyi sağlar:
$tag->isNAttribute(): bool: Etiket n:attribute olarak ayrıştırılıyorsatruedöndürür$tag->prefix: ?string: n:attribute ile kullanılan öneki döndürür;null(n:attribute değil),Tag::PrefixNone,Tag::PrefixInnerveyaTag::PrefixTagolabilir
Artık basit etiketleri, argüman ayrıştırmayı, çiftli etiketleri, provider'ları ve n:attribute'ları anladığımıza
göre, {debug} etiketimizden yola çıkarak başka etiketlerin içine yerleşen etiketleri kapsayan daha karmaşık
bir senaryoya geçelim.
Ara Etiketler
Bazı çiftli etiketler, son kapanış etiketinden önce içlerinde başka etiketlerin görünmesine izin verir, hatta
bunu gerektirir. Bunlara ara etiketler denir. Klasik örnekler arasında {if}...{elseif}...{else}...{/if} veya
{switch}...{case}...{default}...{/switch} yer alır.
{debug} etiketimizi, uygulama geliştirme modunda değilken render edilecek isteğe bağlı bir
{else} yan tümcesini destekleyecek şekilde genişletelim.
Hedef: {debug} etiketini isteğe bağlı bir {else} ara etiketini destekleyecek şekilde
değiştirmek. Nihai söz dizimi {debug} ... {else} ... {/debug} olmalı.
Ara Etiketleri yield ile Ayrıştırma
yield'in ayrıştırma fonksiyonu create()'i duraklattığını ve ayrıştırılan içeriği bitiş
etiketiyle birlikte döndürdüğünü zaten biliyoruz. Ancak yield daha fazla denetim sunar: ona ara etiket
adlarından oluşan bir dizi verebilirsiniz. Ayrıştırıcı bu belirtilen etiketlerden herhangi biriyle aynı iç içe
geçme düzeyinde karşılaştığında (yani üst etiketin doğrudan alt öğeleri olarak, içindeki başka blokların veya
etiketlerin içinde değil), içeriği ayrıştırmayı yine durdurur.
Ayrıştırma bir ara etiket nedeniyle durduğunda, içeriği ayrıştırmayı bırakır, create() generator'ını
yeniden başlatır ve kısmen ayrıştırılmış içeriği son bitiş etiketi yerine ara etiketin kendisiyle birlikte geri
verir. create() fonksiyonumuz bu ara etiketi işleyebilir (örneğin varsa argümanlarını ayrıştırabilir) ve
içeriğin bir sonraki bölümünü son bitiş etiketi ya da beklenen başka bir ara etiket bulunana kadar
ayrıştırmak için yeniden yield edebilir.
DebugNode::create() metodunu {else} bekleyecek şekilde değiştirelim:
<?php
namespace App\Templating;
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
{
// {debug} kısmının içeriği
public AreaNode $thenContent;
// {else} kısmının isteğe bağlı içeriği
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield et ve ya {/debug} ya da {else} bekle
[$node->thenContent, $nextTag] = yield ['else'];
// Durduğumuz etiket {else} miydi diye denetle
if ($nextTag?->name === 'else') {
// {else} ile {/debug} arasındaki içeriği ayrıştırmak için yeniden yield et
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() ve getIterator() bir sonraki adımda güncellenecek ...
}
Artık yield ['else'], Latte'ye yalnızca {/debug} için değil, {else} için de
ayrıştırmayı durdurmasını söyler. {else} ile karşılaşılırsa $nextTag, {else}
için Tag nesnesini içerir. Ardından argümansız olarak yeniden yield ederiz; bu, artık yalnızca
son {/debug} etiketini beklediğimiz anlamına gelir ve sonucu $node->elseContent içinde saklarız.
{else} bulunmasaydı, $nextTag {/debug} için Tag olurdu (ya da n:attribute
olarak kullanılmışsa null) ve $node->elseContent null olarak kalırdı.
{else} ile print() Uygulaması
print() metodunun yeni yapıyı yansıtması gerekiyor. appDevMode provider'ına dayalı bir PHP
if/else deyimi üretmeli.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // 'then' dalının kodu ({debug} içeriği)
} else {
%node // 'else' dalının kodu ({else} içeriği)
}
XX,
$this->position, // 'if' koşulu için satır numarası
$this->thenContent, // İlk %node yer tutucusu
$this->elseContent ?? new NopNode, // İkinci %node yer tutucusu
);
}
Bu standart bir PHP if/else yapısıdır. %node'u iki kez kullanıyoruz; format(),
verilen node'ları sırayla yerine koyar. $this->elseContent null olduğunda hata çıkmaması için
?? new NopNode kullanıyoruz – NopNode hiçbir şey yazdırmaz.
Her İki İçerik İçin getIterator() Uygulaması
Artık potansiyel olarak iki alt içerik node'umuz var ($thenContent ve $elseContent). Varsa her
ikisini de yield etmeliyiz:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Geliştirilmiş Etiketin Kullanımı
Etiket artık isteğe bağlı bir {else} yan tümcesiyle kullanılabilir:
{debug}
<p>devMode AÇIK olduğu için hata ayıklama bilgisi gösteriliyor.</p>
{else}
<p>devMode KAPALI olduğu için hata ayıklama bilgisi gizli.</p>
{/debug}
Durum ve İç İçe Geçmeyi Ele Alma
Önceki örneklerimiz ({datetime}, {debug}) print() metotları içinde görece
durumsuzdu. Ya doğrudan içerik çıktılıyor ya da genel bir provider'a dayalı basit bir koşul denetimi yapıyorlardı. Ancak
birçok etiketin render sırasında bir tür durum yönetmesi ya da performans veya doğruluk açısından yalnızca bir
kez çalıştırılması gereken kullanıcı ifadelerini değerlendirmesi gerekir. Ayrıca özel etiketlerimiz iç içe
geçtiğinde ne olacağını da düşünmeliyiz.
Bu kavramları, {repeat $count}...{/repeat} etiketi oluşturarak örnekleyelim. Bu etiket iç içeriğini
$count kez tekrarlayacak.
Hedef: İçeriğini belirtilen sayıda tekrarlayan {repeat $count} etiketini uygulamak.
Geçici ve Benzersiz Değişkenlere Duyulan İhtiyaç
Kullanıcının şunu yazdığını düşünün:
{repeat rand(1, 5)} İçerik {/repeat}
print() metodumuzda saf bir yaklaşımla şöyle bir PHP for döngüsü üretseydik:
// Basitleştirilmiş, YANLIŞ üretilmiş kod
for ($i = 0; $i < rand(1, 5); $i++) {
// içeriği yazdır
}
Bu yanlış olurdu! rand(1, 5) ifadesi her döngü yinelemesinde yeniden değerlendirilir ve
öngörülemeyen sayıda tekrara yol açardı. $count ifadesini döngü başlamadan bir kez değerlendirip
sonucunu saklamamız gerekir.
Önce sayı ifadesini değerlendirip sonucunu bir geçici çalışma zamanı değişkeninde saklayan PHP kodu
üreteceğiz. Şablon kullanıcısının tanımladığı değişkenlerle ve Latte'nin dahili değişkenleriyle
($ʟ_... gibi) çakışmamak için geçici değişkenlerimizde $__ (çift alt çizgi) öneki
uzlaşımını kullanacağız.
Üretilen kod o zaman şöyle görünür:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// içeriği yazdır
}
Şimdi iç içe geçmeyi düşünün:
{repeat $countA} {* Dış döngü *}
{repeat $countB} {* İç döngü *}
...
{/repeat}
{/repeat}
Hem dış hem iç {repeat} etiketleri aynı geçici değişken adlarını (örneğin $__count
ve $__i) kullanan kod üretseydi, iç döngü dış döngünün değişkenlerinin üzerine yazar ve mantık
bozulurdu.
{repeat} etiketinin her örneği için üretilen geçici değişkenlerin benzersiz olmasını
sağlamalıyız. Bunu PrintContext::generateId() ile başarıyoruz. Bu metot, derleme aşamasında benzersiz bir tam
sayı döndürür. Bu ID'yi geçici değişken adlarımıza ekleyebiliriz.
Yani $__count yerine $__count_0 gibi benzersiz sayısal sonek taşıyan bir ad, döngü sayacı için
de benzer şekilde örneğin $__i_0 üreteceğiz. Gerçek sayılar, tüm node'ların paylaştığı derleme
genelindeki bir sayaçtan gelir; dolayısıyla yalnızca benzersiz olmaları garantidir, etiket başına bir dizi
oluşturmaları değil.
RepeatNode'un Uygulanması
Node sınıfını oluşturalım.
<?php
namespace App\Templating;
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;
/**
* {repeat $count} ... {/repeat} için ayrıştırma fonksiyonu
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // $count verildiğinden emin ol
$node = $tag->node = new self;
// Sayı ifadesini ayrıştır
$node->count = $tag->parser->parseExpression();
// İç içeriği al
[$node->content] = yield;
return $node;
}
/**
* Benzersiz değişken adlarıyla PHP 'for' döngüsünü üretir.
*/
public function print(PrintContext $context): string
{
// Benzersiz değişken adları üret
$id = $context->generateId();
$countVar = '$__count_' . $id; // benzersiz ad, örneğin $__count_0
$iteratorVar = '$__i_' . $id; // benzersiz ad, örneğin $__i_0
return $context->format(
<<<'XX'
// Sayı ifadesini *bir kez* değerlendir ve sakla
%raw = (int) (%node);
// Saklanan sayıyı ve benzersiz sayaç değişkenini kullanarak döngü kur
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // İç içeriği render et
}
XX,
$countVar, // %0 - Sayının saklanacağı değişken
$this->count, // %1 - Sayı için ifade node'u
$iteratorVar, // %2 - Döngü sayacı değişkeninin adı
$this->position, // %3 - Döngünün kendisi için satır numarası yorumu
$this->content // %4 - İç içerik node'u
);
}
/**
* Alt node'ları (sayı ifadesi ve içerik) yield eder.
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
create() metodu, gerekli $count ifadesini parseExpression() ile ayrıştırır. Önce
$tag->expectArguments() çağrılır. Bu, kullanıcının {repeat} sonrasında bir şey
verdiğini garantiler. $tag->parser->parseExpression() hiçbir şey verilmediğinde zaten başarısız olurdu,
ama hata mesajı beklenmedik söz dizimiyle ilgili olabilirdi. expectArguments() kullanmak, {repeat}
etiketi için argümanların eksik olduğunu açıkça belirten çok daha net bir hata verir.
print() metodu, tekrar mantığını çalışma zamanında yürütecek PHP kodunu üretir. İşe, ihtiyaç
duyacağı geçici PHP değişkenleri için benzersiz adlar üreterek başlar.
$context->format() metodu, karşılık gelen argüman olarak verilen ham dizeyi ekleyen yeni
%raw yer tutucusuyla çağrılır. Burada $countVar içinde saklanan benzersiz değişken adını
(örneğin $__count_1) ekler. Peki %0.raw ve %2.raw nedir? Bunlar konumsal yer
tutucuları gösterir. Sıradaki ham argümanı alan basit %raw'ın aksine, %2.raw açıkça
2 numaralı indeksteki argümanı (yani $iteratorVar) alır ve onun ham dize değerini ekler. Bu,
$iteratorVar dizesini format()'ın argüman listesine birden çok kez geçirmeden yeniden kullanmamızı
sağlar.
Özenle kurulmuş bu format() çağrısı, sayı ifadesini doğru şekilde ele alan ve {repeat}
etiketleri iç içe geçse bile değişken adı çakışmalarını önleyen verimli ve güvenli bir PHP döngüsü üretir.
Kayıt ve Kullanım
Etiketi extension'ınızda kaydedin:
use App\Templating\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // repeat etiketini kaydet
];
}
}
İç içe kullanım da dahil olmak üzere şablonda kullanın:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>İç döngü</td>
{/repeat}
</tr>
{/repeat}
Bu örnek, durumun (döngü sayaçları) ve olası iç içe geçme sorunlarının, $__ önekli geçici
değişkenler ve PrintContext::generateId()'den gelen ID'lerle benzersizleştirme kullanılarak nasıl ele
alınacağını gösterir.
Saf n:attribute'lar
n:if veya n:foreach gibi birçok n:attribute, çiftli etiket karşılıklarının
({if}...{/if}, {foreach}...{/foreach}) kullanışlı kısayolu olarak hizmet ederken, Latte
yalnızca n:attribute biçiminde var olan etiketler tanımlamanıza da izin verir. Bunlar genellikle bağlı oldukları
HTML elemanının niteliklerini veya davranışını değiştirmek için kullanılır.
Latte'ye gömülü standart örnekler arasında, class niteliğini dinamik olarak kurmaya yardımcı olan n:class ve birden çok rastgele nitelik ayarlayabilen n:attr bulunur.
Kendi saf n:attribute'umuzu oluşturalım: bir eylem (bağlantıyı izleme veya form gönderme gibi) gerçekleştirilmeden
önce JavaScript onay iletişim kutusu ekleyecek olan n:confirm.
Hedef: Kullanıcı onay iletişim kutusunu iptal ederse varsayılan eylemi engelleyen bir onclick
işleyicisi ekleyen n:confirm="'Emin misiniz?'" uygulamak.
ConfirmNode'un Uygulanması
Bir Node sınıfına ve bir ayrıştırma fonksiyonuna ihtiyacımız var.
<?php
namespace App\Templating;
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;
}
/**
* 'onclick' niteliğinin kodunu doğru kaçışla üretir.
*/
public function print(PrintContext $context): string
{
// Hem JavaScript hem de HTML nitelik bağlamları için doğru kaçışı sağlar.
return $context->format(
<<<'XX'
echo ' onclick="', LR\HtmlHelpers::escapeAttr('return confirm(' . LR\Helpers::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
print() metodu, şablon render edilirken nihayetinde onclick="..." HTML niteliğini çıktılayacak
PHP kodunu üretir. İç içe bağlamları (HTML niteliği içindeki JavaScript) ele almak dikkatli kaçış gerektirir.
LR\Helpers::escapeJs(%node) yardımcısı çalışma zamanında çağrılır ve mesajı JavaScript içinde
kullanılmak üzere doğru şekilde kaçışlar (çıktı "Emin misiniz?" gibi olur). Ardından
LR\HtmlHelpers::escapeAttr(...) yardımcısı, HTML niteliklerinde özel olan karakterleri kaçışlar; böylece
çıktı return confirm("Emin misiniz?") hâline gelir. Bu iki aşamalı çalışma zamanı
kaçışı, mesajın JavaScript için güvenli olmasını ve ortaya çıkan JavaScript kodunun HTML onclick
niteliğine gömülmek için güvenli olmasını sağlar.
Kayıt ve Kullanım
n:attribute'u extension'ınızda kaydedin. Anahtardaki n: önekini unutmayın:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // n:confirm'i kaydet
];
}
}
Artık n:confirm'i bağlantılarda, düğmelerde veya form elemanlarında kullanabilirsiniz:
<a href="delete.php?id=123" n:confirm='"{$id} numaralı öğeyi gerçekten silmek istiyor musunuz?"'>Sil</a>
Üretilen HTML:
<a href="delete.php?id=123" onclick="return confirm("123 numaralı öğeyi gerçekten silmek istiyor musunuz?")">Sil</a>
Kullanıcı bağlantıya tıkladığında tarayıcı onclick kodunu çalıştırır, onay iletişim kutusunu
gösterir ve yalnızca kullanıcı "Tamam"a tıklarsa delete.php'ye geçer.
Bu örnek, saf bir n:attribute'un print() metodunda uygun PHP kodu üreterek bağlı olduğu HTML elemanının
davranışını veya niteliklerini nasıl değiştirebileceğini gösterir. Sıkça gereken çift kaçışı unutmayın: bir kez
hedef bağlam için (bu durumda JavaScript) ve bir kez daha HTML nitelik bağlamı için.
Saf n:attribute'lar yazarken Tag nesnesinin iki üyesi daha işe yarar: $tag->htmlElement size
çevreleyen HTML elemanına (bir ElementNode) erişim verir, böylece onu inceleyebilir veya ayarlayabilirsiniz;
$tag->replaceNAttribute($node) ise niteliği kendi kurduğunuz bir node'la takas etmenizi sağlar. Nitekim saf bir
n:attribute'un create() metodundan döndürülen node, elemanı üzerindeki niteliğin yerini otomatik
olarak alır.
İleri Düzey Konular
Önceki bölümler temel kavramları kapsasa da, özel Latte etiketleri oluştururken karşılaşabileceğiniz birkaç ileri düzey konu daha var.
Etiket Çıktı Modları
create() fonksiyonunuza aktarılan Tag nesnesinin outputMode adlı bir özelliği
vardır. Bu özellik, özellikle etiket kendi satırında kullanıldığında, Latte'nin çevredeki boşlukları ve girintiyi
nasıl ele alacağını etkiler. Bu özelliği create() fonksiyonunuz içinde değiştirebilirsiniz.
Tag::OutputNone(her etiket için varsayılan;{if}veya{foreach}gibi denetim yapıları da bunu korur): etiketin çevresindeki boşluk tam olarakOutputRemoveIndentation'daki gibi ele alınır – baştaki girinti ve sondaki tek bir satır sonu kaldırılır. Asıl fark içseldir: bu mod, şablon ayrıştırıcısını şablonun “head” kipinde tutar.{var}veya{default}gibi doğrudan çıktı üretmeyen bildirim ya da hazırlık etiketlerine uygundur.Tag::OutputRemoveIndentation({block},{embed},{include}ve{sandbox}blok etiketleri tarafından açıkça ayarlanır): etiketten önceki baştaki girintiyi ve sondaki tek bir satır sonunu kaldırır. Bu, üretilen PHP kodunu daha temiz tutmaya yardımcı olur ve etiketin kendisinin HTML çıktısında yol açtığı fazladan boş satırları önler.Tag::OutputKeepIndentation({=...}gibi çıktı etiketleri tarafından açıkça ayarlanır): Latte, etiketten önceki girintiyi korumaya çalışır; etiketten sonraki satır sonları genellikle korunur. Bu, içeriği satır içinde çıktılayan etiketler için uygundur – tam da bu nedenle bu modu ayarlayan yukarıdaki{datetime}örneğine bakın.
Etiketinizin amacına en uygun modu seçin. Varsayılan OutputNone olduğundan, akış denetimi ve bildirim
etiketlerinde değişiklik gerekmez; kendi satırında içerik yazdıran etiketler için OutputKeepIndentation
ayarlayın.
Üst/En Yakın Etiketlere Erişim
Bazen bir etiketin davranışının, kullanıldığı bağlama, özellikle hangi üst etiket(ler)in içinde yer aldığına
bağlı olması gerekir. create() fonksiyonunuza aktarılan Tag nesnesi, tam da bu amaçla
closestTag(array $classes, ?callable $condition = null): ?Tag metodunu sağlar.
Bu metot, o an açık olan Latte etiketlerinin hiyerarşisinde ($tag->parent zincirinde; çevreleyen HTML
elemanları bu zincirin parçası değildir) yukarı doğru arama yapar ve belirli ölçütlere uyan en yakın atanın
Tag nesnesini döndürür. Uyan bir ata bulunamazsa null döndürür.
$classes dizisi, ne tür ata etiketler aradığınızı belirtir. Ata etiketin ilişkili node'unun
($ancestorTag->node) sınıfının, listelenen sınıflardan tam olarak biri olup olmadığı denetlenir; alt
sınıflar eşleşmez.
function create(Tag $tag)
{
// Node'u ForeachNode örneği olan en yakın ata etiketi ara
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// ForeachNode örneğinin kendisine erişebiliriz:
$foreachNode = $foreachTag->node;
}
}
$foreachTag->node ifadesine dikkat edin: Bu yalnızca, Latte etiketi geliştirmede oluşturulan node'un
create() metodu içinde hemen $tag->node'a atanmasının bir uzlaşım olması sayesinde çalışır;
bizim hep yaptığımız gibi.
Bazen yalnızca node türünü eşleştirmek yeterli olmaz. Olası ata etiketin veya node'unun belirli bir özelliğini
denetlemeniz gerekebilir. closestTag()'in isteğe bağlı ikinci argümanı, olası ata Tag nesnesini
alan ve geçerli bir eşleşme olup olmadığını döndürmesi gereken bir callable'dır.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Koşul: blok dinamik olmalı
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
closestTag() kullanmak, bağlama duyarlı etiketler oluşturmanıza ve şablon yapınız içinde doğru
kullanımı zorunlu kılmanıza olanak tanır; bu da daha sağlam ve anlaşılır şablonlar demektir.
PrintContext::format() Yer Tutucuları
Node'larımızın print() metotlarında PHP kodu üretmek için sık sık PrintContext::format()
kullandık. Bu metot bir maske dizesi ve maskedeki yer tutucuların yerine geçen argümanlar alır. İşte kullanılabilir yer
tutucuların bir özeti:
%node: Argüman birNodeörneği olmalıdır. Node'unprint()metodunu çağırır ve ortaya çıkan PHP kodu dizesini ekler.%dump: Argüman herhangi bir PHP değeridir. Değeri geçerli PHP koduna dışa aktarır. Skalerler, diziler ve null için uygundur.$context->format('echo %dump;', 'Hello')→echo 'Hello';$context->format('$arr = %dump;', [1, 2])→$arr = [1, 2];
%raw: Argümanı hiçbir kaçış veya değişiklik yapmadan doğrudan çıktı PHP koduna ekler. Dikkatli kullanın; öncelikle önceden üretilmiş PHP kodu parçalarını veya değişken adlarını eklemek içindir.$context->format('%raw = 1;', '$variableName')→$variableName = 1;
%args: Argüman birExpression\ArrayNodeolmalıdır. Dizi öğelerini bir fonksiyon veya metot çağrısının argümanları biçiminde yazdırır (virgülle ayrılmış, varsa adlandırılmış argümanları da işleyerek).$argsNode = new ArrayNode([...]);$context->format('myFunc(%args);', $argsNode)→myFunc(1, name: 'Joe');
%line: Argüman birPosition(veyaRange) nesnesi olmalıdır (genellikle$this->position). Kaynak satırı ve sütunu gösteren/* pos X:Y */biçiminde bir PHP yorumu ekler.$context->format('echo "Hi" %line;', $this->position)→echo "Hi" /* pos 42:1 */;
%escape(...): Çalışma zamanında, içteki ifadeyi geçerli bağlama duyarlı kaçış kurallarıyla kaçışlayacak PHP kodu üretir.$context->format('echo %escape(%node);', $variableNode)
%modify(...): Argüman birModifierNodeolmalıdır.ModifierNode'da belirtilen filtreleri iç içeriğe uygulayan PHP kodu üretir;|noescapeile devre dışı bırakılmadıysa bağlama duyarlı kaçışı da içerir.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...):%modify'a benzer, ancak yakalanan içerik bloklarını (genellikle HTML) değiştirmek için tasarlanmıştır.
Argümanlara sıfırdan başlayan indeksleriyle açıkça başvurabilirsiniz: %0.node, %1.dump,
%2.raw vb. Bu, bir argümanı format()'a defalarca geçirmeden maskede birden çok kez kullanmanızı
sağlar. %0.raw ve %2.raw'ın kullanıldığı {repeat} etiketi örneğine bakın.
Karmaşık Argüman Ayrıştırma Örneği
parseExpression(), parseArguments() ve benzerleri birçok durumu karşılasa da, bazen
$tag->parser->stream üzerinden erişilen daha düşük seviyeli TokenStream ile daha ayrıntılı
ayrıştırma mantığı yazmanız gerekir.
Hedef: {embedYoutube $videoID, width: 640, height: 480} etiketini oluşturmak. Zorunlu bir video ID'sini
(dize veya değişken), ardından boyutlar için isteğe bağlı anahtar-değer çiftlerini ayrıştırmak istiyoruz.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;
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;
// Zorunlu video ID'sini ayrıştır
$node->videoId = $tag->parser->parseExpression();
// İsteğe bağlı anahtar-değer çiftlerini ayrıştır
$stream = $tag->parser->stream; // Token akışını al
while ($stream->tryConsume(',')) { // Virgülle ayrılmış olmalı
// 'width' veya 'height' tanımlayıcısı bekle
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // İki nokta ayırıcısını bekle
$value = $tag->parser->parseExpression(); // Değer ifadesini ayrıştır
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Unknown argument '$key'. Expected 'width' or 'height'.", $keyToken->position);
}
}
return $node;
}
// ... print() ve getIterator() ...
}
Bu düzeyde denetim, token akışıyla doğrudan etkileşerek özel etiketleriniz için çok özgül ve karmaşık söz dizimleri tanımlamanıza olanak tanır.
AuxiliaryNode Kullanımı
Latte, kod üretimi sırasındaki veya compiler pass'ler içindeki özel durumlar için genel amaçlı “yardımcı”
node'lar sunar. Bunlar AuxiliaryNode ve Php\Expression\AuxiliaryNode'dur.
AuxiliaryNode'u, temel işlevlerini – kod üretimi ve alt node'ları açığa çıkarma – yapıcısına
verilen argümanlara devreden esnek bir kapsayıcı node olarak düşünün:
print()devri: İlk yapıcı argümanı bir PHP closure'ıdır. Latte birAuxiliaryNodeüzerindeprint()metodunu çağırdığında, verilen bu closure'ı çalıştırır. Closure,PrintContext'i ve ikinci yapıcı argümanında verilen node'ları alır; böylece tamamen özel PHP kodu üretme mantığını anında tanımlayabilirsiniz.getIterator()devri: İkinci yapıcı argümanı birNodenesneleri dizisidir. Latte'nin birAuxiliaryNode'un alt öğelerini gezmesi gerektiğinde (örneğin compiler pass'ler sırasında),getIterator()metodu yalnızca bu dizide verilen node'ları yield eder.
Örnek:
$node = new AuxiliaryNode(
// 1. Bu closure, print()'in gövdesi olur
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Bu node'lar getIterator() tarafından yield edilir ve yukarıdaki closure'a aktarılır
[$argumentNode1, $argumentNode2]
);
Latte, üretilen kodu nereye eklemeniz gerektiğine bağlı olarak iki ayrı tür sunar:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: Bir ifade temsil eden PHP kodu parçası üretmeniz gerektiğinde bunu kullanınLatte\Compiler\Nodes\AuxiliaryNode: Bir veya daha fazla deyim temsil eden bir PHP kodu bloğu eklemeniz gereken daha genel amaçlar için bunu kullanın
print() metodunuz veya bir compiler pass içinde standart node'lar (StaticMethodCallNode gibi) yerine
AuxiliaryNode kullanmanın önemli nedeni, sonraki compiler pass'ler için görünürlüğü denetlemektir;
özellikle Sandbox gibi güvenlikle ilgili olanlar için.
Şu senaryoyu düşünün: Compiler pass'inizin, kullanıcı tarafından verilen bir ifadeyi ($userExpr) belirli
ve güvenilir bir yardımcı fonksiyon çağrısıyla, myInternalSanitize($userExpr) ile sarmalaması gerekiyor.
new FunctionCallNode('myInternalSanitize', [$userExpr]) gibi standart bir node oluşturursanız, bu node AST
gezicisine tamamen görünür olur. Daha sonra bir Sandbox pass'i çalışırsa ve myInternalSanitize onun izin
listesinde değilse, Sandbox bu çağrıyı engelleyebilir veya değiştirebilir; siz, etiketin yazarı olarak bu
çağrının güvenli ve gerekli olduğunu bilseniz bile etiketinizin iç mantığı bozulabilir. Bu yüzden çağrıyı doğrudan
AuxiliaryNode'un closure'ı içinde üretebilirsiniz.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... print() içinde veya bir compiler pass içinde ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Doğrudan PHP kodu üretimi
$userExpr,
),
// ÖNEMLİ: Özgün kullanıcı ifadesi node'unu yine de buraya geçirin!
[$userExpr],
);
Bu durumda Sandbox pass'i AuxiliaryNode'u görür ama closure'ının ürettiği PHP kodunu analiz etmez.
Closure'ın içinde üretilen myInternalSanitize çağrısını doğrudan engelleyemez.
Üretilen PHP kodunun kendisi pass'lerden gizlense de, bu kodun girdileri (kullanıcı verisini veya ifadelerini temsil
eden node'lar) yine de gezilebilir kılınmalıdır. AuxiliaryNode yapıcısının ikinci argümanı bu
yüzden kritiktir. Closure'ınızın kullandığı özgün node'ların (yukarıdaki örnekteki $userExpr gibi)
tümünü içeren bir diziyi geçirmek zorundasınız. AuxiliaryNode'un getIterator() metodu
bu node'ları yield eder ve böylece Sandbox gibi compiler pass'lerin onları olası sorunlar açısından analiz etmesine
olanak tanır.
En İyi Uygulamalar
- Net Amaç: Etiketinizin net ve gerekli bir amacı olduğundan emin olun. Filtrelerle veya fonksiyonlarla kolayca çözülebilecek işler için etiket oluşturmayın.
getIterator()'ı Doğru Uygulayın: Her zamangetIterator()uygulayın ve şablondan ayrıştırılan tüm alt node'lara (argümanlar, içerik) referans (&) yield edin. Bu; compiler pass'ler, güvenlik (Sandbox) ve olası gelecekteki optimizasyonlar için şarttır.- Node'lar İçin Public Özellikler: Alt node'ları tutan özellikleri public yapın ki compiler pass'ler gerektiğinde bunları değiştirebilsin.
PrintContext::format()Kullanın: PHP kodu üretmek içinformat()metodundan yararlanın. Tırnaklamayı, yer tutucuların doğru kaçışını üstlenir ve satır numarası yorumlarını otomatik ekler.- Geçici Değişkenler (
$__): Geçici değişken gerektiren çalışma zamanı PHP kodu üretirken (örneğin ara sonuçları veya döngü sayaçlarını saklamak için), kullanıcı değişkenleriyle ve Latte'nin dahili$ʟ_değişkenleriyle çakışmamak için$__öneki uzlaşımını kullanın. - İç İçe Geçme ve Benzersiz ID'ler: Etiketiniz iç içe geçebiliyorsa veya çalışma zamanında örneğe özgü
duruma ihtiyaç duyuyorsa,
$__geçici değişkenleriniz için benzersiz sonekler oluşturmak üzereprint()metodunuzda$context->generateId()kullanın. - Dış Veri İçin Provider'lar: Çalışma zamanı verilerine veya servislerine erişmek için değerleri sabit
kodlamak ya da genel duruma güvenmek yerine ($this->global->… ile) Provider'ları kullanın
(
Extension::getProviders()ile kaydedilir). Provider adlarında üretici öneki kullanın. - n:attribute'ları Değerlendirin: Çiftli etiketiniz mantıksal olarak tek bir HTML elemanı üzerinde çalışıyorsa,
Latte muhtemelen otomatik
n:attributedesteği sağlar. Kullanıcı rahatlığı için bunu aklınızda tutun. Nitelik değiştiren bir etiket oluşturuyorsanız, saf birn:attribute'un en uygun biçim olup olmadığını düşünün. - Test: Etiketleriniz için, hem çeşitli söz dizimi girdilerinin ayrıştırılmasını hem de üretilen PHP kodunun çıktısının doğruluğunu kapsayan testler yazın.
Bu yönergeleri izleyerek, Latte şablon motoruyla kusursuz biçimde bütünleşen güçlü, sağlam ve bakımı kolay özel etiketler oluşturabilirsiniz.
Ayrıştırma sürecinin tüm ince ayrıntılarını öğrenmenin en iyi yolu, Latte'nin parçası olan node sınıflarını incelemektir.