Ders 07 / 21
Şablon Motoru
Yerleşim, bileşen ve döngü kuran bir şablon derleyicisi yazılır; kaçışlamanın her çıktıda koştuğu, derlemenin değişmeyen şablonda atlandığı ve ham çıktıda kaçışlamanın bağlanmadığı ölçülür.
Yayın Güncelleme
Bu derste
- Şablonu PHP koduna çeviren küçük bir derleyici ve yerleşim, bileşen, döngü yapılarını kurmak
- Kaçışlama adımının her çıktı noktasında koştuğunu sandbox çıktısıyla göstermek
- Derlenmiş dosya önbelleğinde derleme adımının ne zaman atlandığını ölçmek
- Ham çıktı sözdiziminde kaçışlamanın bağlanmadığını açıkça seçilmiş bir istisna olarak okumak
İçindekiler
Nesne kabı (container) aynı ada iki farklı şey bağladığında hangisinin geldiği kayıt sırasına bağlı kaldı. Görünüm katmanında benzer bir soru yön değiştirir: bir şablon çıktı verdiğinde, geliştiricinin yazmadığı hangi adım her seferinde çalışıyor? Cevap güvenli olduğu varsayılan bir adımdır ve bu derste o varsayım ölçülür.
Bu dersin ölçü ekseni, kursun geri kalanıyla aynıdır: beklenen parça bu istekte koştu mu. Her ölçüm Gunluk::yeniIstek() ile başlar, parçalar Gunluk::yola(), Gunluk::kostu() ve Gunluk::ertele() ile kaydedilir, sonuç da sinifla() fonksiyonundan okunur. Karar sırası fonksiyonun kendisidir: önce koştu, sonra ertelendi, sonra atlandı, hiçbiri değilse bağlanmadı. Beklenen parçayı geliştirici adlandırır; fonksiyon yalnızca adlandırılmış parçanın hangi sınıfa düştüğünü söyler. Bu derste ağırlıklı sınıf koştu’dur.
GV1. Şablonlar çalışma dizinindeki sablonlar/ altında .html dosyalarıdır ve derlenmiş karşılıkları onbellek/ altına .php olarak yazılır; gerçek bir şablon motoru ayrıştırıcı kurar, burada düzenli ifade yetmektedir. GV2. Kaçışlama (escaping) htmlspecialchars ile yapılır ve yalnızca zararsız biçimleme ölçülür; şablon verisi her zaman geliştiricinin elindeki dizidir. GV3. Dosya zamanı saniye çözünürlüğündedir; ölçüm belirlenimci kalsın diye zaman touch() ile ileri alınır, sleep kullanılmaz.
Aşağıdaki dosya Gunluk sınıfını ve sinifla() fonksiyonunu kurar. Önceki derslerdeki ölçüm düzeneğinin aynısıdır ve bu derste olduğu gibi kullanılır.
<?php // kostu.php — beklenen bir parça bu istekte koştu mu? declare(strict_types=1); final class Gunluk { public static array $yol = []; // isteğin yoluna konan parçalar public static array $kostu = []; // istek içinde çağrılanlar public static array $ertelenen = []; // isteğin dışına bırakılanlar public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; } public static function yola(string $parca): void { self::$yol[] = $parca; } public static function kostu(string $parca): void { self::$kostu[] = $parca; } public static function ertele(string $parca): void { self::$ertelenen[] = $parca; } } function sinifla(string $parca): string { return match (true) { in_array($parca, Gunluk::$kostu, true) => 'koştu', in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi', in_array($parca, Gunluk::$yol, true) => 'atlandı', default => 'bağlanmadı', }; }
Şablonu Koda Çevirmek
Şablon motoru (template engine) bir metin dosyasını PHP koduna çevirir, o kodu bir kez yazar ve her istekte çalıştırır. Aşağıdaki dosya dokuz kuralı olan bir derleyici, bir yerleşim (layout) mekanizması ve bir bileşen çağrısı içerir. {{ ad }} kaçışlanmış çıktıya, ham çıktı sözdizimi kaçışsız çıktıya, {% her ... %} döngüye, {% yerlesim ... %} ve {% bolum ... %} yerleşime, {% ekle ... %} bileşene dönüşür. Kaçışlama kacisla() fonksiyonunda yapılır ve fonksiyon çağrıldığında kendini kaydeder; derleyici her {{ }} için bu fonksiyonu çağıran kodu üretir, yani adım şablon yazarının eline bırakılmaz.
<?php // sablon.php — küçük bir şablon derleyicisi ve motoru declare(strict_types=1); require_once 'kostu.php'; function kacisla(mixed $deger): string { Gunluk::kostu('kacislama'); return htmlspecialchars((string) $deger, ENT_QUOTES, 'UTF-8'); } final class SablonMotoru { public array $bolumler = []; private ?string $yerlesim = null; private array $acik = []; public function __construct(private string $dizin, private string $onbellek) { if (!is_dir($onbellek)) mkdir($onbellek); } private function derle(string $kaynak): string { $kural = [ '/\{\{!\s*(\w+)\s*\}\}/' => '<?= $$1 ?>', '/\{\{\s*(\w+)\s*\}\}/' => '<?= kacisla($$1) ?>', '/\{%\s*her (\w+) as (\w+)\s*%\}/' => '<?php foreach ($$1 as $$2): ?>', '/\{%\s*\/her\s*%\}/' => '<?php endforeach; ?>', '/\{%\s*bolum (\w+)\s*%\}/' => '<?php $motor->bolumAc(\'$1\'); ?>', '/\{%\s*\/bolum\s*%\}/' => '<?php $motor->bolumKapat(); ?>', '/\{%\s*yerlesim (\w+)\s*%\}/' => '<?php $motor->yerlesimSec(\'$1\'); ?>', '/\{%\s*yerles (\w+)\s*%\}/' => '<?= $motor->bolumler[\'$1\'] ?? \'\' ?>', '/\{%\s*ekle (\w+)\s*%\}/' => '<?= $motor->parca(\'$1\', get_defined_vars()) ?>', ]; return preg_replace(array_keys($kural), array_values($kural), $kaynak); } private function calistir(string $ad, array $veri): string { $kaynak = "{$this->dizin}/$ad.html"; $derlenmis = "{$this->onbellek}/$ad.php"; Gunluk::yola('derleme'); if (!is_file($derlenmis) || filemtime($derlenmis) < filemtime($kaynak)) { Gunluk::kostu('derleme'); file_put_contents($derlenmis, $this->derle(file_get_contents($kaynak))); } $veri['motor'] = $this; return self::calistirDosya($derlenmis, $veri); } private static function calistirDosya(string $__dosya, array $__veri): string { extract($__veri); ob_start(); require $__dosya; return ob_get_clean(); } public function goster(string $ad, array $veri = []): string { $sayfa = $this->calistir($ad, $veri); if ($this->yerlesim === null) return $sayfa; Gunluk::kostu('yerlesim'); $yerlesim = $this->yerlesim; $this->yerlesim = null; return $this->calistir($yerlesim, $veri); } public function parca(string $ad, array $veri): string { Gunluk::kostu('bilesen'); unset($veri['__dosya'], $veri['__veri']); return $this->calistir($ad, $veri); } public function yerlesimSec(string $ad): void { $this->yerlesim = $ad; } public function bolumAc(string $ad): void { $this->acik[] = $ad; ob_start(); } public function bolumKapat(): void { $this->bolumler[array_pop($this->acik)] = ob_get_clean(); } } function raporla(array $parcalar): void { foreach ($parcalar as $parca) printf(" %-10s %s\n", $parca, sinifla($parca)); }
Derleme, derlenmiş dosya yoksa ya da kaynaktan eskiyse koşar. Gunluk::yola('derleme') her çağrıda yazılır, Gunluk::kostu('derleme') yalnızca dosya yazıldığında. Bir parça yola konup çağrılmazsa sinifla() onu atlandı diye sınıflar; bu dersin karşıt ölçümü bu farka dayanır.
Üç küçük şablon gerekir: bir ana yerleşim, bir liste sayfası ve satır başına bir bileşen. Dosya her çalıştırmada önbelleği boşaltır ki ilk istek her zaman sıfırdan başlasın.
<?php // sablonlar.php — üç şablonu yazar ve derleme önbelleğini boşaltır if (!is_dir('sablonlar')) mkdir('sablonlar'); if (!is_dir('onbellek')) mkdir('onbellek'); foreach (glob('onbellek/*') as $eski) unlink($eski); file_put_contents('sablonlar/ana.html', <<<'HTML' <title>{{ baslik }}</title> <main>{% yerles icerik %}</main> HTML); file_put_contents('sablonlar/liste.html', <<<'HTML' {% yerlesim ana %} {% bolum icerik %} <ul> {% her yazilar as yazi %}{% ekle kart %}{% /her %} </ul> {% /bolum %} HTML); file_put_contents('sablonlar/kart.html', "<li>{{ yazi }}</li>\n");
Kaçışlamanın Koştuğunu Görmek
Ölçüm üç istek yapar. İlk istekte listedeki üç değer {{ yazi }} ile basılır; değerlerden biri & içerir, biri <b>kalın</b>, biri çift tırnaktır. Beklenen: kaçışlama adımı koşar, biçimleme çıktıda metin olarak kalır. İkinci istek aynı şablonu değişmeden çalıştırır; derleme atlandı olmalıdır çünkü derlenmiş dosya kaynaktan eski değildir, ama kaçışlama her istekte yine koşar, çünkü o derlenmiş kodun içinde durur. Üçüncü istekten önce kart.html dosyasının zamanı ileri alınır.
<?php // kacislama.php — her {{ }} çıktısında kaçışlama koşar; ikinci istekte derleme atlanır require_once 'sablon.php'; require_once 'sablonlar.php'; $motor = new SablonMotoru('sablonlar', 'onbellek'); $veri = ['baslik' => 'Yazilar', 'yazilar' => ['Tom & Jerry', '<b>kalın</b>', '"tırnak"']]; Gunluk::yeniIstek(); echo $motor->goster('liste', $veri), "\n"; raporla(['kacislama', 'yerlesim', 'bilesen', 'derleme']); Gunluk::yeniIstek(); $motor->goster('liste', $veri); echo "ikinci istek\n"; raporla(['kacislama', 'derleme']); touch('sablonlar/kart.html', time() + 5); Gunluk::yeniIstek(); $motor->goster('liste', $veri); echo "kart.html güncellendi\n"; raporla(['derleme']);
<title>Yazilar</title> <main><ul> <li>Tom & Jerry</li> <li><b>kalın</b></li> <li>"tırnak"</li> </ul> </main> kacislama koştu yerlesim koştu bilesen koştu derleme koştu ikinci istek kacislama koştu derleme atlandı kart.html güncellendi derleme koştu
İlk istekte dört parçanın dördü de koştu. Yerleşim, sayfa şablonu çalıştıktan sonra ana şablonu çağırdığı için koştu; bileşen, döngünün her turunda çağrıldığı için; kaçışlama her {{ }} noktasında; derleme de önbellek boş olduğu için. İkinci istekte ayrım belirgindir: derleme atlandı, kaçışlama koştu. Derleme bir önbellek kararıdır, kaçışlama değildir; karar değişse de kaçışlama derlenmiş kodun içinde olduğundan koşmaya devam eder. Üçüncü istekte yalnızca değişen bileşenin dosyası yeniden derlenir, ana şablon aynı kalır. Bu ölçüm sayı değil sınıf ölçer: kaç kez koştuğu sorusu önceki bir kursta sorulmuştu ve burada sorulmuyor.
Karşıt Ölçüm: Atlanan Derleme
Yukarıdaki üç istek karşıt ölçümü zaten içerir. İkinci isteğin derleme satırı, adımın yola konup çağrılmadığı durumdur: Gunluk::yola('derleme') yazıldı, Gunluk::kostu('derleme') yazılmadı. Bu atlandı demektir. Hata değildir; önbelleğin amacı da budur. Ters durum, yani beklenen parçanın hiç yola konmaması, bir sonraki bölümün konusudur.
Sınırlayıcı Ölçüm
Kaçışlamanın her çıktıda koştuğu iddiası, ham çıktı sözdizimi kullanıldığında sınırlanır. Aşağıdaki ölçüm iki şablon yazar: biri {{ ozet }}, öteki ham çıktı sözdizimiyle aynı değeri basar. Değer <b>kalın</b> biçimlemesidir.
<?php // ham.php — ham çıktı sözdiziminde kaçışlama bağlanmadı require_once 'sablon.php'; file_put_contents('sablonlar/ham.html', "<p>{{! ozet }}</p>\n"); file_put_contents('sablonlar/kacisli.html', "<p>{{ ozet }}</p>\n"); $motor = new SablonMotoru('sablonlar', 'onbellek'); $veri = ['ozet' => '<b>kalın</b>']; foreach (['kacisli', 'ham'] as $ad) { Gunluk::yeniIstek(); echo $motor->goster($ad, $veri); echo "$ad\n"; raporla(['kacislama']); }
<p><b>kalın</b></p> kacisli kacislama koştu <p><b>kalın</b></p> ham kacislama bağlanmadı
Ham çıktıda kaçışlama bağlanmadı. Derleyici o noktaya kaçışlama çağrısını hiç koymadı; yola() da yazılmadı, kostu() da. Sınıf bu yüzden atlandı değil. Kaçışlama, ham çıktı noktası dışında her yerde koşar ve sapma o noktayla sınırlıdır. Çıktıda hata yoktur, uyarı yoktur; sayfa üretilir. Ham çıktı güvenilir biçimlemeyi basmak için vardır, ama işareti kaçışlanmamış bir değerin üstüne koyan kişi, adımı kendi eliyle devre dışı bırakmış olur. Bu yüzden ham çıktı açıkça seçilmiş bir istisnadır ve sözdizimi kasıtlı olarak farklıdır: göz ucuyla okunduğunda bile ayırt edilmelidir.
Özet
- Şablon motoru, şablonu PHP koduna çevirir ve derlenmiş dosyayı önbelleğe yazar; yerleşim, bileşen ve döngü bu çeviriye dahildir.
- Bu dersin ölçü sınıfı
koştu’dur: kaçışlama adımı, derleyicinin ürettiği koda gömülü olduğu için her{{ }}noktasında koştu. - Değişmeyen şablonda derleme adımı
atlandı; kaçışlama derlemeden bağımsız olarak her istektekoştu. - Ham çıktı sözdiziminde kaçışlama
bağlanmadı: sınıfatlandıdeğildir, çünkü parça yola hiç konmadı. - Ham çıktı açıkça seçilmiş bir istisnadır; tehlikesi hata vermemesidir.
Sonraki Adım
Şablon, verisine güvenir ve ona verilen her değeri basar. Peki kullanıcıdan gelen bir form, kurala uymadığında kayıt adımına ulaşır mı? Bir sonraki ders doğrulamayı ölçer ve kayıt adımının atlandı sınıfına düşmesini inceler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.