Compiler pass'leri oluşturma
Compiler pass'leri, Latte şablonlarını soyut sözdizim ağacına (AST) ayrıştırıldıktan sonra ve son PHP kodu üretilmeden önce çözümlemek ve değiştirmek için güçlü bir mekanizma sunar. Bu; gelişmiş şablon işleme, iyileştirmeler, güvenlik denetimleri (Sandbox gibi) ve şablon içgörüleri toplamayı mümkün kılar. Bu kılavuz, kendi compiler pass'lerinizi oluşturmanızda size yol gösterecek.
Compiler pass nedir?
Compiler pass'lerin rolünü anlamak için Latte'nin derleme sürecine bakın. Gördüğünüz gibi compiler pass'leri çok önemli bir aşamada çalışır ve ilk ayrıştırma ile son kod çıktısı arasında derin bir müdahaleye olanak tanır.
Özünde bir compiler pass, tek bir argüman kabul eden basit bir PHP callable'ıdır (bir fonksiyon, bir statik metot veya bir
örnek metodu): şablonun AST'sinin kök düğümü; bu her zaman Latte\Compiler\Nodes\TemplateNode'un bir
örneğidir.
Bir compiler pass'in birincil amacı genellikle şunlardan biri ya da her ikisidir:
- Çözümleme: AST'yi dolaşmak ve şablon hakkında bilgi toplamak (örneğin tanımlanmış tüm blokları bulmak, belirli bir etiket kullanımını denetlemek, belirli güvenlik kısıtlarının sağlandığından emin olmak).
- Değiştirme: AST yapısını veya düğüm özelliklerini değiştirmek (örneğin otomatik HTML nitelikleri eklemek, belirli etiket bileşimlerini iyileştirmek, kullanımdan kaldırılmış etiketleri yenileriyle değiştirmek, sandbox kurallarını uygulamak).
Kayıt
Compiler pass'leri, bir uzantının
getPasses() metodu üzerinden kaydedilir. Bu metot, anahtarları pass'ler için benzersiz adlar (içeride ve
sıralama için kullanılır), değerleri ise pass mantığını uygulayan PHP callable'ları olan ilişkisel bir dizi
döndürür.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... diğer pass'ler ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// Uygulama...
}
}
Latte'nin çekirdek uzantılarının ve sizin özel uzantılarınızın kaydettiği pass'ler sırayla çalışır. Sıra
önemli olabilir, özellikle bir pass başka bir pass'in sonuçlarına veya değişikliklerine dayanıyorsa. Latte, gerekirse bu
sırayı denetlemek için bir yardımcı mekanizma sunar; ayrıntılar için Extension::getPasses()
dokümantasyonuna bakın.
AST örneği
AST hakkında daha iyi bir fikir edinmek için bir örnek ekliyoruz. Kaynak şablon şudur:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Ve bu da onun AST biçimindeki gösterimidir:
Latte\Compiler\Nodes\TemplateNode(
Latte\Compiler\Nodes\FragmentNode(
- Latte\Essential\Nodes\ForeachNode(
expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
)
value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
content: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode(' ')
- Latte\Compiler\Nodes\Html\ElementNode('li')(
content: Latte\Compiler\Nodes\PrintNode(
expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
)
modifier: Latte\Compiler\Nodes\Php\ModifierNode(
filters:
- Latte\Compiler\Nodes\Php\FilterNode('upper')
)
)
)
)
else: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode('no items found')
)
)
)
)
AST'yi NodeTraverser ile dolaşma
Karmaşık AST yapısını dolaşmak için elle özyinelemeli fonksiyonlar yazmak sıkıcı ve hataya açıktır. Latte bunun için özel bir araç sunar: Latte\Compiler\NodeTraverser. Bu sınıf, Ziyaretçi tasarım desenini uygular ve AST dolaşmasını sistematik ve yönetilebilir kılar.
Temel kullanım, bir NodeTraverser örneği oluşturmayı ve traverse() metodunu, kök AST
düğümünü ve bir ya da iki “ziyaretçi” callable'ını vererek çağırmayı içerir:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// 'enter' ziyaretçisi: bir düğüme girilirken çağrılır (çocuklarından önce)
enter: function (Node $node) {
echo "Entering node of type: " . $node::class . "\n";
// Düğümü burada inceleyebilirsiniz
if ($node instanceof Nodes\TextNode) {
// echo "Found text: " . $node->content . "\n";
}
},
// 'leave' ziyaretçisi: bir düğümden çıkılırken çağrılır (çocuklarından sonra)
leave: function (Node $node) {
echo "Leaving node of type: " . $node::class . "\n";
// Çocuklar işlendikten sonra burada işlem yapabilirsiniz
},
);
İhtiyacınıza göre yalnızca enter ziyaretçisini, yalnızca leave ziyaretçisini ya da her
ikisini verebilirsiniz.
enter(Node $node): Bu fonksiyon, dolaşıcı o düğümün çocuklarından herhangi birini ziyaret etmeden
önce her düğüm için çalıştırılır. Şunlar için yararlıdır:
- Ağaçta aşağı inerken bilgi toplamak.
- Çocukları işlemeden önce karar vermek (örneğin onları atlamaya karar vermek, bkz. Dolaşmayı iyileştirme).
- Çocuklar ziyaret edilmeden önce düğümü değiştirmek (daha az yaygın).
leave(Node $node): Bu fonksiyon, düğümün tüm çocukları (ve onların tüm alt ağaçları) tamamen
ziyaret edildikten (hem girilip hem çıkıldıktan) sonra her düğüm için çalıştırılır. Şunlar için en yaygın
yerdir:
- Çocukları işlendikten sonra bir düğümü değiştirmek.
- AST'den düğüm kaldırmak.
- Tüm alt ağaçtan toplanan bilgiyi bir araya getirmek.
Hem enter hem leave ziyaretçileri, dolaşma sürecini etkilemek için isteğe bağlı olarak bir
değer döndürebilir. null (veya hiçbir şey) döndürmek dolaşmayı olağan biçimde sürdürür, bir
Node örneği döndürmek geçerli düğümü değiştirir, NodeTraverser::RemoveNode veya
NodeTraverser::StopTraversal gibi özel sabitler döndürmek ise akışı değiştirir; bu, aşağıdaki bölümlerde
anlatılıyor.
Dolaşma nasıl çalışır
NodeTraverser, içeride her Node sınıfının uygulaması gereken getIterator()
metodunu kullanır (Özel etiketler
oluşturma bölümünde ele alındığı gibi). getIterator()'ın verdiği çocuklar üzerinde döner, onlar
üzerinde traverse()'i özyinelemeli çağırır ve enter ile leave ziyaretçilerinin,
yineleyicilerle erişilebilen ağaçtaki her düğüm için doğru derinlik öncelikli sırada çağrılmasını sağlar. Bu,
özel etiket düğümlerinizde doğru uygulanmış bir getIterator()'ın compiler pass'lerin doğru çalışması
için neden kesinlikle gerekli olduğunu bir kez daha vurgular.
Şablonda {do} etiketinin (Latte\Essential\Nodes\DoNode ile temsil edilir) kaç kez
kullanıldığını sayan basit bir pass yazalım.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;
function countDoTags(TemplateNode $templateNode): void
{
$count = 0;
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use (&$count): void {
if ($node instanceof DoNode) {
$count++;
}
},
// bu görev için 'leave' ziyaretçisi gerekmez
);
echo "Found {do} tag $count times.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
Bu örnekte, karşılaşılan her düğümün tipini denetlemek için yalnızca enter ziyaretçisine
ihtiyacımız oldu.
Şimdi bu ziyaretçileri AST'yi gerçekten değiştirmek için nasıl kullanacağımızı inceleyeceğiz.
AST'yi değiştirme
Compiler pass'lerin başlıca amaçlarından biri soyut sözdizim ağacını değiştirmektir. Bu; PHP kodu üretilmeden önce
doğrudan şablon yapısı üzerinde güçlü dönüşümlere, iyileştirmelere ya da kuralların dayatılmasına olanak tanır.
NodeTraverser, bunu enter ve leave ziyaretçilerinin içinde başarmanın birkaç
yolunu sunar.
Önemli not: AST'yi değiştirmek dikkat ister. Yanlış değişiklikler (gerekli düğümleri kaldırmak ya da bir düğümü uyumsuz bir tiple değiştirmek gibi) kod üretimi sırasında hatalara ya da beklenmedik çalışma zamanı davranışına yol açabilir. Değiştirme pass'lerinizi her zaman iyice test edin.
Düğüm özelliklerini değiştirme
Ağacı değiştirmenin en basit yolu, dolaşma sırasında karşılaşılan düğümlerin public özelliklerini doğrudan değiştirmektir. Tüm düğümler, ayrıştırılmış argümanlarını, içeriklerini veya niteliklerini public özelliklerde saklar.
Örnek: Tüm statik metin düğümlerini (TextNode, Latte etiketlerinin dışındaki düz HTML veya metni
temsil eder) bulan ve içeriklerini doğrudan AST'nin içinde büyük harfe çeviren bir pass oluşturalım.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;
function uppercaseStaticText(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// TextNode'un önce işlenecek çocuğu olmadığından 'enter' kullanabiliriz
enter: function (Node $node) {
// Bu düğüm bir statik metin bloğu mu?
if ($node instanceof TextNode) {
// Evet! Public 'content' özelliğini doğrudan değiştir.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// Bir şey döndürmeye gerek yok; değişiklik yerinde olur.
},
);
}
Bu örnekte enter ziyaretçisi, geçerli $node'un bir TextNode olup olmadığını
denetler. Öyleyse, public $content özelliğini mb_strtoupper() ile doğrudan güncelleriz. Bu, AST'de
saklanan statik metin içeriğini PHP kodu üretilmeden önce doğrudan değiştirir. Nesneyi doğrudan değiştirdiğimiz
için ziyaretçiden bir şey döndürmemiz gerekmez.
Etki: Şablon <p>Hello</p>{= $var }<span>World</span> içeriyorsa, bu pass'ten sonra AST
şuna benzer bir şeyi temsil eder: <p>HELLO</p>{= $var }<span>WORLD</span>. Bu,
$var'ın içeriğini ETKİLEMEZ.
Düğümleri değiştirme
Daha güçlü bir değiştirme tekniği, bir düğümü tamamen başka bir düğümle değiştirmektir. Bu, enter
veya leave ziyaretçisinden yeni Node örneğini döndürerek yapılır.
NodeTraverser o zaman özgün düğümü, üst düğümün yapısında döndürülenle değiştirir.
Örnek: PHP_VERSION sabitinin (ConstantFetchNode ile temsil edilir) tüm kullanımlarını
bulan ve onları, derleme sırasında saptanan gerçek PHP sürümünü içeren bir dize sabitiyle
(StringNode) doğrudan değiştiren bir pass oluşturalım. Bu, bir derleme zamanı iyileştirmesi biçimidir.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
function inlinePhpVersion(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// değiştirmelerde sıklıkla 'leave' kullanılır; böylece (varsa) çocukların
// önce işlendiğinden emin olunur, ama burada 'enter' de işe yarardı.
leave: function (Node $node) {
// Bu düğüm bir sabit erişimi ve sabitin adı 'PHP_VERSION' mı?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// Geçerli PHP sürümünü taşıyan yeni bir StringNode oluştur
$newNode = new StringNode(PHP_VERSION);
// İsteğe bağlı ama iyi bir alışkanlık: konum bilgisini kopyala
$newNode->position = $node->position;
// Yeni StringNode'u döndür. Dolaşıcı, özgün ConstantFetchNode'u
// bu $newNode ile değiştirecek.
return $newNode;
}
// Bir Node döndürmezsek özgün $node korunur.
},
);
}
Burada leave ziyaretçisi, PHP_VERSION için belirli ConstantFetchNode'u belirler.
Sonra derleme zamanındaki PHP_VERSION sabitinin değerini içeren tamamen yeni bir StringNode
oluşturur. Bu $newNode'u döndürerek dolaşıcıya, AST'deki özgün
ConstantFetchNode'u değiştirmesini söyler.
Etki: Şablon {= PHP_VERSION } içeriyorsa ve derleme PHP 8.2.1 üzerinde çalışıyorsa, bu pass'ten sonraki
AST fiilen {= '8.2.1' }'i temsil eder.
Değiştirme için enter mi leave mi seçilir:
- Yeni düğümün oluşturulması eski düğümün çocuklarının işlenme sonuçlarına bağlıysa ya da yalnızca
değiştirmeden önce çocukların ziyaret edildiğinden emin olmak istiyorsanız
leavekullanın (yaygın uygulama). - Bir düğümü, çocukları ziyaret edilmeden önce değiştirmek istiyorsanız
enterkullanın.
Düğümleri kaldırma
Bir ziyaretçiden özel NodeTraverser::RemoveNode sabitini döndürerek bir düğümü AST'den tamamen
kaldırabilirsiniz.
Örnek: Tüm HTML yorumlarını (<!-- ... -->) çıktıdan kaldıralım. Latte yorumları
{* ... *} bu yolla hedeflenemez, çünkü ayrıştırıcı içeriklerini atar ve onları özel bir yorum düğümü
yerine boş bir NopNode ile değiştirir; ama HTML yorumları Html\CommentNode düğümleri olarak
korunur, bu yüzden onları burada soyabiliriz.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Html\CommentNode;
function removeHtmlComments(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// bir yorumu kaldırmak için çocuk bilgisine ihtiyacımız olmadığından 'enter' uygundur
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// Dolaşıcıya bu düğümü AST'den kaldırmasını bildir
return NodeTraverser::RemoveNode;
}
},
);
}
Dikkat: RemoveNode'u dikkatle kullanın. Gerekli içerik taşıyan ya da yapıyı etkileyen bir düğümü
kaldırmak (örneğin bir döngünün içerik düğümünü kaldırmak) bozuk şablonlara ya da geçersiz üretilmiş koda yol
açabilir. En güvenlisi, gerçekten isteğe bağlı veya kendi kendine yeten düğümlerdir (yorumlar veya hata ayıklama
etiketleri gibi) ya da boş yapısal düğümlerdir (örneğin boş bir FragmentNode, bazı bağlamlarda bir temizlik
pass'i tarafından güvenle kaldırılabilir).
Bu üç yöntem (özellikleri değiştirmek, düğümleri değiştirmek ve düğümleri kaldırmak) compiler pass'lerinizin içinde AST'yi işlemek için temel araçları sağlar.
Dolaşmayı iyileştirme
Şablon AST'leri epey büyüyebilir, olası binlerce düğüm içerebilir. Pass'iniz yalnızca ağacın belirli bölümleriyle
ilgileniyorsa, her tek düğümü dolaşmak gereksiz olabilir ve derleme performansını etkileyebilir.
NodeTraverser, dolaşmayı iyileştirmenin yollarını sunar:
Çocukları atlama
Belirli bir tipte bir düğümle karşılaştığınızda, torunlarından hiçbirinin aradığınız düğümleri
içeremeyeceğini biliyorsanız, dolaşıcıya onun çocuklarını ziyaret etmemesini söyleyebilirsiniz. Bu,
enter ziyaretçisinden NodeTraverser::DontTraverseChildren sabitini döndürerek yapılır.
Böylece dolaşma yolundan tüm dalları budarsınız; bu, özellikle etiketlerin içinde karmaşık PHP ifadeleri olan
şablonlarda ciddi zaman kazandırabilir.
Dolaşmayı durdurma
Pass'inizin yalnızca bir şeyin ilk geçişini bulması gerekiyorsa (belirli bir düğüm tipi, sağlanan bir koşul),
onu bulur bulmaz tüm dolaşma sürecini tamamen durdurabilirsiniz. Bu, enter veya leave
ziyaretçisinden NodeTraverser::StopTraversal sabitini döndürerek sağlanır. traverse() metodu daha
fazla düğüm ziyaret etmeyi bırakır. Olası çok büyük bir ağaçta yalnızca ilk eşleşmeye ihtiyacınız varsa bu son
derece etkilidir.
Yararlı NodeHelpers sınıfı
NodeTraverser ince taneli denetim sunarken, Latte ayrıca birkaç yaygın arama ve çözümleme işi için
NodeTraverser'ı saran ve genellikle daha az tekrar kod isteyen kullanışlı bir yardımcı sınıf da sağlar: Latte\Compiler\NodeHelpers.
find (Node $startNode, callable $filter): array
Bu statik metot, $startNode'dan (dahil) başlayan alt ağaçtaki, $filter callback'ini sağlayan
tüm düğümleri bulur. Eşleşen düğümlerin bir dizisini döndürür.
Örnek: Tüm şablondaki tüm değişken düğümlerini (VariableNode) bul.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;
function findAllVariables(TemplateNode $templateNode): array
{
return NodeHelpers::find(
$templateNode,
fn($node) => $node instanceof VariableNode,
);
}
findFirst (Node $startNode, callable $filter): ?Node
find'a benzer, ama $filter callback'ini sağlayan ilk düğümü bulduktan hemen sonra
dolaşmayı durdurur. Bulunan Node nesnesini ya da eşleşen düğüm yoksa null döndürür. Bu,
aslında NodeTraverser::StopTraversal çevresinde kullanışlı bir sarmalayıcıdır.
Örnek: {parameters} düğümünü bul.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // Verimlilik için yalnızca head bölümünde ara
fn($node) => $node instanceof ParametersNode,
);
}
clone (Latte\Compiler\Node $node): Node
Bu statik metot, bir düğümün ve tüm alt ağacının derin kopyasını oluşturur. AST'nin bir dalını çoğaltmanız gerektiğinde yararlıdır; örneğin özgününü olduğu gibi bırakırken bir düğümün değiştirilmiş bir kopyasını eklemek için.
use Latte\Compiler\NodeHelpers;
$copy = NodeHelpers::clone($node);
toValue (ExpressionNode $node, bool $constants = false): mixed
Bu statik metot, bir ExpressionNode'u derleme zamanında değerlendirmeyi ve karşılık gelen PHP
değerini döndürmeyi dener. Yalnızca basit değişmez düğümlerde (StringNode, IntegerNode,
FloatNode, BooleanNode, NullNode) ve yalnızca böyle değerlendirilebilir öğeler içeren
ArrayNode örneklerinde güvenilir çalışır.
$constants true yapılırsa, defined()'ı denetleyip constant()'ı
kullanarak ConstantFetchNode ve ClassConstantFetchNode'u da çözmeyi dener.
Düğüm değişkenler, fonksiyon çağrıları veya başka dinamik öğeler içeriyorsa derleme zamanında değerlendirilemez
ve metot bir InvalidArgumentException fırlatır.
Kullanım durumu: Derleme zamanı kararları vermek için bir etiket argümanının statik değerini derleme sırasında elde etmek.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;
function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
try {
$value = NodeHelpers::toValue($argumentNode);
return is_string($value) ? $value : null;
} catch (\InvalidArgumentException $e) {
// Argüman statik bir değişmez dize değildi
return null;
}
}
toText (?Node $node): ?string
Bu statik metot, basit düğümlerden düz metin içeriğini çıkarmak için yararlıdır. Öncelikle şunlarla çalışır:
TextNode:$content'ini döndürür.FragmentNode: Tüm çocukları içintoText()sonucunu birleştirir. Herhangi bir çocuk metne dönüştürülemiyorsa (örneğin birPrintNodeiçeriyorsa)nulldöndürür.NopNode: Boş bir dize döndürür.- Diğer düğüm tipleri:
nulldöndürür.
Kullanım durumu: Bir compiler pass sırasında çözümleme için bir HTML niteliğinin değerinin ya da basit bir HTML elemanının statik metin içeriğini elde etmek.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value genellikle bir AreaNode'dur (FragmentNode veya TextNode gibi)
return NodeHelpers::toText($attr->value);
}
// Bir pass'te örnek kullanım:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = $node->getAttribute('name');
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers, yaygın AST dolaşma ve çözümleme işleri için hazır çözümler sunarak compiler pass'lerinizi
basitleştirebilir.
Pratik örnekler
AST dolaşma ve değiştirme kavramlarını bazı pratik sorunları çözmek için uygulayalım. Bu örnekler, compiler pass'lerde kullanılan yaygın desenleri gösterir.
<img>'e otomatik loading="lazy" ekleme
Modern tarayıcılar, loading="lazy" niteliğiyle görseller için yerel tembel yüklemeyi destekler. Bu
niteliği, henüz bir loading niteliği olmayan tüm <img> etiketlerine otomatik ekleyen bir pass
oluşturalım.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;
function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Düğümü doğrudan değiştirdiğimiz ve bu karar için çocuklara
// bağlı olmadığımız için 'enter' kullanabiliriz.
enter: function (Node $node) {
// Adı 'img' olan bir HTML elemanı mı?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// 'loading' niteliği zaten var mı (büyük-küçük harfe duyarsız)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // Statik nitelik adı
&& strtolower($attrNode->name->content) === 'loading'
) {
return; // Zaten var, bir şey yapma
}
}
// Nitelikler boş değilse önüne bir boşluk ekle
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// Yeni nitelik düğümünü oluştur: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// Değişiklik yerinde yapıldı, dönüş gerekmez.
}
},
);
}
Açıklama:
enterziyaretçisi, adıimgolanHtml\ElementNodedüğümlerini arar.- Bir
loadingniteliğinin zaten bulunup bulunmadığını denetlemek için var olan nitelikler ($node->attributes->children) üzerinde döner. - Bulunmazsa,
loading="lazy"'yi temsil eden yeni birHtml\AttributeNodeoluşturur ve onu ekler (gerekirse önüne bir boşluk koyarak).
Fonksiyon çağrılarını denetleme
Compiler pass'leri Latte'nin Sandbox'ının temelidir. Gerçek Sandbox karmaşık olsa da, yasak fonksiyon çağrılarını denetlemenin temel ilkesini gösterebiliriz.
Amaç: Şablon ifadelerinin içinde olası tehlikeli shell_exec fonksiyonunun kullanımını
engellemek.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;
function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Basit liste
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// Doğrudan bir fonksiyon çağrısı düğümü mü?
if ($node instanceof Php\Expression\FunctionCallNode
&& $node->name instanceof Php\NameNode
&& isset($forbiddenFunctions[strtolower((string) $node->name)])
) {
throw new SecurityViolationException(
"Function {$node->name}() is not allowed.",
$node->position,
);
}
},
);
}
Açıklama:
- Yasak fonksiyon adlarının bir listesini tanımlarız.
enterziyaretçisiFunctionCallNode'u denetler.- Fonksiyon adı (
$node->name) statik birNameNodeise, küçük harfli dize gösterimini yasak listemizle karşılaştırırız. - Yasak bir fonksiyon bulunursa, güvenlik kuralı ihlalini açıkça gösteren ve derlemeyi durduran bir
Latte\SecurityViolationExceptionfırlatırız.
Bu örnekler, compiler pass'lerin NodeTraverser kullanarak, şablonun AST yapısıyla doğrudan etkileşerek
çözümleme, otomatik değişiklikler ve güvenlik kısıtlarının dayatılması için nasıl kullanılabileceğini
gösterir.
En iyi uygulamalar
Compiler pass'leri yazarken, sağlam, bakımı kolay ve verimli uzantılar oluşturmak için şu yönergeleri aklınızda tutun:
- Sıra önemlidir: Pass'lerin çalışma sırasına dikkat edin. Pass'iniz başka bir pass'in oluşturduğu AST
yapısına dayanıyorsa (örneğin Latte'nin çekirdek pass'lerine veya başka bir özel pass'e) ya da başka pass'ler sizin
değişikliklerinize bağlı olabiliyorsa, bağımlılıkları tanımlamak için
Extension::getPasses()'in sunduğu sıralama mekanizmasını kullanın (before/after). Ayrıntılar içinExtension::getPasses()dokümantasyonuna bakın. - Tek sorumluluk: Tek, iyi tanımlanmış bir iş yapan pass'ler hedefleyin. Karmaşık dönüşümler için mantığı birden fazla pass'e bölmeyi düşünün; belki biri çözümleme, diğeri çözümleme sonuçlarına göre değiştirme için. Bu, anlaşılırlığı ve test edilebilirliği artırır.
- Performans: Compiler pass'lerin şablon derleme süresine eklendiğini unutmayın (bu genellikle şablon değişene
kadar yalnızca bir kez olur). Mümkünse pass'lerinizin içinde hesaplama açısından pahalı işlemlerden kaçının. AST'nin
belirli bölümlerini ziyaret etmenize gerek olmadığını bildiğiniz her durumda
NodeTraverser::DontTraverseChildrenveNodeTraverser::StopTraversalgibi dolaşma iyileştirmelerinden yararlanın. NodeHelperskullanın: Belirli düğümleri bulmak veya basit ifadeleri statik değerlendirmek gibi yaygın işler için, özelNodeTraversermantığı yazmadan önceLatte\Compiler\NodeHelpers'ın uygun bir metot sunup sunmadığına bakın. Zaman kazandırır ve tekrar kodu azaltır.- Hata işleme: Pass'iniz şablonun AST'sinde bir hata ya da geçersiz bir durum saptarsa, açık bir mesajla ve ilgili
Positionnesnesiyle (genellikle$node->position) birLatte\CompileException(güvenlik sorunlarındaLatte\SecurityViolationException) fırlatın. Bu, şablon geliştiricisine yararlı geri bildirim sağlar. - Etkisizlik (mümkünse): İdeal olarak, pass'inizi aynı AST üzerinde birden fazla kez çalıştırmak, bir kez çalıştırmakla aynı sonucu üretmelidir. Bu her zaman uygulanabilir değildir, ama sağlanırsa hata ayıklamayı ve pass etkileşimleri hakkında akıl yürütmeyi kolaylaştırır. Örneğin, değiştirme pass'inizin, değişikliği yeniden uygulamadan önce zaten uygulanıp uygulanmadığını denetlediğinden emin olun.
Bu uygulamalara uyarak, Latte'nin yeteneklerini güçlü ve güvenilir biçimde genişletmek için compiler pass'lerden etkili biçimde yararlanabilir; daha güvenli, daha iyileştirilmiş ya da daha zengin özellikli şablon işlemeye katkıda bulunabilirsiniz.