# Tema Kancaları ve Çıktı Filtreleri

https://dev.wisecp.com/tr/tema-kancalari-ve-cikti-filtreleri

Tema, aldığı veriyi ve ürettiği markup'ı kendi dosyasından değiştirir. Hiçbir controller ya da çekirdek şablonu düzenlenmez.

## Genel Bakış

Her tema bir `hooks.php` taşıyabilir. Dosya, ilk sayfa oluşmadan önce bir kez include edilir; temayla taşınır ve temayla silinir.

**Veri** dinleyicileri view'a ulaşanı değiştirir: bu temanın her sayfada ihtiyaç duyduğu bir değişken, bir menü ağacı, bitmiş HTML. **Markup** noktaları ters yönde çalışır: tema onları açar, modül de içine enjekte eder.

## Yapı

### Nerede Durur

- **templates/website/{Tema}/hooks.php**: İsteğe bağlı. Düz PHP; sınıf yok, dönüş değeri yok. Dinleyicileri ve temanın yardımcı fonksiyonlarını tanımlar.
- **Theme::boot()**: Dosyayı istek başına bir kez, ilk sayfa oluşmadan önce include eder. View katmanı ve bağlamsız istek-dışı yol çağırır.
- **Kapsam**: Sadece website istekleri. Admin paneli ve zamanlanmış görevler tema çözmez.
- **Maliyet**: Her dinleyici her sayfa görüntülemesinde çalışır.

### İki Aile

| Aile | Tema şudur | Mekanizma | Dönüş |
| --- | --- | --- | --- |
| Veri (filtre) | dinleyici | kanca dosyasında `Hook::add()` | Dönüşle değişir ya da referansla değiştirilir |
| Markup (enjeksiyon) | yayıncı | yerleşim ve parçalarda `{hook name='...'}` | Dizeler birleştirilir, ham gösterilir |

## Referans

### Kanca API'si

```php
// $priority: küçük olan önce çalışır; çakışma artırılarak çözülür, dinleyici asla düşürülmez.
// $properties: bir closure ya da ['class' => 'X', 'method' => 'y'] ya da ['class' => 'X', 'method::static' => 'y'].
public static function add($name, $priority, $properties = []): void;

// Değer kopyasıyla. Dinleyici başına bir giriş döner; markup noktası bunları birleştirir.
public static function run($name, ...$args): array;

// Referansla. HER argüman referansla geçer, bu yüzden her biri düz bir değişken olmalıdır.
public static function runRefs($name, &...$args): array;
```

### Değişken Filtresi

Bir temanın her sayfaya değişken eklediği tek nokta. Her şablon için çalışır; bir kısmını hedefleyen dinleyici yolu sınar.

- **filter:template.variables**: View katmanında, veri seti tamamlandıktan sonra, şablon include edilmeden önce çalışır.
- **($template, $data)**: İşlenen şablonun tam yolu ve değişken dizisi. İkisi de referansla geçmez.
- **Dönüş**: Dizi dönerse veri seti tamamen ONUNLA DEĞİŞTİRİLİR. Başka her şey yok sayılır, set olduğu gibi kalır.
- **Tema başına tek dinleyici**: Sadece SON dönen dizi yaşar; temanın eklediği her şey tek gövdeye aittir.

### Çıktı Filtresi

Motor işini bitirdikten sonra, tarayıcıya ulaşmadan önceki bitmiş HTML. Her yükü tek tek değiştirecek yeniden yazım burada yapılır.

- **filter:client.page.output**: View katmanının tema dalında, hem gösterilen hem döndürülen yolda çalışır.
- **(&$output, $view, $engine)**: Tüm HTML referansla, onu üreten view adı ve aktif motor. Yerinde değiştirilir; dönüş kullanılmaz.
- **Kapsam**: Sadece etiket motorları. Düz PHP motorunu bildiren tema çıktıyı include ile gösterir, burada yakalanmaz.
- **Parçalar da geçer**: AJAX ile cevaplanan bölüm parçaları aynı daldan geçer; `$view` ile ayırt edin.

### Markup Noktaları

Yerleşimde `{hook name='ui:client.head.css'}` yazılır. Kurulu temalar yüz elliden fazlasını açar; taşınması gerekenler aşağıdadır.

