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 leave kullanın (yaygın uygulama).
  • Bir düğümü, çocukları ziyaret edilmeden önce değiştirmek istiyorsanız enter kullanı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çin toText() sonucunu birleştirir. Herhangi bir çocuk metne dönüştürülemiyorsa (örneğin bir PrintNode içeriyorsa) null döndürür.
  • NopNode: Boş bir dize döndürür.
  • Diğer düğüm tipleri: null dö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:

  • enter ziyaretçisi, adı img olan Html\ElementNode düğümlerini arar.
  • Bir loading niteliğinin zaten bulunup bulunmadığını denetlemek için var olan nitelikler ($node->attributes->children) üzerinde döner.
  • Bulunmazsa, loading="lazy"'yi temsil eden yeni bir Html\AttributeNode oluş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.
  • enter ziyaretçisi FunctionCallNode'u denetler.
  • Fonksiyon adı ($node->name) statik bir NameNode ise, 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\SecurityViolationException fı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çin Extension::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::DontTraverseChildren ve NodeTraverser::StopTraversal gibi dolaşma iyileştirmelerinden yararlanın.
  • NodeHelpers kullanın: Belirli düğümleri bulmak veya basit ifadeleri statik değerlendirmek gibi yaygın işler için, özel NodeTraverser mantığı yazmadan önce Latte\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 Position nesnesiyle (genellikle $node->position) bir Latte\CompileException (güvenlik sorunlarında Latte\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.

versiyon: 3.x