Ö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:

  1. Sözcüksel çözümleme (lexing): Lexer, şablonun kaynak kodunu (.latte dosyası) okur ve onu token adı verilen küçük, ayrı parçalar dizisine böler (örneğin {, foreach, $variable, }, HTML metni vb.).
  2. 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.
  3. 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.
  4. 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.
  5. Ö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çin Latte\Compiler\Nodes\TextNode, HTML elemanları için Latte\Compiler\Nodes\Html\ElementNode, etiketlerin içindeki ifadeler için Latte\Compiler\Nodes\Php\ExpressionNode ve özel etiketler açısından en önemlisi Latte\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 yield kullanı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 create adı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çin ExpressionNode, ayrıştırılmış içerik için AreaNode).
  • print(PrintContext $context): string metodu, şablon render edilirken etiketin eylemini gerçekleştiren PHP kodunu (bir deyimi veya deyimler dizisini) üretir.
  • getIterator(): \Generator metodu, 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ı bir getIterator() 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(): array metodunu 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ğin MyNamespace\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 bir StringNode döndürür.
  • parseArguments(): ArrayNode: 10, name: 'John', true gibi, gerektiğinde anahtarlı olabilen virgülle ayrılmış argümanları ayrıştırır.
  • parseModifier(): ModifierNode: |upper|truncate:10 gibi filtreleri ayrıştırır.
  • parseType(): ?SuperiorTypeNode: int, ?string, array|Foo gibi 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ğin Token::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, bir CompileException fı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 ve null dö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:

  1. Başlangıç ve bitiş etiketleri arasında ayrıştırılan içeriği temsil eden bir AreaNode.
  2. Kapanış etiketini temsil eden Tag nesnesi (ö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ıyorsa true döndürür
  • $tag->prefix: ?string: n:attribute ile kullanılan öneki döndürür; null (n:attribute değil), Tag::PrefixNone, Tag::PrefixInner veya Tag::PrefixTag olabilir

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(&quot;Emin misiniz?&quot;) 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(&quot;123 numaralı öğeyi gerçekten silmek istiyor musunuz?&quot;)">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 olarak OutputRemoveIndentation'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 bir Node örneği olmalıdır. Node'un print() 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 bir Expression\ArrayNode olmalı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 bir Position (veya Range) 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 bir ModifierNode olmalıdır. ModifierNode'da belirtilen filtreleri iç içeriğe uygulayan PHP kodu üretir; |noescape ile 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 bir AuxiliaryNode üzerinde print() 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ı bir Node nesneleri dizisidir. Latte'nin bir AuxiliaryNode'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ın
  • Latte\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 zaman getIterator() 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çin format() 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 üzere print() 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:attribute desteği sağlar. Kullanıcı rahatlığı için bunu aklınızda tutun. Nitelik değiştiren bir etiket oluşturuyorsanız, saf bir n: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.

versiyon: 3.x