| Nokta | Yerleşimdeki konumu | Oraya ne ait | Dönüş |
| --- | --- | --- | --- |
| `ui:client.head.css` | head sonu | Stil bağlantıları, stil blokları | Sadece `<link>` ya da `<style>` döndürün; görünür markup girmez |
| `ui:client.head.js` | head sonu | Temanın betiğinin kullandığı kütüphane | Sadece `<script>` döndürün; satır içi ya da `src`'li |
| `ui:client.body.begin` | body başı | Etiket yöneticisi çerçeveleri, üst bantlar | Herhangi bir HTML dizesi döndürün |
| `ui:client.body.end` | body sonu | Ertelenmiş betikler, sohbet bileşenleri, diyaloglar | Herhangi bir HTML dizesi döndürün |
| `ui:client.nav.items` | Üst menü sonu | Ek bir üst seviye öğe | `<li>` döndürün, çıplak `<a>` değil |
| `ui:client.header.actions` | Üst bant aksiyon kümesi | Sepetin yanına bir ikon butonu | `.btn.btn-soft.header-icon-btn` taşıyan `<a>` ya da `<button>` döndürün |
| `ui:client.user_menu.items` | Hesap açılır menüsü | Giriş yapılmış menüde bir satır | `.dropdown-item` taşıyan `<a>` döndürün |
| `ui:client.drawer.items` | Mobil çekmece | Menü öğesinin mobil ikizi | `.drawer-link` taşıyan çıplak `<a>` döndürün; çevresinde `<li>` olmaz |
| `ui:client.content.top` | Sayfa içeriği üstü | Site geneli bir duyuru şeridi | Herhangi bir HTML dizesi döndürün |
| `ui:client.footer.columns` | Altbilgi kolonlarından sonra | Ek bir bağlantı kolonu | Tek grid kolonu `<div>` döndürün; kardeşlerle aynı yapı, `<ul>`/`<li>` sarmalayıcı yok |
| `ui:client.footer.bottom` | Altbilgi alt şeridi | Bir rozet, bir sözleşme satırı | Herhangi bir HTML dizesi döndürün |

### Diğer Kancalar

| Kanca | Ne zaman | Tema bununla ne yapar |
| --- | --- | --- |
| `filter:client.theme` | Tema adı çözülürken | Host, segment ya da önizleme için farklı tema |
| `filter:client.menu` | Menü ağacı kurulduktan sonra | Panelin yönetmediği düğümü eklemek, çıkarmak, sıralamak |
| `filter:client.breadcrumb` | İz kısaltılmadan önce | İlk kırıntıyı yeniden adlandırmak ya da köklemek |
| `filter:client.predefined_data` | Müşteri veri paketinin sonu | Rota bilinirken bir paket değerini düzeltmek |
| `filter:client.routes` | Rotalar eşleştirilmeden önce | Bu temanın yayımladığı sayfaya kısa adres |
| `filter:routing.match` | Son yönlendirme yedeği | Gerçek bir rotanın sahiplenmediği slug'ı cevaplamak |
| `gate:client.page_access` | Controller kurulmadan önce | Sayfayı kanonik hostuna göndermek ya da reddetmek |
| `filter:sitemap.links` | Site haritası toplanırken | Sayfanın kanonik ilan ettiği adresi yayımlamak |

## Örnek

Eksiksiz bir kanca dosyası: tek değişken dinleyicisi, bir çıktı filtresi, bir enjeksiyon.

```php
/*
 * Tüm tema için TEK değişken dinleyicisi: view katmanı yalnız SON dönen diziyi
 * saklar, ikinci bir kayıt bunun yazdığı her şeyi düşürürdü.
 */
Hook::add("filter:template.variables", 1, function ($template, $data) {

    // Şablonun kendi başına okuyamayacağı çekirdek ayarından türetilir.
    $data["support_enabled"] = (int) (Config::get("options/ticket-system") ?? 0) === 1;

    // Bu temanın 404 döndürdüğü rotalar, anahtarlı harita olarak: sandbox'ta in_array()
    // yoktur, bu yüzden şablon {if !$route_off.affiliate} diye sorar.
    $data["route_off"] = array_fill_keys(Theme::active()->meta()["disabled_routes"] ?? [], true);

    // HER sayfada çalışır, bu yüzden sorgusu olan her şey önbellekten geçer. Anahtar
    // para birimini VE dili taşır, çünkü çıktı ikisine birden bağlıdır; biri eksik
    // kalırsa bir ziyaretçiye diğerinin kopyası servis edilir.
    $data["footer_groups"] = Cache::remember('website',
        'acme_footer_' . Money::getUCID() . '_' . Language::selected(), 3600,
        fn (): array => Products::groups());

    // Sayfaya özel iş ikinci bir kayıt değil, bir yol sınamasının arkasında durur.
    if (str_ends_with(str_replace('\\', '/', (string) $template), '/page/pricing.php'))
        $data["plans"] = AcmePricing::cards();

    return $data;   // başka bir şey döndürmek platformun topladığı her anahtarı düşürür
});

/*
 * Bitmiş sayfa. Bağlantı üreten her yükü yeniden yazmaktan hem ucuz hem güvenli:
 * tek yer, ve bir tanesini bile atlayamaz.
 */
Hook::add('filter:client.page.output', 1, function (&$output, $view, $engine) {
    if (str_contains($view, '/')) return;   // parçalar belge değildir

    $output = str_replace('</body>', '<script src="/assets/acme-widget.js" defer></script></body>', $output);
});

/*
 * Veri değil markup: yerleşimin noktaları bir dize alır ve olduğu gibi basar. Burada
 * kaydedilir, böylece temanın ek stil dosyası için hiçbir yerleşim dosyası düzenlenmez.
 */
Hook::add("ui:client.head.css", 1, fn (): string =>
    '<link rel="stylesheet" href="' . Theme::active()->assetUrl('css/extra.css') . '">');
```

```smarty
<head>
    {block name=head}{/block}
    {hook name='ui:client.head.css'}
    {hook name='ui:client.head.js'}
</head>
<body>
    {hook name='ui:client.body.begin'}

    {* Kanca dosyasının koyduğu bir değer, başka her değişken gibi okunur. *}
    {if !$route_off.affiliate}<a href="{link route='affiliate'}">{lang key='nav_affiliate'}</a>{/if}

    {block name=content}{/block}
    {hook name='ui:client.body.end'}
</body>
```

## Tuzaklar

> **İkinci değişken dinleyicisi birincininkini siler**
> 
> Hata, ona sebep olan dosyada görünmez: kırılan sayfa, diğer dinleyicinin set ettiği değişkeni okur. Tek gövde tutun, anahtarları oraya ekleyin.

> **Referanslı çalıştırmada her argüman referanslıdır**
> 
> Literal, satır içi dizi, tip dönüşümü, null birleştirmeli okuma ya da fonksiyon dönüşü geçemez; PHP uyarı değil ölümcül hata verir. Bağlamı önce bir değişkene alın.

> **Dinleyici sayfa başına bir maliyettir**
> 
> Dinleyicideki çıplak bir sorgu her sayfa görüntülemesinde çalışır. Veritabanı okumalarını önbelleğe alın; sonuç dile ve para birimine bağlıysa anahtarı ikisiyle kurun.

> **Dinleyicinin içindeki ölümcül hata yutulur**
> 
> Dağıtıcı onu yakalar ve devam eder; ölen dinleyici hata bırakmaz, değiştirmesi gereken değer değişmeden gelir. Bir dinleyici hiçbir şey yapmıyorsa kaydından değil içindeki ölümcül hatadan şüphelenin.

> **Enjekte edilen markup sayfaya ham ulaşır**
> 
> Markup noktaları escape etmez; sorumluluk enjekte eden taraftadır. Noktanın döndürdüğü dizeye ziyaretçi girdisi eklemeyin.

## İlgili Makaleler

- [Şablon Değişkenleri](https://dev.wisecp.com/tr/sablon-degiskenleri)
- [Kancalar Nasıl Çalışır](https://dev.wisecp.com/tr/kancalar-nasil-calisir)
- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
- [Menüler ve Navigasyon](https://dev.wisecp.com/tr/menuler-ve-navigasyon)
- [Tema Performansı ve Önbellek](https://dev.wisecp.com/tr/tema-performansi-ve-onbellek)
