---
title: 'Web Uygulama Çerçevesi'
source: 'https://academia.sh/tr/kurslar/web-uygulama-cercevesi'
language: tr
updated: '2026-10-21T05:03:38+00:00'
license: 'CC BY-SA 4.0'
lessons: 21
outcomes:
    - 'Beklenen her parçanın bir istekte koştuğunu, atlandığını, ertelendiğini ya da bağlanmadığını bir istek günlüğüyle ölçer'
    - 'Yönlendirme, denetleyici ve ara katmanla bir isteğin yanıta dönüşme yolunu kurar'
    - 'Nesne ve tablo bağlamasıyla modelleri ve ilişkilerini tanımlar, tembel ve istekli yüklemeyi ayırt eder'
    - 'Kuyruk, olay ve zamanlanmış görevle bir işi yanıtın dışına taşır'
    - 'Sessizce bağlanmayan bir parçayı başarılı bir yanıtın arkasında bulur'
---

# Web Uygulama Çerçevesi

Bir çerçeveyle yazılan kodu sen yazarsın, onu çerçeve çağırır. Yazdığın parça bu istekte koştu mu? Kimi parça istek içinde koşar, kimi önündeki bir katman yanıtı erken verdiği için atlanır, kimi kuyruğa ya da yanıttan sonraya ertelenir, kimi hiçbir kayıtla isteğe bağlanmaz ve hiçbir hata da vermez. Kurs yönlendirmeyi, denetleyicileri, ara katmanları, nesne kabını, şablonları, doğrulamayı, nesne ve tablo bağlamasını, kuyrukları, olayları ve zamanlanmış görevleri tek bir soruyla ölçer — bu parça bu istekte koştu mu?

## Çerçeve Temelleri

Denetimin ters çevrilmesi, istek–yanıt yaşam döngüsü, yönlendirme, denetleyiciler, ara katmanlar, nesne kabı ve cepheler — her biri yazılan bir parçanın bu istekte çağrılıp çağrılmadığını belirleyen bir düzenek olarak.

### Neden Çerçeve

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/neden-cerceve

Akışı betiğin kendisinin yürüttüğü bir if zinciri ile akışı çerçevenin yürüttüğü bir rota kaydının karşılaştırılması; bir parçanın bu istekte koşup koşmadığını sınıflayan dört ad ve on iki parçalık ölçüm.

Nesneye Dayalı PHP ve Standartlar kursu kodu birden çok dosyaya ve birden çok yazara böldü ve bir tanım değişince kullananın ne yaşadığını ölçtü. Bu kurs kodu bir çerçeveye (framework) yerleştirir ve ilişkiyi tersine çevirir: kodu yazan taraf kendi parçalarını yazar, onları çerçeve çağırır. Denetimin bu biçimde ters çevrilmesi, çerçeveyle çalışırken en sık sorulan soruyu doğurur: yazılan parça neden koşmadı? Kurs yirmi bir derste her düzeneği tek bir soruyla ölçer: **bu parça bu istekte koştu mu?**

#### Akışı Kim Yürütüyor

Çerçevesiz bir betikte akışı betiğin kendisi yürütür. Hangi isteğin hangi fonksiyonu çağıracağı, çağıranın yazdığı satırlarda durur.

```php
<?php
// elle.php — akışı betiğin kendisi yürütür
function form(): string { return 'boş form'; }
function liste(): string { return 'yazı listesi'; }

$yol = '/yazilar/yeni';

if ($yol === '/yazilar') {
    echo liste(), "\n";
} elseif ($yol === '/yazilar/yeni') {
    echo form(), "\n";
} else {
    echo "404\n";
}
```

```
boş form
```

**ÇT1.** `form()` çağrısını betiğin kendi `if` zinciri yaptı. Yeni bir yol eklemek zinciri düzenlemek demektir ve zincirin sırası, hangi fonksiyonun çağrılacağını belirleyen tek şeydir. Çerçevede bu ilişki tersine döner: yazan taraf `form` eylemini bir kalıpla birlikte **kaydeder**, kalıbı isteğe karşı deneyip eylemi çağıran kod çerçevenin içindedir. Yazılan parça artık çağıran değil çağrılandır. Çağrıyı çerçeve yaptığı için, parçanın çağrılıp çağrılmadığı da yazanın göremediği bir yerde karar verilir. Soru bu yüzden ölçülmeye değerdir.

#### Ölçüm Çekirdeği

Ölçümün kendisi `kostu.php` dosyasında durur ve kurs boyunca değişmez. Her derste aynı yirmi iki satır yeniden verilir.

```php
<?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ı',
    };
}
```

`Gunluk` sınıfı üç liste tutar: isteğin **yoluna** konan parçalar, istek içinde **çağrılan** parçalar ve isteğin **dışına bırakılan** parçalar. `yeniIstek()` üçünü de boşaltır, her ölçüm onunla başlar. Bir düzenek kendini çekirdeğe üç çağrıyla bağlar: parçayı yola koyduğunda `Gunluk::yola()`, çağırdığında `Gunluk::kostu()`, isteğin dışına bıraktığında `Gunluk::ertele()`. `sinifla()` bir parçanın adını alır ve bu üç listeye **sırayla** bakar.

**ÇT2.** Karar sırası `sinifla()`'nın kendisidir. Parça `$kostu` listesindeyse **koştu**: istek sırasında çağrıldı ve yanıt onu bekledi. Değilse ve `$ertelenen` listesindeyse **ertelendi**: kaydedildi ama istekte koşmadı, yanıttan sonra ya da bir işçide koşacak. Değilse ve `$yol` listesindeyse **atlandı**: parça isteğin yolundaydı ama önündeki bir adım yanıtı erken verdi. Üçünde de yoksa **bağlanmadı**: hiçbir kayıt parçayı bu isteğe bağlamadı ve hata da verilmedi. Sıra bir kural olarak yazılmıştır; parça iki listede birden bulunsaydı ilk eşleşen kazanırdı. Bu derste hiçbir parça iki listede birden bulunmaz.

Bir koşul sınıflamanın parçasıdır: **beklenen parçayı geliştirici adlandırır.** Ölçülen şey çerçevenin bütün kayıtları değil, geliştiricinin bu istekte koşmasını beklediği parçalardır ve `sinifla()` o adı alıp ona ne olduğunu söyler. Bir dersin ölçtüğü her parça önceden adlandırılır.

İki ayrım baştan yapılmalıdır. Nesneye Dayalı PHP ve Standartlar kursunda bir tanım değişikliğinin kullananlara ulaşımı iki koşumla, öncesi ve sonrasıyla ölçüldü; burada tek bir koşum vardır ve sorulan, çağrının olup olmadığıdır. PHP Temelleri kursunda sessiz sınıfı bir aksaklığın hiçbir sinyal vermemesiydi; **bağlanmadı** ise bir aksaklık değil, bir eşleşme eksikliğidir: kayıt vardır, yalnız bu isteğe bağlanmamıştır. Sinyal ölçülmez, `Gunluk` ölçülür.

#### Çerçeve Modeli

`cerceve.php` çerçevenin küçük bir modelidir. Bu kursta çerçeve ürün adıyla değil, bu model üzerinden öğretilir; yalnız bu ilk derste bütünüyle yazılır, sonraki dersler kendi düzeneklerinin dilimini kurar.

```php
<?php
// cerceve.php — küçük bir web uygulama çerçevesi modeli
declare(strict_types=1);
require_once 'kostu.php';

final class Istek {
    public array $parametre = [];
    public function __construct(
        public string $yontem, public string $yol,
        public array $govde = [], public bool $oturum = false,
    ) {}
}

final class Yanit {
    public function __construct(public int $durum, public string $govde = '') {}
}

final class NesneKabi {
    private array $tarifler = [];
    private array $tekiller = [];
    public function bagla(string $ad, Closure $tarif, bool $tekil = false): void {
        $this->tarifler[$ad] = [$tarif, $tekil];
    }
    public function al(string $ad): object {
        if (isset($this->tekiller[$ad])) return $this->tekiller[$ad];
        if (isset($this->tarifler[$ad])) {
            [$tarif, $tekil] = $this->tarifler[$ad];
            $nesne = $tarif($this);
            if ($tekil) $this->tekiller[$ad] = $nesne;
            return $nesne;
        }
        $bagimliliklar = [];
        foreach ((new ReflectionClass($ad))->getConstructor()?->getParameters() ?? [] as $p) {
            $bagimliliklar[] = $this->al($p->getType()->getName());
        }
        return new $ad(...$bagimliliklar);
    }
}

final class Kuyruk {
    private array $bekleyen = [];
    public function ekle(string $parca, Closure $is): void {
        Gunluk::ertele($parca);
        $this->bekleyen[] = [$parca, $is];
    }
    public function isle(): array {            // işçi: istekten bağımsız koşar
        $kosan = [];
        while ($kayit = array_shift($this->bekleyen)) {
            [$parca, $is] = $kayit;
            $is();
            $kosan[] = $parca;
        }
        return $kosan;
    }
}

final class Olaylar {
    private array $dinleyiciler = [];
    public function __construct(private Kuyruk $kuyruk) {}
    public function dinle(string $olay, string $parca, Closure $is, bool $kuyrukta = false): void {
        $this->dinleyiciler[$olay][] = [$parca, $is, $kuyrukta];
    }
    public function yay(string $olay): void {
        foreach ($this->dinleyiciler[$olay] ?? [] as [$parca, $is, $kuyrukta]) {
            if ($kuyrukta) { $this->kuyruk->ekle($parca, $is); continue; }
            Gunluk::kostu($parca);
            $is();
        }
    }
}

final class Uygulama {
    private array $rotalar = [];
    private array $sonra = [];             // yanıt gönderildikten sonra koşacaklar
    public function __construct(public NesneKabi $kap) {}
    public function rota(string $yontem, string $kalip, string $parca, Closure $eylem, array $ara = []): void {
        $this->rotalar[] = [$yontem, $kalip, $parca, $eylem, $ara];
    }
    public function yanittanSonra(string $parca, Closure $is): void {
        Gunluk::ertele($parca);
        $this->sonra[$parca] = $is;
    }
    private function eslestir(Istek $istek): ?array {
        foreach ($this->rotalar as $rota) {        // ilk eşleşen kazanır
            $desen = '#^' . preg_replace('#\{(\w+)\}#', '(?P<$1>[^/]+)', $rota[1]) . '$#';
            if ($rota[0] === $istek->yontem && preg_match($desen, $istek->yol, $m)) {
                $istek->parametre = array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY);
                return $rota;
            }
        }
        return null;
    }
    public function isle(Istek $istek): Yanit {
        Gunluk::yeniIstek();
        $this->sonra = [];
        $rota = $this->eslestir($istek);
        if ($rota === null) return new Yanit(404);
        [, , $parca, $eylem, $ara] = $rota;
        foreach ([...array_column($ara, 0), $parca] as $p) Gunluk::yola($p);
        $hat = function (Istek $i) use ($parca, $eylem): Yanit {
            Gunluk::kostu($parca);
            return $eylem($i, $this->kap);
        };
        foreach (array_reverse($ara) as [$ad, $katman]) {
            $hat = function (Istek $i) use ($ad, $katman, $hat): Yanit {
                Gunluk::kostu($ad);
                return $katman($i, $hat);
            };
        }
        return $hat($istek);
    }
    public function bitir(): array {           // yanıt istemciye gitti
        foreach ($this->sonra as $is) $is();
        return array_keys($this->sonra);
    }
}
```

Dosya altı parçadan oluşur ve her sonraki ders bunlardan birini açar. `Istek` ve `Yanit` isteğin ve yanıtın taşıyıcılarıdır. `NesneKabi` (nesne kabı, object container) sınıf adından nesne kuran bir fabrikadır; kayıtsız bir sınıfı yapıcı parametrelerinin tipinden `ReflectionClass` ile kurar ve yalnız sınıf tipli parametreyi çözer. `Kuyruk` işleri isteğin dışına bırakır; `isle()` çağrısı işçidir ve model aynı süreçte çalışır. `Olaylar` bir olay adına kayıtlı dinleyicileri çağırır ya da kuyruğa koyar. `Uygulama` rotaları kaydeder, `eslestir()` ile kayıt sırasıyla dener ve **ilk eşleşen kazanır**, ardından ara katmanları iç içe geçmiş bir zincire sarıp çağırır.

**ÇT3.** Ters çevrilmiş denetim `isle()` içinde görünür. Eylem `$eylem($i, $this->kap)` satırında çağrılır, çağıran çerçevedir; ve `Gunluk::kostu($parca)` kaydı da eylemin kendisinde değil, çerçevenin çağrı satırının hemen önündedir. Ölçüm parçaların içine yazılmaz, parçaları çağıran düzeneğe yazılır: parça kendini koştu diye bildirmez, çerçeve onu çağırdığını bildirir.

#### Dört İstek, On İki Parça

`istekler.php` bir nesne kabı kurar, üç rota ve üç dinleyici kaydeder ve dört isteği sırayla işler. Her isteğin beklenen parçaları istekle birlikte adlandırılmıştır.

```php
<?php
// istekler.php — dört istek, on iki beklenen parça
require 'cerceve.php';

$kap = new NesneKabi();
$kap->bagla(Kuyruk::class, fn() => new Kuyruk(), tekil: true);
$kap->bagla(Olaylar::class, fn(NesneKabi $k) => new Olaylar($k->al(Kuyruk::class)), tekil: true);
$uygulama = new Uygulama($kap);

$oturum = ['oturum', fn(Istek $i, Closure $sonraki) =>
    $i->oturum ? $sonraki($i) : new Yanit(302, '/giris')];
$dogrulama = ['dogrulama', fn(Istek $i, Closure $sonraki) =>
    ($i->govde['baslik'] ?? '') !== '' ? $sonraki($i) : new Yanit(422, 'baslik gerekli')];

$uygulama->rota('GET', '/yazilar/{id}', 'goster', fn(Istek $i) => new Yanit(200, "yazı {$i->parametre['id']}"));
$uygulama->rota('GET', '/yazilar/yeni', 'form', fn() => new Yanit(200, 'boş form'));
$uygulama->rota('POST', '/yazilar', 'kaydet', function (Istek $i, NesneKabi $k) use ($uygulama) {
    $k->al(Olaylar::class)->yay('yazi.kaydedildi');
    $k->al(Kuyruk::class)->ekle('kucukResim', fn() => null);
    $uygulama->yanittanSonra('istatistik', fn() => null);
    return new Yanit(201, 'kaydedildi');
}, [$oturum, $dogrulama]);

$olaylar = $kap->al(Olaylar::class);
$olaylar->dinle('yazi.kaydedildi', 'onbellek', fn() => null);
$olaylar->dinle('yazi.kaydedildi', 'bildirim', fn() => null, kuyrukta: true);
$olaylar->dinle('yazi.olusturuldu', 'dizin', fn() => null);

$istekler = [
    [new Istek('GET', '/yazilar/yeni'), ['form']],
    [new Istek('POST', '/yazilar', ['baslik' => 'Merhaba']), ['oturum', 'dogrulama', 'kaydet']],
    [new Istek('POST', '/yazilar', ['baslik' => ''], true), ['dogrulama', 'kaydet']],
    [new Istek('POST', '/yazilar', ['baslik' => 'Merhaba'], true),
        ['kaydet', 'onbellek', 'bildirim', 'kucukResim', 'istatistik', 'dizin']],
];

$sayac = ['koştu' => 0, 'atlandı' => 0, 'ertelendi' => 0, 'bağlanmadı' => 0];
foreach ($istekler as [$istek, $beklenen]) {
    $yanit = $uygulama->isle($istek);
    printf("%s %s -> %d %s\n", $istek->yontem, $istek->yol, $yanit->durum, $yanit->govde);
    foreach ($beklenen as $parca) {
        $sinif = sinifla($parca);
        $sayac[$sinif]++;
        printf("  %-11s %s\n", $parca, $sinif);
    }
    if ($sonra = $uygulama->bitir()) echo "  yanıttan sonra: ", implode(', ', $sonra), "\n";
}
echo "işçi: ", implode(', ', $kap->al(Kuyruk::class)->isle()), "\n";
foreach ($sayac as $sinif => $n) echo "$sinif $n\n";
```

```
GET /yazilar/yeni -> 200 yazı yeni
  form        bağlanmadı
POST /yazilar -> 302 /giris
  oturum      koştu
  dogrulama   atlandı
  kaydet      atlandı
POST /yazilar -> 422 baslik gerekli
  dogrulama   koştu
  kaydet      atlandı
POST /yazilar -> 201 kaydedildi
  kaydet      koştu
  onbellek    koştu
  bildirim    ertelendi
  kucukResim  ertelendi
  istatistik  ertelendi
  dizin       bağlanmadı
  yanıttan sonra: istatistik
işçi: bildirim, kucukResim
koştu 4
atlandı 3
ertelendi 3
bağlanmadı 2
```

**ÇT4.** On iki beklenen parçadan **4'ü koştu, 3'ü atlandı, 3'ü ertelendi, 2'si bağlanmadı.** Bu dağılım kursun bağlayıcı sayısıdır ve bu çıktıdan çıkar. Sonraki derslerin hiçbiri onu değiştirmez; her ders kendi kesitini ölçer ve kapanış dersi iki ayrı sayıyı ayrı ayrı yazar: çekirdeğin on iki parçası ve yirmi bir dersin kendi sınıfları.

Çıktı üç sınıfı durum koduyla ele verir, dördüncüsünü vermez. İkinci istekte `oturum` koştu ve 302 verdi; arkasındaki `dogrulama` ile `kaydet` yoldaydı ama çağrılmadı, ikisi de **atlandı**. Üçüncü istekte `dogrulama` koştu ve 422 verdi, `kaydet` yine atlandı. Atlanan parça 302, 422 gibi bir kodla kendini ele verir. Dördüncü istekte `bildirim` ve `kucukResim` işçide koştu, `istatistik` yanıt gönderildikten sonra koştu; üçü de 201'in içinde yoktur, yanıt onları beklemedi. Ertelenen parça bir işçide görünür.

**Bağlanmadı en tehlikeli sınıftır**, çünkü hiçbir yerde görünmez. Birinci istek `/yazilar/yeni` adresine gitti ve `form` hiç çağrılmadı: yanıt 200 ve gövdesi `yazı yeni`. Rota tablosunda `form` kayıtlıydı ama `/yazilar/{id}` kalıbı önce kaydedildiği için istek ona uydu. Dördüncü istekte `dizin` dinleyicisi `yazi.olusturuldu` adına kaydedilmişti, eylem `yazi.kaydedildi` yaydı: dinleyici çağrılmadı, yanıt 201. İki durumda da yanıt başarılıdır ve sorun yalnız `Gunluk`'ta görünür.

Bu dersin ölçü sınıfı **koştu**'dur. Dördüncü istekte `kaydet` eylemi ve `onbellek` dinleyicisi yanıt gönderilmeden çağrıldı: çerçevenin söz verdiği akış budur.

#### Sınırlayıcı Ölçüm

Sınıflama, geliştiricinin beklediği parçanın adına dayanır. Bir parça gerçekten koşmuş olsa bile, beklenen ad kayıtlı adla uyuşmazsa sınıf değişir mi? Aynı istekte iki ad sorulur: kayıtlı `kaydet` ve bir harf farklı yazılmış `kaydeder`.

```php
<?php
// adlandirma.php — beklenen parçayı geliştirici adlandırır
require 'cerceve.php';

$uygulama = new Uygulama(new NesneKabi());
$uygulama->rota('POST', '/yazilar', 'kaydet', fn() => new Yanit(201, 'kaydedildi'));

$yanit = $uygulama->isle(new Istek('POST', '/yazilar'));
echo $yanit->durum, "\n";
foreach (['kaydet', 'kaydeder'] as $parca) {
    printf("%-9s %s\n", $parca, sinifla($parca));
}
```

```
201
kaydet    koştu
kaydeder  bağlanmadı
```

**ÇT5.** Aynı istek, aynı yanıt, tek bir çağrı: `kaydet` **koştu**, `kaydeder` **bağlanmadı**. Eylem iki ad için de aynı kez çağrıldı, ama `Gunluk` yalnız kayıtlı adı bildi. Demek ki bağlanmadı her zaman bir eşleşme hatası değildir; beklenen ad yanlış yazılmış da olabilir. Sınıflama çerçevenin kayıtlarını değil, geliştiricinin beklentisini sorguladığı için ölçümün güvenilirliği adlandırmanın doğruluğuna bağlıdır.

#### Özet

- Çerçevede denetim ters çevrilir: yazılan parça kaydedilir ve onu çerçeve çağırır, çağıran artık yazan taraf değildir.
- `sinifla()` bir parçayı sırayla koştu, ertelendi, atlandı ve bağlanmadı olarak sınıflar; beklenen parçayı geliştirici adlandırır.
- Çekirdek dört istek ve on iki beklenen parçada 4 koştu, 3 atlandı, 3 ertelendi ve 2 bağlanmadı verdi; bu dağılım kursun bağlayıcı sayısıdır.
- Atlanan parça bir durum koduyla, ertelenen parça işçide görünür; bağlanmayan parça yanıt 200 ya da 201 iken hiçbir yerde görünmez.
- Bu dersin ölçü sınıfı **koştu**'dur: dördüncü isteğin `kaydet` eylemi ve `onbellek` dinleyicisi yanıttan önce çağrıldı.

#### Sonraki Adım

Dördüncü istekte üç parça yanıtın içinde yoktu: `bildirim` ve `kucukResim` işçide, `istatistik` ise yanıt gönderildikten sonra koştu. Sonraki ders, İstek–Yanıt Yaşam Döngüsü, tek bir soruyu soruyor: yanıt istemciye gittikten sonra çerçeve hâlâ ne koşturuyor ve bunun yanıtın gövdesinde bir izi kalıyor mu?

### İstek–Yanıt Yaşam Döngüsü

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/istek-yanit-yasam-dongusu

Önyükleme, servis sağlayıcıların kayıt ve önyükleme evreleri, çekirdeğin isteği işlemesi ve yanıt gönderildikten sonra koşan işin yanıtın dışında kalması; ertelenen iş ile doğrudan çağrılan işin karşılaştırılması.

Önceki ders dördüncü istekte üç parçanın yanıtın içinde olmadığını gösterdi: ikisi işçide, biri yanıt gönderildikten sonra koştu. Bu ders işin ne zaman koştuğunu bir isteğin ömrü boyunca sırayla izler. Çerçeve bir isteği tek bir çağrı olarak görmez; önyükleme, işleme ve gönderim evrelerine böler ve her evrenin kendi kaydı vardır. Evrelerin sırası bilinmezse, bir parçanın neden bu istekte görünmediği de anlaşılmaz.

#### Bir İsteğin Evreleri

Çerçeveyle yazılmış bir uygulamada bir isteğin ömrü dört ayrı evreye bölünür ve her evrenin sahibi farklıdır.

| Evre | Ne olur | Kimin kodu |
|---|---|---|
| Önyükleme | Sağlayıcılar kaydolur ve önyüklenir | Uygulamanın sağlayıcıları |
| İşleme | Çekirdek isteği karşılayan eylemi bulur ve çağırır | Çerçeve çağırır, eylemi geliştirici yazar |
| Gönderim | Eylemin döndürdüğü yanıt istemciye bırakılır | Çerçeve |
| Yanıttan sonra | Kaydedilmiş işler koşar | Geliştiricinin kaydettiği iş, çerçevenin çağrısı |

Tablonun üçüncü ve dördüncü satırı arasındaki sınır bu dersin konusudur. İstemci yanıtı aldığı anda, çerçevenin elinde hâlâ koşmamış işler olabilir. Bu işlerin yanıtın içeriğine karışmadığını görmek kolaydır; asıl soru, onların gerçekten koşup koşmadığını nereden bileceğimizdir.

#### Önyükleme ve Servis Sağlayıcılar

Önyükleme (bootstrap), çerçevenin ilk isteği işlemeden önce kendini kurduğu evredir. Bu evrede çerçeve **servis sağlayıcıları** (service provider) çağırır: her sağlayıcı, uygulamanın bir bölümünü çerçeveye tanıtan bir kayıt birimidir. Sağlayıcının işi iki evreye ayrılır. **Kayıt** evresinde sağlayıcı yalnızca neyi sunduğunu bildirir ve başka bir sağlayıcının sunduğuna dokunmaz. **Önyükleme** evresinde bütün sağlayıcılar kaydolmuş olduğundan, sağlayıcı artık başkalarının sunduklarına güvenebilir. Evreler sağlayıcı başına değil, bütün sağlayıcılar için sırayla koşar: önce herkesin kaydı, sonra herkesin önyüklemesi.

Aşağıdaki `yasam.php` bu yaşam döngüsünün küçük bir modelidir. `Cekirdek`'te kayıt çağrılarından ikisi bu modelin özüdür: `saglayici()` bir sağlayıcının kayıt ve önyükleme fonksiyonlarını, `eylem()` isteğin sonunda çağrılacak parçayı kaydeder. `onyukle()` iki evreyi sırayla koşturur, `isle()` isteği işler ve `bitir()` yanıt istemciye gittikten sonra çağrılır.

```php
<?php
// yasam.php — servis sağlayıcılı küçük bir yaşam döngüsü
declare(strict_types=1);
require_once 'kostu.php';

final class Yanit {
    public function __construct(public int $durum, public string $govde = '') {}
}

final class Cekirdek {
    public array $evreler = [];
    private array $saglayicilar = [];
    private array $izleyiciler = [];
    private array $sonra = [];
    private ?array $eylem = null;
    public function saglayici(Closure $kaydet, Closure $onyukle): void {
        $this->saglayicilar[] = [$kaydet, $onyukle];
    }
    public function izleyici(string $parca, Closure $is): void { $this->izleyiciler[$parca] = $is; }
    public function eylem(string $parca, Closure $is): void { $this->eylem = [$parca, $is]; }
    public function yanittanSonra(string $parca, Closure $is): void {
        Gunluk::ertele($parca);
        $this->sonra[$parca] = $is;
    }
    public function onyukle(): void {
        foreach ($this->saglayicilar as [$kaydet]) $kaydet($this);     // 1. evre: kayıt
        foreach ($this->saglayicilar as [, $onyukle]) $onyukle($this); // 2. evre: önyükleme
    }
    public function isle(): Yanit {
        Gunluk::yeniIstek();
        $this->sonra = [];
        foreach ($this->izleyiciler as $parca => $is) {
            Gunluk::yola($parca); Gunluk::kostu($parca); $is();
        }
        [$parca, $is] = $this->eylem;
        Gunluk::yola($parca); Gunluk::kostu($parca);
        return $is($this);
    }
    public function bitir(): array {           // yanıt istemciye gitti
        foreach ($this->sonra as $is) $is();
        return array_keys($this->sonra);
    }
}
```

Bu sınıf `kostu.php` çekirdeğine üç çağrıyla bağlanır. Her derste aynı yirmi iki satır yeniden verilir.

```php
<?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ı',
    };
}
```

**ÇT6.** `onyukle()` iki ayrı döngüdür ve bu bilerek böyledir. Birinci döngü bitmeden ikinci döngü başlamaz, yani B sağlayıcısının kaydı, A sağlayıcısının önyüklemesinden önce koşar. A, önyüklemesinde B'nin sunduğuna güvenebilir; bunun tersi de doğrudur. Tek döngüde sağlayıcı sırası bu güveni bozardı. `isle()` ise `onyukle()` çağrısından bağımsızdır: model, önyüklemenin bir kez koştuğu ve ardından birçok isteğin işlendiği bir süreci temsil eder. Bir sağlayıcıyı kayıt evresinde başkasının sunduğuna dayandırmak da yanlış olurdu: o anda öteki sağlayıcı henüz kaydolmamış olabilir ve ortada bağlanmış bir şey yoktur. Kayıt evresinde yalnızca bildirim yapılır, kullanım önyükleme evresine bırakılır.

C Temelleri kursunda derleme aşamaları (ön işleme, derleme, birleştirme, bağlama) program çalışmadan önce koşan ayrı adımlardı. Burada böyle bir ayrım yoktur: önyükleme, kayıt, işleme ve yanıttan sonra evresi, hepsi çalışma anında koşar ve dört sınıf da çalışma anındadır. **Ertelendi bir aşama değil, işin yanıtın dışına bırakılmasıdır.**

#### Yanıttan Sonra Koşan İş

Bir denetleyici bazen yanıtı geciktirmemesi gereken bir iş ister: bir sayacı artırmak, bir istatistik satırı yazmak. İş yanıtın içeriğini etkilemiyorsa, istemcinin onu beklemesi için bir neden yoktur. Çerçeve bunun için `yanittanSonra()` sunar: iş kaydedilir, yanıt oluşturulur ve istemciye gönderildikten sonra `bitir()` onu koşturur. `istatistik` işi iki biçimde ölçülür. Birinde eylem işi `yanittanSonra()` ile bırakır, ötekinde işi doğrudan çağırır ve `Gunluk::kostu()` ile bildirir.

```php
<?php
// yasamdongusu.php — yanıttan sonra koşan iş yanıtın dışındadır
declare(strict_types=1);
require_once 'yasam.php';

function kur(bool $dogrudan): Cekirdek {
    $c = new Cekirdek();
    $c->saglayici(
        fn(Cekirdek $c) => $c->evreler[] = 'kayıt A',
        fn(Cekirdek $c) => $c->evreler[] = 'önyükleme A',
    );
    $c->saglayici(
        fn(Cekirdek $c) => $c->evreler[] = 'kayıt B',
        fn(Cekirdek $c) => $c->evreler[] = 'önyükleme B',
    );
    $c->eylem('kaydet', function (Cekirdek $c) use ($dogrudan): Yanit {
        $is = fn() => null;
        if ($dogrudan) { Gunluk::kostu('istatistik'); $is(); }
        else { $c->yanittanSonra('istatistik', $is); }
        return new Yanit(201, 'kaydedildi');
    });
    $c->onyukle();
    return $c;
}

foreach ([false, true] as $dogrudan) {
    $c = kur($dogrudan);
    echo implode(', ', $c->evreler), "\n";
    $yanit = $c->isle();
    printf("%d %s | istatistik %s\n", $yanit->durum, $yanit->govde, sinifla('istatistik'));
    echo "yanıttan sonra: ", implode(', ', $c->bitir()) ?: '-', "\n";
}
```

```
kayıt A, kayıt B, önyükleme A, önyükleme B
201 kaydedildi | istatistik ertelendi
yanıttan sonra: istatistik
kayıt A, kayıt B, önyükleme A, önyükleme B
201 kaydedildi | istatistik koştu
yanıttan sonra: -
```

**ÇT7.** İki koşumun yanıtı aynıdır: 201 ve `kaydedildi`. Gövdede `istatistik` ile ilgili tek bir iz yoktur; ikisini ayıran, yalnızca `Gunluk`'un işi hangi listede bulduğudur. `yanittanSonra()` ile bırakılan iş `ertele()` çağrısıyla `$ertelenen` listesine girdi, `isle()` döndüğünde hâlâ koşmamıştı ve `bitir()` ondan sonra onu koşturdu. Doğrudan çağrılan iş ise yanıt oluşmadan önce, eylemin içinde koştu.

Modelde gönderim, `isle()` çağrısının `Yanit` döndürmesidir ve `bitir()` bunun ardından gelir. Gerçek bir sunucuda gönderim, çıktının istemciye bırakılması ve bağlantının kapatılmasıdır; yanıttan sonra koşan iş aynı süreçte ve aynı bellekte, ama istemci artık beklemezken koşar. Bu, önceki dersin işçisinden farklıdır: `Kuyruk` işi başka bir sürecin alabileceği bir yere bırakır ve isteğin süreciyle bağı kopar, `yanittanSonra()` ise isteğin süreci içinde kalır. İkisi de ertelendi sınıfındadır, ama arıza biçimleri farklıdır; süreç yanıttan hemen sonra kapanırsa kayıtlı iş hiç koşmayabilir.

Aynı işin iki ayrı sınıfa düşmesi, sınıfın işin kendisine değil işin kaydedildiği yere bağlı olduğunu gösterir. Karar sırası (koştu, ertelendi, atlandı, bağlanmadı) burada da geçerlidir: ilk koşumda `istatistik` `$kostu` listesinde yoktur, `$ertelenen` listesindedir; ikincisinde `$kostu` listesindedir ve sıra orada biter. Beklenen parçayı geliştirici adlandırdı: iki koşumda da aynı `istatistik` adı sorgulandı.

Bu dersin ölçü sınıfı **ertelendi**'dir. Ertelenen iş kaybolmuş değildir ve yanıtın dışında koşar; ama bir hata çıkarsa yanıt çoktan 201 olarak gitmiştir ve hata isteğin sonucunda görünmez. Sınıfların tehlike sırası burada da geçerlidir. Atlanan parça bir durum koduyla, ertelenen parça işçide ya da `bitir()` sonrasında görünür; en tehlikeli sınıf yine **bağlanmadı**'dır, çünkü hiçbir yerde görünmez. Ertelenen işin bağlanmadı olup olmadığını, yani `yanittanSonra()` çağrısının hiç yapılıp yapılmadığını, yine yalnızca geliştiricinin beklediği ad söyler.

#### Hangi İş Yanıttan Sonraya Bırakılır

Bir işi yanıttan sonraya bırakmak her zaman doğru değildir. Ayrımı yapan soru, yanıtın o işin sonucuna bağlı olup olmadığıdır. Eylem bir kaydı kaydedip yanıtta kaydın numarasını döndürüyorsa, numara kaydın kendisinden gelir ve iş doğrudan çağrılmalıdır. Yanıt işin sonucuna bağlı değilse, bir istatistik satırı örneğinde olduğu gibi, iş yanıttan sonraya bırakılabilir ve istemci onu beklemez. İş sonradan başarısız olursa istemciye haber verecek bir yanıt kalmadığı için, bırakılan işin başarısızlığı başka bir kanaldan, bir günlükten ya da bir uyarıdan duyurulmalıdır; bu kanalı seçmek bu dersin konusu değildir.

İki koşumun ortak bir noktası da vardır: eylem iki durumda da çağrıldı ve iki durumda da 201 döndürdü. Yanıt kodu, işin koşup koşmadığı hakkında bilgi vermez. İşin kendisi `Gunluk`'ta aranır. Bu yüzden bir geliştirici "istatistik yazıldı mı" diye sorduğunda, yanıtın 201 olması cevap değildir; cevap işin hangi listede bulunduğudur.

#### Sınırlayıcı Ölçüm

Önyükleme bir kez koşar, istekler birçok kez. Sağlayıcının önyükleme evresinde kurduğu bir parça her istekte koşar mı? Bunu iki parçayla ölçeriz: önyükleme evresinde yalnızca bir kez çağrılan `ayarYukle` ve aynı evrede kaydedilen, sonra her istekte çağrılan `zamanlayici`. İki istek işlenir ve her birinde iki ad sınıflanır.

```php
<?php
// onyukleme.php — önyüklemede kurulan parça her istekte koşar mı
declare(strict_types=1);
require_once 'yasam.php';

$c = new Cekirdek();
$c->saglayici(
    fn(Cekirdek $c) => null,
    function (Cekirdek $c): void {
        Gunluk::kostu('ayarYukle');                   // önyükleme evresinde, istek başlamadan
        $c->izleyici('zamanlayici', fn() => null);    // her istekte çağrılacak parça
    },
);
$c->eylem('goster', fn() => new Yanit(200, 'tamam'));
$c->onyukle();
printf("önyükleme sonrası  ayarYukle %s\n", sinifla('ayarYukle'));
foreach ([1, 2] as $n) {
    $c->isle();
    printf("istek %d  ayarYukle %-11s zamanlayici %s\n", $n, sinifla('ayarYukle'), sinifla('zamanlayici'));
}
```

```
önyükleme sonrası  ayarYukle koştu
istek 1  ayarYukle bağlanmadı zamanlayici koştu
istek 2  ayarYukle bağlanmadı zamanlayici koştu
```

**ÇT8.** Beklenen sonuç iki parçanın ayrı sınıflara düşmesidir ve ölçüm bunu doğruladı. `ayarYukle` önyüklemeden hemen sonra **koştu** olarak görünür, çünkü `Gunluk` hâlâ önyüklemenin kaydını tutuyor. İlk `isle()` çağrısı `yeniIstek()` ile listeleri boşaltır ve `ayarYukle` her iki istekte de **bağlanmadı** olur: parça gerçekten koşmuştur, ama isteğin penceresinin dışında. `zamanlayici` ise iki istekte de **koştu**. Önyüklemede kurulan şey her istekte koşar, önyüklemede koşan şey koşmaz. Bağlanmadı burada bir hatayı değil, işin ölçüm penceresinin dışında kalmasını anlatır.

Bu ayrımın pratik karşılığı şudur: bir sağlayıcının önyüklemesine bir izleyici eklemek, o izleyicinin her istekte koşmasını sağlar; ama sağlayıcının önyüklemede yaptığı bir hazırlık işi, bir ayar dosyasını okumak gibi, bir kez yapılır ve sonraki isteklerde yeniden yapılmaz. Geliştirici bir parçanın "her istekte" koşmasını bekliyorsa, onu önyüklemede değil, izleyici gibi isteğin yoluna kaydetmelidir. Önyüklemeye konan iş ise bir kez koşar ve onu her istekte bekleyen biri, ikinci istekte onu bulamaz.

Bu sonucun sınırı modelin kendisidir. Model önyüklemeyi bir kez çağırır; her istekte betiği baştan yükleyen bir düzende önyükleme her isteğin içine düşerdi ve `ayarYukle` her istekte koşardı. Hangi düzenin kullanıldığını çerçeve değil, uygulamanın çalıştırıldığı süreç belirler.

#### Özet

- Önyükleme iki evrede koşar: önce bütün servis sağlayıcıların kaydı, sonra bütün önyüklemeleri; böylece bir sağlayıcı önyüklemesinde başkasının sunduğuna güvenebilir.
- Dört sınıf da çalışma anındadır; ertelendi bir aşama değil, işin yanıtın dışına bırakılmasıdır.
- `yanittanSonra()` ile bırakılan iş yanıt gövdesinde iz bırakmaz ve `bitir()` çağrısından sonra koşar; aynı iş eylemde doğrudan çağrılırsa koştu sınıfına düşer.
- Önyüklemede kurulan `zamanlayici` iki istekte de koştu; önyüklemede koşan `ayarYukle` iki istekte de bağlanmadı olarak göründü.
- Bu dersin ölçü sınıfı **ertelendi**'dir: iş yanıttan sonra koşar ve hatası yanıta yansımaz.

#### Sonraki Adım

Bu derste isteğin hangi eyleme düştüğü hazır verildi: tek bir eylem kayıtlıydı ve her istek ona gitti. Sonraki ders, Yönlendirme, birden çok kaydın aynı isteğe uyduğu durumu soruyor: iki kalıp aynı yola uyarsa hangisinin eylemi çağrılır ve öteki parça isteğe hiç bağlanmış olur mu?

### Yönlendirme

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/yonlendirme

Rota kalıpları, parametreler, gruplar ve adlandırılmış rotalar; kayıt sırasında ilk eşleşen rotanın kazanması ve bir rotanın başka bir rotanın gölgesinde bağlanmadan kalması.

Önceki ders isteğin hangi eyleme düştüğünü hazır verdi. Bu ders o adımı açar. Çerçeve bir isteği karşılamak için kayıtlı rotalara tek tek bakar ve bu bakışın kuralı yalnızca bir cümleden oluşur. İki kalıp aynı yola uyduğunda neyin olacağı, o cümlenin nasıl yazıldığına bağlıdır.

#### Rota Kalıbı ve Parametre

Bir rota (route) dört şeyi bir araya getirir: bir HTTP yöntemi, bir yol kalıbı, bir parça adı ve bir eylem. Kalıp, yolun sabit kısımlarını ve süslü parantezle yazılan **parametreleri** taşır. `/yazilar/{id}` kalıbı `/yazilar/7` yoluna uyar ve eylem `id` parametresini `7` olarak alır. Çerçeve kalıbı bir düzenli ifadeye çevirir: her parametre, eğik çizgi içermeyen bir ya da daha çok karakterle eşleşen bir gruba dönüşür.

Aynı kalıbı birçok rotada yinelememek için rotalar **gruplanır**. Grup ortak bir önek taşır: `/yazilar` grubu içindeki `/{id}` kalıbı gerçekte `/yazilar/{id}` olur. Gerçek çerçevelerde grup ayrıca bir ara katman listesini her üyesine ekleyebilir; ara katmanların kendisi beşinci dersin konusudur, burada yalnızca önek modellenir. Üçüncü araç **adlandırılmış rotadır**: rota bir adla kaydedilir ve URL, kalıptan o adla üretilir. Böylece bir kalıp değişince adresin yazıldığı her yeri aramak gerekmez.

`yonlendirici.php` bu üç aracı ve eşleştirme kuralını tek sınıfta toplar. Bu dosya KAYNAK olarak verilir ve sonraki iki ölçüm onu `require_once` ile yükler.

```php
<?php
// yonlendirici.php — kayıt sırasıyla eşleşen küçük bir yönlendirici
declare(strict_types=1);
require_once 'kostu.php';

final class Yanit {
    public function __construct(public int $durum, public string $govde = '') {}
}

final class Yonlendirici {
    private array $rotalar = [];
    private array $adlar = [];
    private array $onekler = [];
    public function grup(string $onek, Closure $kayit): void {
        $this->onekler[] = $onek;
        $kayit($this);
        array_pop($this->onekler);
    }
    public function rota(string $yontem, string $kalip, string $parca, Closure $eylem, ?string $ad = null): void {
        $kalip = implode('', $this->onekler) . $kalip;
        $this->rotalar[] = [$yontem, $kalip, $parca, $eylem];
        if ($ad !== null) $this->adlar[$ad] = $kalip;
    }
    public function url(string $ad, array $parametre = []): string {
        return preg_replace_callback('#\{(\w+)(?::[^}]+)?\}#',
            fn(array $m) => (string) $parametre[$m[1]], $this->adlar[$ad]);
    }
    public function isle(string $yontem, string $yol): Yanit {
        Gunluk::yeniIstek();
        foreach ($this->rotalar as [$y, $kalip, $parca, $eylem]) {     // ilk eşleşen kazanır
            $desen = '#^' . preg_replace_callback('#\{(\w+)(?::([^}]+))?\}#',
                fn(array $m) => '(?P<' . $m[1] . '>' . ($m[2] ?? '[^/]+') . ')', $kalip) . '$#';
            if ($y === $yontem && preg_match($desen, $yol, $m)) {
                Gunluk::yola($parca);
                Gunluk::kostu($parca);
                return $eylem(array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY));
            }
        }
        return new Yanit(404);
    }
}
```

Çekirdek `kostu.php` her derste aynı yirmi iki satırla yeniden verilir. Yönlendirici ona iki çağrıyla bağlanır: eşleşen rotanın adını hem `Gunluk::yola()` hem `Gunluk::kostu()` ile bildirir.

```php
<?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ı',
    };
}
```

**ÇT9.** `isle()` içindeki `foreach` rotaları kaydedildikleri sırayla dener ve ilk eşleşende `return` eder. Bu satır bütün dersin kuralıdır: **ilk eşleşen kazanır.** Model, hangi kalıbın daha özel olduğunu bir sıralamayla değerlendirmez; "sabit kalıp parametreli kalıptan önce gelir" diye bir kural da yoktur. Sıra, geliştiricinin `rota()` çağrılarını yazdığı sıradır. Gerçek çerçevelerin bazıları bu kuralı kendi ek sıralamalarıyla yumuşatır; burada ölçülen model yumuşatmaz ve bu bilinçli bir sadeleştirmedir.

#### Gruplar ve Adlandırılmış Rotalar

`grup()` bir önek listesiyle çalışır: önek eklenir, grubun içindeki `rota()` çağrıları çalışır ve önek geri alınır. Önek, `rota()` içinde kalıbın başına eklendiği için eşleştirici gruptan habersizdir; yalnızca tam kalıba bakar. Gruplar iç içe yazılabilir ve her kademe kendi önekini ekler. Bu tasarımın bir sonucu vardır: grup yalnızca yazım kolaylığıdır, eşleşmeye yeni bir kural eklemez. `/yazilar` grubunun içinde `/{id}` ile `/yeni` kaydedildiğinde, bu iki kalıp gruptan bağımsız yazılmış `/yazilar/{id}` ve `/yazilar/yeni` ile aynı şekilde davranır.

`url()` ise tersini yapar. Kayıtlı kalıbı alır, her `{parametre}` yerine verilen değeri koyar ve sonucu döndürür. Rota adı kalıba bağlıdır, kalıp ise önekle birlikte saklanmıştır; bu yüzden grubun önekini değiştirmek, adı kullanan her kodun ürettiği adresi birlikte değiştirir. Adlandırılmış rotanın bu dersteki işi bu kadardır: adres üretimini kalıpta tek bir yere bağlar. Hangi rotanın bir isteğe uyacağı hakkında hiçbir söz taşımaz.

#### İlk Eşleşen Kazanır

`yonlendirme.php` bir `/yazilar` grubu kurar ve grubun içine iki rota koyar: `/{id}` kalıbıyla `goster` ve `/yeni` kalıbıyla `form`. `kur()` iki kayıt sırası üretir. İlk sırada `goster` önce, `form` sonra kaydedilir. İkinci sırada tersi olur. Çıktıdaki `sonra` ve `önce` sözcükleri `form`'un `goster`'den sonra mı önce mi kaydedildiğini söyler. İki yönlendirici de aynı yola, `/yazilar/yeni` adresine sorulur; geliştiricinin beklediği parça `form`'dur. Grup içindeki `goster` rotası ayrıca `yazilar.goster` adını taşır ve son satır bu addan `id` için `7` değeriyle URL üretir.

```php
<?php
// yonlendirme.php — kayıt sırası hangi rotanın bağlanacağını belirler
declare(strict_types=1);
require_once 'yonlendirici.php';

function kur(bool $yeniOnce): Yonlendirici {
    $y = new Yonlendirici();
    $y->grup('/yazilar', function (Yonlendirici $y) use ($yeniOnce) {
        $form = fn() => new Yanit(200, 'boş form');
        $goster = fn(array $p) => new Yanit(200, "yazı {$p['id']}");
        if ($yeniOnce) $y->rota('GET', '/yeni', 'form', $form);
        $y->rota('GET', '/{id}', 'goster', $goster, ad: 'yazilar.goster');
        if (!$yeniOnce) $y->rota('GET', '/yeni', 'form', $form);
    });
    return $y;
}

foreach (['sonra' => false, 'önce' => true] as $ad => $yeniOnce) {
    $y = kur($yeniOnce);
    $yanit = $y->isle('GET', '/yazilar/yeni');
    printf("yeni %-5s %d %-9s form %s\n", $ad, $yanit->durum, $yanit->govde, sinifla('form'));
}
echo $y->url('yazilar.goster', ['id' => 7]), "\n";
```

```
yeni sonra 200 yazı yeni form bağlanmadı
yeni önce 200 boş form form koştu
/yazilar/7
```

**ÇT10.** Aynı yol, aynı iki rota, tek fark kayıt sırası. `goster` önce kaydedildiğinde `/yazilar/yeni` ona uydu: `{id}` parametresi `yeni` değerini aldı, yanıt 200 oldu ve gövdesi `yazı yeni` çıktı. `form` eylemi **hiç çağrılmadı**; `Gunluk` onu isteğin yoluna koymadı ve karar sırası `$kostu`, `$ertelenen`, `$yol` listelerinde de bulamayınca **bağlanmadı** dedi. `form` önce kaydedildiğinde aynı yol ona uydu ve `boş form` yanıtı geldi: `form` **koştu**. Yanıt iki durumda da 200'dür ve bir hata ya da uyarı çıkmaz; iki durumu yalnızca gövde ve `Gunluk` ayırır.

Sorun bu yüzden sinsidir. Gölgelenen rota kayıtlıdır ve rota tablosunu okuyan biri onu görür. İstek ise ona hiç ulaşmaz ve çerçeve bunu bildirmez; `yazı yeni` yanıtı da bir hata gibi görünmez, çünkü `yeni` geçerli bir `id` değeri gibi okunabilir. Geliştirici bir parçanın koşmadığını yanıtın bir yerinden değil, **beklediği parçayı adlandırıp sorgulayarak** anlar.

Üretilen URL de modelin parçasıdır: son satırdaki `/yazilar/7`, kalıptan türetilmiştir. Adresi elle yazmak yerine adı kullanan kod, kalıp değiştiğinde kırılmaz. Ama adlandırılmış rota yalnızca URL üretir; hangi rotanın bir isteğe uyacağını belirleyen şey yine kayıt sırasıdır.

Bu dersin ölçü sınıfı **bağlanmadı**'dır. Karar sırasının sonuncusu olan bu sınıf, ilk derste en tehlikeli sınıf olarak adlandırılmıştı: atlanan parça bir durum koduyla, ertelenen parça işçide ya da yanıttan sonra görünür, bağlanmayan parça ise hiçbir yerde.

#### Rota Tablosunu Okumak

Gölgelenen bir rotayı bulmanın tek güvenilir yolu, o rotanın adını önceden yazıp isteğe sormaktır. Rota tablosunu gözle okuyan biri `form` rotasını kayıtlı görür ve işi bitmiş sanır. `Gunluk` ise tabloyu değil isteği okur: bu istekte `form` yoldaydı mı, çağrıldı mı. İki soru ayrıdır ve yalnızca ikincisi geliştiricinin gerçekte merak ettiği şeydir.

Bu okuma alışkanlığının iki pratik sonucu vardır. Birincisi, yeni bir rota eklemek var olan bir rotayı gölgeleyebilir: sonradan eklenen geniş bir kalıp, kendinden sonra kaydedilmiş bütün dar kalıpları örter. Ekleyenin görmediği bu etki, aynı isteği önce ve sonra ölçmekle yakalanır. İkincisi, bir rotanın koşmadığını anlayan geliştirici ilk olarak eylemin içine bakmaya meyleder; oysa eylem hiç çağrılmamıştır ve içinde aranacak bir şey yoktur. Doğru ilk soru, isteğin hangi rotaya düştüğüdür.

#### Sınırlayıcı Ölçüm

Gölgeyi sıra mı doğuruyor? Önceki ölçüm sırayı değiştirince sonucun değiştiğini gösterdi; ama bu, sıranın tek etken olduğunu kanıtlamaz. Sınırlayıcı ölçüm sırayı **değiştirmez**. `goster` yine önce kaydedilir, yalnız kalıbı `{id:\d+}` olarak daraltılır: parametre yalnızca rakamlarla eşleşir. Aynı iki yola sorulur: `/yazilar/yeni` ve `/yazilar/7`.

```php
<?php
// kisit.php — gölgeyi sıra değil kalıbın genişliği doğurur
declare(strict_types=1);
require_once 'yonlendirici.php';

$y = new Yonlendirici();
$y->grup('/yazilar', function (Yonlendirici $y) {
    $y->rota('GET', '/{id:\d+}', 'goster', fn(array $p) => new Yanit(200, "yazı {$p['id']}"));
    $y->rota('GET', '/yeni', 'form', fn() => new Yanit(200, 'boş form'));
});

foreach (['/yazilar/yeni' => 'form', '/yazilar/7' => 'goster'] as $yol => $parca) {
    $yanit = $y->isle('GET', $yol);
    printf("%-14s %d %-9s %s %s\n", $yol, $yanit->durum, $yanit->govde, $parca, sinifla($parca));
}
```

```
/yazilar/yeni  200 boş form form koştu
/yazilar/7     200 yazı 7   goster koştu
```

**ÇT11.** Sıra aynı kaldı ve `form` bu kez **koştu**. `goster` kalıbı artık `yeni` metniyle eşleşmediği için `form` rotasına sıra geldi; `/yazilar/7` ise yine `goster`'e düştü. Gölgeyi doğuran şey sıra değil, **geniş kalıp**tır: `[^/]+` her metni kabul ettiği için `yeni` sözcüğü de ona uyar. Sıra yalnızca iki kalıp birbirini kapsadığında önem kazanır. Kalıp daraltılırsa sıra önemini yitirir.

Bu iki ölçümün birlikte söylediği şudur: bağlanmadığını gördüğünüz bir rota için iki yoldan biri düzeltir ve ikisi aynı sonucu verir. Dar kalıbı geniş kalıptan önce kaydetmek ya da geniş kalıbı daraltmak. İkincisi daha sağlamdır, çünkü sıra değişse bile sonuç değişmez. Birincisi geliştirici dikkatine dayanır ve bir sonraki rota eklendiğinde yeniden bozulabilir.

Bir sınır daha vardır: bu model, varsayılan olarak eğik çizgi içermeyen tek bir yol parçasını eşleştirir; joker ve isteğe bağlı parametre desteklemez. Gerçek çerçevelerin eşleştiricileri bunları ve alan adı kısıtı gibi daha fazlasını destekler; burada ölçülen yalnızca ilk eşleşen kazanır kuralının sonucudur.

#### Özet

- Rota yöntem, kalıp, parça adı ve eylemden oluşur; grup ortak bir önek ekler, adlandırılmış rota ise kalıptan URL üretir.
- Modelde ilk eşleşen kazanır ve özgüllük sıralaması yoktur; sıra rotaların kaydedildiği sıradır.
- `/yazilar/{id}` önce kaydedildiğinde `/yazilar/yeni` ona uydu, yanıt 200 ve `yazı yeni` oldu, `form` bağlanmadı; sıra değişince `form` koştu.
- Parametreyi `{id:\d+}` ile daraltmak, sırayı değiştirmeden `form` rotasını koştu sınıfına getirdi; gölgeyi sıra değil kalıbın genişliği doğurur.
- Bu dersin ölçü sınıfı **bağlanmadı**'dır: gölgelenen rota hata vermez, yanıt başarılı görünür ve rota yalnızca adlandırılıp sorgulanınca bulunur.

#### Sonraki Adım

Eşleşen rota artık belli: `goster` ya da `form` koştu. Ama rota yalnızca bir eylemi gösterir; eylemin hangi sınıfta durduğu, o sınıfın bağımlılıklarını kimin kurduğu ve yedi eylemli bir kaynağın rotalara nasıl yayıldığı henüz görünmedi. Sonraki ders, Denetleyiciler, rotanın gösterdiği sınıfın nasıl kurulduğunu ve eylemin gerçekten çağrılıp çağrılmadığını soruyor.

### Denetleyiciler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/denetleyiciler

Denetleyici sınıfı ve eylem yöntemi, kaynak denetleyicisinin yedi eylemi rotalara açması ve tek eylemli denetleyici; yapıcı bağımlılıklarının nesne kabı tarafından çözülmesi.

Önceki ders isteği bir rotaya düşürdü, ama rotanın gösterdiği şeyi bir fonksiyon olarak varsaydı. Gerçek uygulamalarda eylem bir sınıfın yöntemidir ve sınıf başka nesnelere dayanır. Bu ders rotadan eyleme giden yolu açar: sınıfı kim kurar, bağımlılığını kim sağlar ve ancak o zaman eylem çağrılır.

#### Denetleyici Sınıfı ve Eylem

Denetleyici (controller), ilgili eylemleri bir sınıfta toplar. Eylem, o sınıfın bir yöntemidir ve bir isteği karşılar: parametreleri alır, bir iş yapar ve bir yanıt döndürür. Rota artık bir fonksiyon değil, bir sınıf adı ve bir yöntem adı taşır. Çerçeve isteği bu rotaya eşledikten sonra üç şey yapar: sınıfı kurar, yöntemi çağırır ve yöntemin döndürdüğü yanıtı geri verir.

Sınıfın kurulması kolay bir adım değildir, çünkü denetleyici başka nesnelere dayanabilir. Bir yazı denetleyicisi yazıları saklayan bir depoya ihtiyaç duyar ve bunu yapıcısında bir parametre olarak ister. Rotanın bu depoyu sağlaması beklenemez; kuran taraf çerçevedir. Çerçeve bunu **nesne kabına** (object container) bırakır: ayrıntısı sonraki derslerde açılan, sınıf adından nesne kuran fabrikaya. Burada bilinmesi gereken tek şey, kabın yapıcı parametrelerinin tipine bakıp onları da kurmasıdır. Bağımlılık Enjeksiyonu dersinde ölçülen şey bu çözümlemenin yapısıydı; burada yalnız denetleyicinin kurulup eylemin çağrılması önemlidir.

`denetleyici.php` bu zinciri tek dosyada kurar. `Yonlendirici` rotaya bir sınıf ve yöntem adı kaydeder, `isle()` eşleşen rotanın denetleyicisini kaptan alır ve eylemi çağırır. Kap, yalnız sınıf tipli yapıcı parametresini çözer; sınıf tipi olmayan bir parametre bu modelin kapsamı dışındadır. Dosya KAYNAK olarak verilir, ölçümler onu yükler.

```php
<?php
// denetleyici.php — rotayı bir denetleyici sınıfına ve eylem yöntemine bağlar
declare(strict_types=1);
require_once 'kostu.php';

final class Yanit {
    public function __construct(public int $durum, public string $govde = '') {}
}

final class NesneKabi {
    public function al(string $ad): object {
        $bagimliliklar = [];
        foreach ((new ReflectionClass($ad))->getConstructor()?->getParameters() ?? [] as $p) {
            $bagimliliklar[] = $this->al($p->getType()->getName());
        }
        return new $ad(...$bagimliliklar);
    }
}

final class Yonlendirici {
    private array $rotalar = [];
    public function __construct(private NesneKabi $kap) {}
    public function rotalar(): array { return $this->rotalar; }
    public function rota(string $yontem, string $kalip, string $parca, string $sinif, string $eylem = '__invoke'): void {
        $this->rotalar[] = [$yontem, $kalip, $parca, $sinif, $eylem];
    }
    public function kaynak(string $ad, string $sinif): void {
        $yedi = [
            ['GET', "/$ad", 'liste'], ['GET', "/$ad/olustur", 'olustur'], ['POST', "/$ad", 'kaydet'],
            ['GET', "/$ad/{id}", 'goster'], ['GET', "/$ad/{id}/duzenle", 'duzenle'],
            ['PUT', "/$ad/{id}", 'guncelle'], ['DELETE', "/$ad/{id}", 'sil'],
        ];
        foreach ($yedi as [$yontem, $kalip, $eylem]) {
            if (method_exists($sinif, $eylem)) $this->rota($yontem, $kalip, $eylem, $sinif, $eylem);
        }
    }
    public function isle(string $yontem, string $yol): Yanit {
        Gunluk::yeniIstek();
        foreach ($this->rotalar as [$y, $kalip, $parca, $sinif, $eylem]) {      // ilk eşleşen kazanır
            $desen = '#^' . preg_replace('#\{(\w+)\}#', '(?P<$1>[^/]+)', $kalip) . '$#';
            if ($y === $yontem && preg_match($desen, $yol, $m)) {
                Gunluk::yola($parca);
                $denetleyici = $this->kap->al($sinif);       // yapıcı bağımlılıkları çözülür
                Gunluk::kostu($parca);
                return $denetleyici->$eylem(array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY));
            }
        }
        return new Yanit(404);
    }
}
```

Çekirdek `kostu.php` her derste aynı yirmi iki satırla yeniden verilir.

```php
<?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ı',
    };
}
```

**ÇT12.** `isle()` iki ayrı bildirim yapar ve ikisinin arasında denetleyici kurulur. `Gunluk::yola()` rota eşleştiğinde çağrılır; `Gunluk::kostu()` ise `$this->kap->al($sinif)` satırından sonra, yani denetleyici kurulduktan sonra çağrılır. Kurulum bir istisna atsaydı, parça yola konmuş ama koştu listesine girmemiş olurdu; `sinifla()` kuralına göre o parça **atlandı** sayılırdı. Bu sıra bilinçlidir: koştu, eylemin çağrılacağı noktaya gelindiğini söyler; çağrıdan önceki her adım yoldadır, ama henüz koşmamıştır.

#### Kaynak Denetleyicisi ve Tek Eylemli Denetleyici

Çoğu uygulamada bir varlık aynı yedi eylem etrafında döner: listele, oluşturma formunu göster, kaydet, göster, düzenleme formunu göster, güncelle ve sil. Her biri için bir rota yazmak yinelemedir. **Kaynak denetleyicisi** (resource controller) bu yedi eylemi tek bir kayıtla rotalara açar. `kaynak()` yedi satırlık bir tablo taşır ve tanımlı her eylem için bir rota kaydeder.

| Yöntem | Kalıp | Eylem |
|---|---|---|
| GET | /yazilar | liste |
| GET | /yazilar/olustur | olustur |
| POST | /yazilar | kaydet |
| GET | /yazilar/{id} | goster |
| GET | /yazilar/{id}/duzenle | duzenle |
| PUT | /yazilar/{id} | guncelle |
| DELETE | /yazilar/{id} | sil |

Tablodaki sıra rastgele değildir. `/yazilar/olustur` kalıbı `/yazilar/{id}` kalıbından önce gelir, çünkü önceki derste ölçüldüğü gibi ilk eşleşen kazanır: sıra ters olsaydı `olustur` sözcüğü bir `id` olarak okunurdu. Kaynak denetleyicisi yedi rotayı bu sırayla kaydeder ve dar kalıbı geniş kalıptan önce koyar.

Her eylem bir yöntemdir, ama bazı eylemler tek başına bir sınıfı hak eder. **Tek eylemli denetleyici** yalnızca `__invoke` yöntemini taşır ve rota yalnızca sınıf adını verir. Yayınlama gibi, bir varlığın standart yedi eylemine girmeyen bir işlem için uygundur. Aşağıdaki dosya hem kaynak denetleyicisini hem tek eylemli denetleyiciyi kaydeder, yedi rotayı ve bir ek rotayı yazdırır, sonra iki isteği sınıflar.

```php
<?php
// denetleyiciler.php — kaynak denetleyicisi yedi eylemi yedi rotaya açar
declare(strict_types=1);
require_once 'denetleyici.php';

final class Depo {}

final class YaziDenetleyicisi {
    public function __construct(private Depo $depo) {}
    public function liste(): Yanit { return new Yanit(200, 'liste'); }
    public function olustur(): Yanit { return new Yanit(200, 'form'); }
    public function kaydet(): Yanit { return new Yanit(201, 'kaydedildi'); }
    public function goster(array $p): Yanit { return new Yanit(200, 'yazı ' . $p['id'] . ' (' . $this->depo::class . ')'); }
    public function duzenle(array $p): Yanit { return new Yanit(200, "düzenle {$p['id']}"); }
    public function guncelle(array $p): Yanit { return new Yanit(200, "güncellendi {$p['id']}"); }
    public function sil(array $p): Yanit { return new Yanit(204); }
}

final class YayinlaDenetleyicisi {
    public function __invoke(array $p): Yanit { return new Yanit(200, "yayında {$p['id']}"); }
}

$y = new Yonlendirici(new NesneKabi());
$y->kaynak('yazilar', YaziDenetleyicisi::class);
$y->rota('POST', '/yazilar/{id}/yayinla', 'yayinla', YayinlaDenetleyicisi::class);

foreach ($y->rotalar() as [$yontem, $kalip, $parca]) {
    printf("%-6s %-22s %s\n", $yontem, $kalip, $parca);
}
foreach ([['GET', '/yazilar/7', 'goster'], ['POST', '/yazilar/7/yayinla', 'yayinla']] as [$yontem, $yol, $parca]) {
    $yanit = $y->isle($yontem, $yol);
    printf("%-4s %-19s %d %-17s %s %s\n", $yontem, $yol, $yanit->durum, $yanit->govde, $parca, sinifla($parca));
}
```

```
GET    /yazilar               liste
GET    /yazilar/olustur       olustur
POST   /yazilar               kaydet
GET    /yazilar/{id}          goster
GET    /yazilar/{id}/duzenle  duzenle
PUT    /yazilar/{id}          guncelle
DELETE /yazilar/{id}          sil
POST   /yazilar/{id}/yayinla  yayinla
GET  /yazilar/7          200 yazı 7 (Depo)    goster koştu
POST /yazilar/7/yayinla  200 yayında 7        yayinla koştu
```

**ÇT13.** Yedi eylem yedi rotaya açıldı ve sekizinci rota tek eylemli denetleyiciye ait. `/yazilar/7` isteğinde `YaziDenetleyicisi` kuruldu ve yapıcısındaki `Depo` parametresi kap tarafından `ReflectionClass` ile çözüldü; yanıt gövdesindeki `Depo` adı bunun kanıtıdır. Hiçbir yerde `new Depo()` yazılmadı. Eylem **koştu**. Tek eylemli denetleyici de aynı biçimde kuruldu ve `__invoke` çağrıldı; `yayinla` da koştu.

Bu dersin ölçü sınıfı **koştu**'dur. Ama koştu sınıfı yeni bir şeyi söylüyor: bir eylemin koşması için yalnız rotanın eşleşmesi yetmez, denetleyicinin kurulabilmesi de gerekir. Kurulamayan bir denetleyicinin eylemi çağrılmaz; `sinifla()` kuralını okursak o parça yolda kalır ve atlandı sınıfına düşer. Bu dersin ölçümleri bunu koşturmaz, yalnız kurala dayanır.

#### Denetleyicinin İşi

Denetleyici, isteğin sınırında durur. İsteğin parametrelerini alır, işi başka nesnelere yaptırır ve yanıtı kurar. İşin kendisini, yani bir yazının nasıl saklanacağını ya da hangi kuralla doğrulanacağını denetleyicide yazmak, o kodu isteğe bağlar: bir komut satırı betiğinden ya da bir işçiden çağrılamaz. Bu yüzden eylem küçük tutulur ve gerçek iş, yapıcıda istenen `Depo` gibi bir nesnede durur. Denetleyicinin yapıcıda bağımlılık istemesinin nedeni de budur: eylem işi yapan nesneyi kendisi kurmaz, kendisine verilmesini bekler.

Bu ölçümde yanıtlar bilerek kısa tutuldu: `sil` eylemi yalnızca 204 durumuyla bir yanıt döndürür, `kaydet` 201 ile bir ileti verir. Eylemin döndürdüğü her şey `Yanit` nesnesidir; eylem çıktıyı kendisi yazmaz. Çerçeve yanıtı alır ve göndermeyi üstlenir. Eylemin yanıt yerine doğrudan çıktı yazması, yanıttan sonra koşan işlerin ve ara katmanların dönüş yolunda yanıta dokunmasını imkânsız kılar.

İki denetleyici biçimi arasındaki seçim bir üslup kararıdır. Kaynak denetleyicisi, standart yedi eylem etrafında dönen bir varlık için tek sınıfla yedi rotayı kaydeder ve adresleri kuralla belirler. Tek eylemli denetleyici, yedi eylemin hiçbirine uymayan bir işlem için kullanılır ve rota tablosunda sınıfın adı işlemin adıdır. Birini seçmek eşleşmeyi değiştirmez; ikisinde de ilk eşleşen kazanır ve ikisinde de eylemin koşması denetleyicinin kurulmasına bağlıdır.

#### Sınırlayıcı Ölçüm

Kaynak denetleyicisi, tanımlı olmayan eylemler için rota kaydetmez: `kaynak()` içindeki `method_exists` denetimi yedi eylemi süzer. `TaslakDenetleyicisi` yalnız `liste` ve `goster` taşır. Bu durumda `GET /taslaklar/7/duzenle` isteği ne olur? Parça yoldaki başka bir adıma mı bağlanır, yoksa istek 404 mü verir? İkinci istek `GET /taslaklar/olustur`, yani tanımsız bir eylemin sabit adresidir.

```php
<?php
// eksik.php — tanımsız eylemin rotası hiç kaydedilmez
declare(strict_types=1);
require_once 'denetleyici.php';

final class TaslakDenetleyicisi {
    public function liste(): Yanit { return new Yanit(200, 'taslaklar'); }
    public function goster(array $p): Yanit { return new Yanit(200, "taslak {$p['id']}"); }
}

$y = new Yonlendirici(new NesneKabi());
$y->kaynak('taslaklar', TaslakDenetleyicisi::class);

foreach ([['/taslaklar/7/duzenle', 'duzenle'], ['/taslaklar/olustur', 'olustur']] as [$yol, $parca]) {
    $yanit = $y->isle('GET', $yol);
    printf("%-21s %d %-16s %s %s\n", $yol, $yanit->durum, $yanit->govde, $parca, sinifla($parca));
}
```

```
/taslaklar/7/duzenle  404                  duzenle bağlanmadı
/taslaklar/olustur    200 taslak olustur   olustur bağlanmadı
```

**ÇT14.** Sonuç iki ayrı biçimde çıktı. `duzenle` için rota hiç kaydedilmediğinden hiçbir kalıp isteğe uymadı: yanıt **404** oldu ve `duzenle` **bağlanmadı**. Bu bir bağlanmadı olmasına rağmen sessiz değildir, çünkü 404 durum kodu isteğin hiçbir rotaya düşmediğini söyler. `olustur` için ise tablo sabit adresi kaydetmedi, ama `/taslaklar/{id}` kalıbı `olustur` sözcüğünü bir `id` olarak kabul etti: yanıt **200** ve gövde `taslak olustur`, parça yine **bağlanmadı**. Aynı sınıf, bu kez bir durum koduyla değil sessizce göründü. İki durumda da geliştiricinin beklediği parça aynı adlandırmayla sorgulanıp aynı sınıfa düştü; ayrımı yanıtın durumu yaptı.

Bu sonuç önceki dersle birleşir: bir rotanın hiç kaydedilmemesi ile bir rotanın gölgelenmesi aynı bağlanmadı sınıfına düşer, ama biri 404 ile, öteki 200 ile gelir. Kaynak denetleyicisinin eksik bıraktığı eylemi geliştirici fark etmeyebilir; çerçeve bunu bir uyarı olarak vermez, çünkü `method_exists` süzmesi bilerek sessizdir.

#### Özet

- Rota bir sınıf adı ve yöntem adı taşır; çerçeve eşleşmeden sonra denetleyiciyi nesne kabından alır ve yapıcı bağımlılıklarını `ReflectionClass` ile çözdürür.
- Kaynak denetleyicisi yedi eylemi yedi rotaya açar ve dar kalıbı geniş kalıptan önce kaydeder; tek eylemli denetleyici yalnızca `__invoke` taşır.
- `/yazilar/7` isteğinde denetleyici kuruldu, `Depo` bağımlılığı çözüldü ve eylem koştu; yayinla da aynı biçimde koştu.
- Tanımsız `duzenle` eyleminin rotası kaydedilmediği için istek 404 verdi ve parça bağlanmadı; tanımsız `olustur` eylemi `/taslaklar/{id}` kalıbına düştü, yanıt 200 oldu ve parça yine bağlanmadı.
- Bu dersin ölçü sınıfı **koştu**'dur: eylemin koşması için rotanın eşleşmesi ve denetleyicinin kurulması gerekir.

#### Sonraki Adım

Rota denetleyiciyi kurdu ve eylemi çağırdı. Ama her isteğin eylemden önce geçmesi gereken adımlar da vardır: oturumun açık olup olmadığı, girdinin geçerli olup olmadığı. Bu denetimler eylemin içine yazılırsa her denetleyicide yinelenir. Sonraki ders, Ara Katmanlar, bu denetimleri eylemin önüne koyar ve tek bir soruyu soruyor: önündeki katman yanıt verirse eylem ve arkadaki katmanlar ne olur?

### Ara Katmanlar

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/ara-katmanlar

Kesişen ilgilerin eylemin önüne konan katmanlara taşınması, isteğin katmanlardan geçerek eyleme ulaşması, $sonraki çağrısının yapılması ya da yapılmaması ve halka sırasının sonucu belirlemesi.

Önceki ders eylemin koşması için rotanın eşleşmesinin ve denetleyicinin kurulmasının gerektiğini gösterdi. Ama eylemin önünde, her isteğin geçmesi gereken adımlar da olabilir. Bu denetimler eylemin içine yazılırsa her denetleyicide yinelenir. Çerçeve onları eylemin önüne tek bir yere koyar ve eylem çağrılmadan önce o yerde bir yanıt oluşabilir.

#### Kesişen İlgiler ve Ara Katman

Oturumun açık olup olmadığı, girdinin geçerli olup olmadığı, yanıta bir başlık eklenmesi gibi işler, birçok eylemi aynı biçimde ilgilendirir. Bunlara **kesişen ilgi** (cross-cutting concern) denir: hiçbir eyleme özgü değildir, ama hepsinin yolundan geçer. **Ara katman** (middleware) bu işi tek bir yerde yapan bir fonksiyondur. İsteği ve bir `$sonraki` fonksiyonunu alır ve iki şeyden birini yapar: ya isteği `$sonraki`'ye aktararak zincirin devamını çağırır, ya da `$sonraki`'yi çağırmadan kendi yanıtını döndürür.

Katmanlar bir **zincir** oluşturur. Eylem zincirin en içindeki halkadır; her katman bir dış halkadır ve içindekini `$sonraki` olarak alır. İstek en dıştaki halkaya girer, her katman `$sonraki`'yi çağırdıkça bir içeri iner ve eyleme ulaşır. Yanıt aynı yoldan tersine döner: eylemin yanıtı en içteki katmana, oradan bir dıştakine ve en sonunda çağırana ulaşır. Bu yüzden bir katmanın iki bölümü olabilir: `$sonraki` çağrısından önce koşan kısım ve çağrıdan sonra, dönüşte koşan kısım.

`katman.php` bu yapıyı kurar. `zincirle()` katmanları ters sırayla sarar: eylem en içte, en son sarılan katman en dışta kalır. Üç hazır katman vardır. `oturumKatmani()` oturum yoksa 302 ile `/giris` adresine yönlendiren yanıtı kendisi verir. `dogrulamaKatmani()` başlık boşsa 422 verir. `izKatmani()` `$sonraki`'yi çağırır ve **dönüşte** yanıta bir başlık ekler.

Bu yapının bir sonucu, ara katmanın yalnız istekle ilgilenen bir süzgeç olmadığıdır. `$sonraki` çağrısının iki yanı vardır ve her yan ayrı bir iş için uygundur. Çağrıdan önceki kısım isteği inceler ya da değiştirir: oturumu okur, girdiyi doğrular, isteğe bir alan ekler. Çağrıdan sonraki kısım yanıtı inceler ya da değiştirir: başlık ekler, gövdeyi sarar, süre ölçer. Tek bir katman ikisini de yapabilir ve ikisi de aynı fonksiyonun içinde, aynı `$sonraki` çağrısının iki yanında durur.

Katmanın üçüncü bir seçeneği daha vardır ve bu ders onu kasıtlı olarak ölçmez: `$sonraki`'yi birden çok kez çağırmak ya da hiç dönmemek. Çerçeveler bunu genellikle kullanıcıya bırakır. Kural basittir: bir katman ya zinciri sürdürür ya da yanıtı kendisi verir ve hangisini yaptığı zincirin geri kalanı için belirleyicidir.

```php
<?php
// katman.php — iç içe geçmiş ara katmanlar ve zincir
declare(strict_types=1);
require_once 'kostu.php';

final class Istek {
    public function __construct(
        public string $yol, public array $govde = [], public bool $oturum = false,
    ) {}
}

final class Yanit {
    public array $baslik = [];
    public function __construct(public int $durum, public string $govde = '') {}
}

function oturumKatmani(): array {
    return ['oturum', fn(Istek $i, Closure $sonraki) =>
        $i->oturum ? $sonraki($i) : new Yanit(302, '/giris')];
}

function dogrulamaKatmani(): array {
    return ['dogrulama', fn(Istek $i, Closure $sonraki) =>
        ($i->govde['baslik'] ?? '') !== '' ? $sonraki($i) : new Yanit(422, 'baslik gerekli')];
}

function izKatmani(): array {                  // $sonraki döndükten sonra da çalışır
    return ['iz', function (Istek $i, Closure $sonraki): Yanit {
        $yanit = $sonraki($i);
        $yanit->baslik['x-iz'] = 'gecti';
        return $yanit;
    }];
}

function zincirle(Istek $istek, array $ara, string $parca, Closure $eylem): Yanit {
    Gunluk::yeniIstek();
    foreach ([...array_column($ara, 0), $parca] as $p) Gunluk::yola($p);
    $hat = function (Istek $i) use ($parca, $eylem): Yanit {
        Gunluk::kostu($parca);
        return $eylem($i);
    };
    foreach (array_reverse($ara) as [$ad, $katman]) {      // en dıştaki katman en son sarılır
        $hat = function (Istek $i) use ($ad, $katman, $hat): Yanit {
            Gunluk::kostu($ad);
            return $katman($i, $hat);
        };
    }
    return $hat($istek);
}
```

Çekirdek `kostu.php` her derste aynı biçimde yeniden verilir.

```php
<?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ı',
    };
}
```

**ÇT15.** `zincirle()` iki iş yapar ve ikisi ayrıdır. İlk `foreach` bütün katman adlarını ve parçayı **yola** koyar: bu istek için zincirde bulunan her halka yoldadır. Sonra her halka çağrıldığında `Gunluk::kostu()` ile kendini koştu olarak bildirir. Fark bilerek böyledir: yol, zincirin ne olduğunu söyler; koştu, zincirin nereye kadar ilerlediğini. Bir halka yoldaysa ama koştu listesinde değilse, ondan önceki bir halka `$sonraki`'yi çağırmamıştır ve karar sırası o parçayı **atlandı** yapar.

#### Önündeki Katman Yanıt Verirse

Zincir `[izKatmani, oturumKatmani, dogrulamaKatmani]` sırasındadır ve eylem `kaydet`'tir. `iz` en dıştadır. İki istek işlenir: oturumsuz ve oturumlu. İkisinin gövdesi geçerlidir, yani doğrulamadan geçecek bir başlık taşır; fark yalnız oturumdadır.

```php
<?php
// katmanlar.php — önündeki katman yanıt verirse arkadaki katmanlar atlanır
declare(strict_types=1);
require_once 'katman.php';

$ara = [izKatmani(), oturumKatmani(), dogrulamaKatmani()];
$eylem = fn(Istek $i) => new Yanit(201, 'kaydedildi');
$govde = ['baslik' => 'Merhaba'];

foreach (['oturumsuz' => false, 'oturumlu' => true] as $ad => $oturum) {
    $yanit = zincirle(new Istek('/yazilar', $govde, $oturum), $ara, 'kaydet', $eylem);
    printf("%s -> %d %s %s\n", $ad, $yanit->durum, $yanit->govde, json_encode($yanit->baslik));
    foreach (['iz', 'oturum', 'dogrulama', 'kaydet'] as $parca) {
        printf("  %-10s %s\n", $parca, sinifla($parca));
    }
}
```

```
oturumsuz -> 302 /giris {"x-iz":"gecti"}
  iz         koştu
  oturum     koştu
  dogrulama  atlandı
  kaydet     atlandı
oturumlu -> 201 kaydedildi {"x-iz":"gecti"}
  iz         koştu
  oturum     koştu
  dogrulama  koştu
  kaydet     koştu
```

**ÇT16.** Oturumsuz istekte `oturum` katmanı `$sonraki`'yi çağırmadı ve 302 döndürdü. Onun arkasındaki `dogrulama` katmanı ve `kaydet` eylemi yoldaydı ama çağrılmadı; ikisi **atlandı**. Oturumlu istekte ise `oturum` `$sonraki`'yi çağırdı ve zincirin her halkası çağrıldı: dört parçanın dördü de **koştu** ve yanıt 201 oldu. Aynı zincir, aynı eylem, tek fark isteğin `oturum` alanıdır.

İlk istekte bir ayrıntı daha vardır: yanıt 302 olduğu hâlde başlıkta `x-iz` var. `iz` katmanı oturum katmanının dışındadır; `$sonraki`'yi çağırmış, yanıtı 302 olarak almış ve dönüşte başlığı eklemiştir. Dönüşte koşan kısım, içerideki bir katman erken yanıt verse bile, dışarıdaki katmanlar için koşar. Yani bir katmanın `$sonraki`'den önceki kısmı atlanabilir, ama dışarıdaki bir katmanın dönüş kısmı atlanmaz.

Bu ölçümün bir sınırı vardır. `Gunluk`, bir halkanın çağrılıp çağrılmadığını söyler; çağrılan halkanın içinde ne yaptığını söylemez. `iz` katmanı koştu olarak görünür, ama başlığı dönüşte eklediği günlükte yoktur; onu yalnız yanıtın başlıklarından okuruz. Koştu sınıfı bu yüzden "halka çağrıldı" demektir, "halkanın bütün işi bitti" demek değildir. Dönüşte koşan kısım bir hatayla kesilmiş olsa da halka koştu görünürdü.

Bu fark, ara katman hatalarında önemlidir. Bir katmanın çağrıdan önceki kısmı hata verirse zincir orada kesilir ve arkadaki halkalar yoldadır ama çağrılmamıştır: günlükte atlandı görünürler. Atlanmanın nedeni normal bir erken yanıt da olabilir, bir hata da; ikisini ayıran şey durum kodu ve yanıt gövdesidir, sınıf değil. Bu yüzden atlandı sınıfı bir arıza işareti değildir, bir kontrol akışı işaretidir.

Bu dersin ölçü sınıfı **atlandı**'dır. Atlanan parça bir durum koduyla kendini ele verir: 302 oturumun, 422 doğrulamanın durduğunu söyler. Ama durum kodu yalnız yanıtı veren katmanı gösterir, arkada kaç parçanın atlandığını göstermez; bunu `Gunluk` listeler.

#### Sınırlayıcı Ölçüm

Halka sırası sonucu değiştirir mi? İki katmanın sırası, hangisinin yanıtı vereceğini belirler. Oturumsuz **ve** boş başlıklı bir istek iki katmanın da yanıt verme koşulunu sağlar. Aynı istek iki ayrı sırayla ölçülür: önce `oturum`, sonra `dogrulama`; sonra tersi.

```php
<?php
// sira.php — halka sırası hangi katmanın yanıt vereceğini belirler
declare(strict_types=1);
require_once 'katman.php';

$eylem = fn(Istek $i) => new Yanit(201, 'kaydedildi');
$istek = new Istek('/yazilar', ['baslik' => ''], false);     // oturumsuz ve boş başlıklı

foreach (['oturum önce' => [oturumKatmani(), dogrulamaKatmani()],
          'dogrulama önce' => [dogrulamaKatmani(), oturumKatmani()]] as $ad => $ara) {
    $yanit = zincirle($istek, $ara, 'kaydet', $eylem);
    printf("%s: %d %s\n", $ad, $yanit->durum, $yanit->govde);
    foreach (['oturum', 'dogrulama', 'kaydet'] as $parca) {
        printf("  %-10s %s\n", $parca, sinifla($parca));
    }
}
```

```
oturum önce: 302 /giris
  oturum     koştu
  dogrulama  atlandı
  kaydet     atlandı
dogrulama önce: 422 baslik gerekli
  oturum     atlandı
  dogrulama  koştu
  kaydet     atlandı
```

**ÇT17.** İki sırada iki ayrı sonuç çıktı. `oturum` önce olduğunda istek 302 aldı, `oturum` koştu, `dogrulama` ve `kaydet` atlandı. `dogrulama` önce olduğunda istek 422 aldı, `dogrulama` koştu, `oturum` ve `kaydet` atlandı. İstek ve katmanlar aynıdır; yalnız sıra değişti ve durum kodu ile atlanan parça birlikte değişti. Eylem iki sırada da atlandı, çünkü her iki sırada da öndeki katman yanıtı kendisi verdi.

Burada atlanan katmanın hangisi olduğu önemlidir. Sıra `dogrulama` önce olduğunda, oturumsuz bir kullanıcı girdinin ayrıntısını öğrenir: 422 yanıtı, başlığın boş olduğunu söyler. Sıra `oturum` önce olduğunda oturumsuz istek girdiye hiç bakılmadan yönlendirilir. Hangi sıranın doğru olduğu bir güvenlik ve kullanım kararıdır; ölçüm bunu karara bağlamaz, yalnızca sıranın sonucu değiştirdiğini gösterir.

Pratikte bu sıra, hangi katmanın pahalı olduğuna göre de seçilir. Oturum denetimi genellikle ucuzdur ve gerektiğinde isteği daha kapıda eler; doğrulama ise gövdeyi ayrıştırmayı gerektirebilir. Ucuz ve çok eleyen katman öne konursa, atlanan arkadaki katmanlar bu maliyeti hiç ödemez. Bu ölçümde iki katman da ucuzdur ve yalnız hangisinin yanıt verdiği ölçülmüştür; maliyet ayrı bir ölçüm konusudur.

#### Özet

- Ara katman, isteği ve `$sonraki` fonksiyonunu alır ve ya `$sonraki`'yi çağırarak zinciri sürdürür, ya da çağırmadan kendi yanıtını döndürür.
- Zincirde eylem en içtedir; istek dıştan içe, yanıt içten dışa ilerler ve bir katmanın `$sonraki`'den sonraki kısmı dönüşte koşar.
- Oturumsuz istekte `oturum` 302 döndürdü, `dogrulama` ve `kaydet` atlandı; dıştaki `iz` katmanı yine de yanıta başlık ekledi. Oturumlu istekte dört parça da koştu.
- `oturum` önce olduğunda aynı istek 302 aldı, `dogrulama` önce olduğunda 422 aldı; halka sırası hangi katmanın yanıt vereceğini belirler.
- Bu dersin ölçü sınıfı **atlandı**'dır: atlanan parça yoldadır ama önündeki bir katman yanıtı erken vermiştir.

#### Sonraki Adım

Bu derste katmanları ve eylemi `zincirle()` içinde elle kurduk: hangi parçanın hangi nesneye dayandığını `zincirle()` bilmiyordu. Gerçek bir çerçevede katmanlar, denetleyiciler ve onların bağımlılıkları bir nesne kabında durur ve ad verilerek alınır. Sonraki ders, Nesne Kabı ve Cepheler, kabın bir adı iki kez bağlarsa ne olacağını soruyor: ilk kayıt mı, ikinci kayıt mı çözülür ve ezilen parça isteğe bağlanmış sayılır mı?

### Nesne Kabı ve Cepheler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/nesne-kabi-ve-cepheler

Nesne kabında kayıt, tekil kayıt ve otomatik çözümleme, aynı ada ikinci kaydın ilkini sessizce ezmesi ve cephenin her çağrıda nesneyi kaptan çözmesi.

Önceki ders, eylemin önündeki katmanlardan birinin yanıt vermesiyle zincirin geri kalanının atlandığını gösterdi. Katmanlar, denetleyiciler ve onların bağımlılıkları ise zincirin dışında bir yerde kurulur. Çerçevede bu yer **nesne kabıdır** (object container): ad verilerek istenen nesneyi kuran ve ona bir bağımlılık gerekiyorsa onu da kurup veren bir kayıt defteri. Bir ad iki kez kaydedilirse kabın hangi kaydı seçeceği, bu dersin ölçeceği şeydir.

#### Nesne Kabı Üç Yoldan Nesne Kurar

Bu kursta nesne kabı daha önce iki kez göründü; burada üç yolunu da açıyoruz. İlk yol **kayıt**'tır (binding): `bagla()` bir ada bir tarif, yani nesneyi kuran bir fonksiyon ilişkilendirir. İkinci yol **tekil kayıt**'tır (singleton): tarif ilk çözümde bir kez çalışır ve ortaya çıkan nesne saklanır, sonraki bütün `al()` çağrıları aynı nesneyi döndürür. Üçüncü yol **otomatik çözümleme**'dir (autowiring): ad kayıtlı değilse kap, sınıfın yapıcı parametrelerinin tiplerine `ReflectionClass` ile bakar ve her birini yine kaptan alır.

Bağımlılığın neden dışarıdan verildiği M12/K02'deki Bağımlılık Enjeksiyonu dersinde anlatıldı: sınıf bağımlılığını kendisi kurmaz, dışarıdan alır. Orada bağımlılığı elle veren bizdik. Burada onu veren kaptır ve bunun bedeli, kimin neyi kurduğunun bir kayıt defterine taşınmasıdır. Çerçeve kursunun öteki yüzü olan M08/K06'da, bu devredilen denetim denetimin ters çevrilmesi olarak adlandırılır ve orada bir davranışın hangi kaynaktan geldiği aranır. Burada aranan başka bir şeydir: kayıtlı bir parçanın bu istekte gerçekten koşup koşmadığı.

`kap.php` kabı kurar. `GunlukYazici` arayüzünün iki gerçekleştirimi vardır: `DosyaYazici` ve `BellekYazici`. İkisi de `yaz()` çağrıldığında kendi adını koştu olarak günlüğe bildirir. `bagla()` her kaydı `$tarifler` dizisine ada göre yazar; dizi anahtarı ad olduğu için aynı ad varsa önceki kayıt **sessizce ezilir**. `kayitlariYola()` kaptaki kayıtlı parçaları yola koyar: yola, kabın bu istekte elindeki kayıtlardır. `Cephe` ve `Yazici` ise derste sonra kullanılacak: cephe, kaptaki bir nesneye statik bir adla ulaşmanın yoludur.

Üç yolun sırası da `al()` içinde görünür ve sıra önemlidir. Önce saklanan tekil nesneye bakılır, sonra kayıtlı tarife, en son otomatik çözümlemeye. Yani bir ad için tekil nesne varsa kayıt hiç sorulmaz, kayıt varsa sınıfın yapıcısı hiç okunmaz. Otomatik çözümleme bu yüzden varsayılan davranıştır: kaydedilmemiş her sınıf, yapıcı parametrelerinin tipleri çözülebildiği sürece çalışır. Parametre bir arayüzse ya da skaler bir değerse çözümleme durur, çünkü kap o adın hangi gerçekleştirime gideceğini bilemez. Arayüzü bir gerçekleştirime bağlamak kaydın gerçek işidir; `GunlukYazici` arayüzü bu yüzden kayıtsız çözülemez.

```php
<?php
// kap.php — kayıt, tekil kayıt, otomatik çözümleme ve cephe
declare(strict_types=1);
require_once 'kostu.php';

interface GunlukYazici { public function yaz(string $mesaj): void; }

final class DosyaYazici implements GunlukYazici {
    public function yaz(string $mesaj): void { Gunluk::kostu('DosyaYazici'); }
}

final class BellekYazici implements GunlukYazici {
    public function yaz(string $mesaj): void { Gunluk::kostu('BellekYazici'); }
}

final class NesneKabi {
    private array $tarifler = [];
    private array $tekiller = [];
    public function bagla(string $ad, Closure $tarif, bool $tekil = false, string $parca = ''): void {
        $this->tarifler[$ad] = [$tarif, $tekil, $parca];      // aynı ad varsa sessizce ezilir
    }
    public function kayitlariYola(): void {
        foreach ($this->tarifler as [, , $parca]) Gunluk::yola($parca);
    }
    public function al(string $ad): object {
        if (isset($this->tekiller[$ad])) return $this->tekiller[$ad];
        if (isset($this->tarifler[$ad])) {
            [$tarif, $tekil] = $this->tarifler[$ad];
            $nesne = $tarif($this);
            if ($tekil) $this->tekiller[$ad] = $nesne;
            return $nesne;
        }
        $bagimliliklar = [];
        foreach ((new ReflectionClass($ad))->getConstructor()?->getParameters() ?? [] as $p) {
            $bagimliliklar[] = $this->al($p->getType()->getName());
        }
        return new $ad(...$bagimliliklar);
    }
}

abstract class Cephe {
    private static NesneKabi $kap;
    public static function kapla(NesneKabi $kap): void { self::$kap = $kap; }
    abstract protected static function ad(): string;
    public static function __callStatic(string $yontem, array $arguman): mixed {
        return self::$kap->al(static::ad())->$yontem(...$arguman);      // her çağrıda kaptan çözülür
    }
}

final class Yazici extends Cephe {
    protected static function ad(): string { return GunlukYazici::class; }
}
```

Çekirdek `kostu.php` her derste aynı biçimde yeniden verilir.

```php
<?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ı',
    };
}
```

**ÇT18.** Kapta `GunlukYazici` adıyla tek bir kayıt varsa, `al()` o kaydın tarifini çalıştırır ve dönen nesnenin `yaz()` çağrısı koştu olarak görünür. Aynı ada ikinci bir kayıt eklenirse `$tarifler` dizisindeki ilk kayıt silinir; çözümleme ikinci kaydı çalıştırır. İlk kaydın gerçekleştirimi yola hiç girmez, çünkü yola yalnız kapta kalan kayıtlar konur.

#### İkinci Kayıt İlkini Ezer

İki kap kurulur. İlkinde `GunlukYazici` adına önce `DosyaYazici`, sonra `BellekYazici` kaydedilir. İkincisinde yalnız `DosyaYazici` kaydedilir. Her kapta aynı istek yapılır: kayıtlar yola konur, `GunlukYazici` kaptan alınır ve `yaz()` çağrılır.

```php
<?php
// kayit.php — aynı ada ikinci kayıt ilkini sessizce ezer
declare(strict_types=1);
require_once 'kap.php';

function olc(NesneKabi $kap): void {
    Gunluk::yeniIstek();
    $kap->kayitlariYola();
    $kap->al(GunlukYazici::class)->yaz('kaydedildi');
}

$iki = new NesneKabi();
$iki->bagla(GunlukYazici::class, fn() => new DosyaYazici(), parca: 'DosyaYazici');
$iki->bagla(GunlukYazici::class, fn() => new BellekYazici(), parca: 'BellekYazici');

$tek = new NesneKabi();
$tek->bagla(GunlukYazici::class, fn() => new DosyaYazici(), parca: 'DosyaYazici');

foreach (['iki-kayit' => $iki, 'tek-kayit' => $tek] as $ad => $kap) {
    olc($kap);
    foreach (['DosyaYazici', 'BellekYazici'] as $parca) {
        printf("%-10s %-13s %s\n", $ad, $parca, sinifla($parca));
    }
}
```

```
iki-kayit  DosyaYazici   bağlanmadı
iki-kayit  BellekYazici  koştu
tek-kayit  DosyaYazici   koştu
tek-kayit  BellekYazici  bağlanmadı
```

**ÇT19.** İki kayıtlı kapta `BellekYazici` **koştu**, ama `DosyaYazici` **bağlanmadı**: onu kaydeden satır dosyada duruyor ve hiçbir hata ya da uyarı çıkmadı. Tek kayıtlı kapta ise aynı `DosyaYazici` **koştu**. Fark, o kaydın dosyadaki varlığında değil, kapta kalıp kalmadığındadır. Kaydı yazan geliştirici, ilk satırın yürürlükte olduğunu varsayabilir; ama kapta yürürlükte olan, son yazılan kayıttır.

Bu sınıfın neden zor yakalandığı, kaydın doğasından gelir. Kayıtlar genellikle çerçevenin ve uygulamanın başlangıç dosyalarında, birbirinden uzak yerlerde durur: biri çerçevenin kendi kayıtları, biri uygulamanın, biri bir paketin. Aynı ada yazan ikisi arasında hangisinin son olacağı, dosyaların yüklenme sırasına bağlıdır. Her kayıt kendi başına doğru görünür; çakışma yalnız ikisi birlikte yüklendiğinde ortaya çıkar. Ölçümde bu çakışmayı iki ardışık satırla kurduk; gerçek bir uygulamada aralarında yüzlerce satır ve birkaç dosya olabilir.

Ölçümün buradan çıkardığı iş şudur: beklenen gerçekleştirimi ad vererek sormak. Kaydı okumak yetmez, çünkü okunan satır yürürlükte olan satır olmayabilir. Yola konan parçaların listesi, kapta kalan kayıtların listesidir; beklenen parça bu listede yoksa kayıt ezilmiştir ya da hiç yapılmamıştır ve ikisinin dışarıdan görünüşü aynıdır.

Bu dersin ölçü sınıfı **bağlanmadı**'dır. Atlandı'dan farkı şudur: atlanan parça yoldaydı ve önündeki bir katman yanıt verdiği için çağrılmadı; bağlanmayan parça ise yola hiç girmedi, çünkü kaydı kapta yok. Günlükte bir izi olmayan şey, ne bir hata mesajıyla ne bir durum koduyla kendini belli eder. Bu yüzden bağlanmadı, karar sırasının en tehlikeli sınıfıdır ve onu görmek için beklenen parçayı geliştiricinin adlandırmış olması gerekir.

#### Sınırlayıcı Ölçüm

Aynı ada ikinci kaydın ilkini ezmesi her koşulda geçerli midir? Bu ölçüm iki şeyi birleştirir: tekil kayıt ve cephe. `Yazici` cephesi, `Yazici::yaz()` yazıldığında PHP'nin `__callStatic` sihirli yöntemini çalıştırır; bu yöntem kaptan `GunlukYazici` adını çözer ve çağrıyı elde edilen nesneye iletir. Cephe nesneyi saklamaz, her çağrıda kaptan yeniden alır.

Kap önce `DosyaYazici` ile **tekil** olarak kaydedilir. İlk çağrıdan sonra aynı ada `BellekYazici` kaydedilir, ardından cephe yeniden çağrılır.

```php
<?php
// cephe.php — tekil kayıt, yeniden bağlamayı gizler
declare(strict_types=1);
require_once 'kap.php';

$kap = new NesneKabi();
$kap->bagla(GunlukYazici::class, fn() => new DosyaYazici(), tekil: true, parca: 'DosyaYazici');
Cephe::kapla($kap);

foreach (['ilk çağrı', 'yeniden bağladıktan sonra'] as $adim => $ad) {
    if ($adim === 1) {
        $kap->bagla(GunlukYazici::class, fn() => new BellekYazici(), parca: 'BellekYazici');
    }
    Gunluk::yeniIstek();
    $kap->kayitlariYola();
    Yazici::yaz('kaydedildi');
    printf("%s: Dosya %s, Bellek %s\n", $ad, sinifla('DosyaYazici'), sinifla('BellekYazici'));
}
```

```
ilk çağrı: Dosya koştu, Bellek bağlanmadı
yeniden bağladıktan sonra: Dosya koştu, Bellek atlandı
```

**ÇT20.** İlk çağrıda `DosyaYazici` koştu ve tekil kayıt gereği nesne saklandı. İkinci kayıt `$tarifler` içindeki tarifi gerçekten ezdi: `BellekYazici` yola girdi. Ama `al()`, tarife bakmadan önce saklanan tekil nesneye bakar ve onu bulduğu için eski nesneyi döndürdü. Sonuçta ikinci çağrıda `DosyaYazici` yine koştu ve yeniden bağlanan `BellekYazici` yolda olduğu hâlde **atlandı**. Önceki bölümdeki sonuç, yani ikinci kaydın kazanması, tekil kayıt henüz çözülmediyse geçerlidir; bir kez çözülmüşse saklanan nesne kazanır.

Bu sınırın bir sonucu daha var: tekil kayıtla bir test ya da bir isteğin ortasındaki yeniden bağlama, beklenen etkiyi göstermeyebilir. Yeniden bağlamadan sonra saklanan nesneyi de unutturmak gerekir; aksi hâlde yeni kayıt yürürlüğe girmiş görünür ama çözümleme hâlâ eskisini verir. Bu derste kullanılan kap bunu yapmaz ve bu bilerek böyle bırakılmıştır, çünkü sınırın görünmesi için kapta bir saklama noktası olması gerekir.

Bu ölçüm cephenin kendi başına bir sorun yaratmadığını da gösterir. Cephe her çağrıda kaptan sorar; yanlış nesneye giden şey cephe değil, kabın verdiği yanıttır. Cephe, bu karmaşayı statik bir ad arkasına gizlediği için sorunu görmeyi güçleştirir: `Yazici::yaz()` satırı hangi gerçekleştirimin koşacağını söylemez.

#### Özet

- Nesne kabı kayıt, tekil kayıt ve otomatik çözümleme olarak üç yoldan nesne kurar; cephe kaptaki bir nesneye statik bir adla ulaşmanın yoludur.
- Aynı ada ikinci kayıt ilkini hata ya da uyarı olmadan ezer; iki kayıtlı kapta `DosyaYazici` bağlanmadı, `BellekYazici` koştu.
- Tek kayıtlı kapta aynı `DosyaYazici` koştu; fark kaydın dosyada olması değil, kapta kalmasıdır.
- Tekil kayıt çözüldükten sonra yeniden bağlama saklanan nesneyi değiştirmez: `DosyaYazici` koştu, `BellekYazici` yoldaydı ama atlandı.
- Bu dersin ölçü sınıfı **bağlanmadı**'dır: kapta kalmayan kayıt yola girmez ve günlükte hiçbir iz bırakmaz.

#### Sonraki Adım

Altı derstir çerçevenin parçalarını tek tek, elle kurduk: yaşam döngüsü, rota, denetleyici, ara katman, kap. Sıradaki konu, Görünüm ve Veri, bunların ürettiği yanıtın gövdesine geçer ve ilk dersi, Şablon Motoru, şu soruyu soruyor: şablon her seferinde hangi adımı yeniden koşturuyor ve o adımın sonucu saklanıyorsa bir sonraki istek ne görür?

## Görünüm ve Veri

Şablon motoru, form ve doğrulama, nesne ve tablo bağlaması, ilişkiler, göçler ve tohumlama, sayfalama ve kaynak dönüşümü — her biri bir kaydın, bir kuralın ya da bir alanın bu istekte yer alıp almadığını belirleyen bir düzenek olarak.

### Şablon Motoru

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/sablon-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.

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
<?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
<?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
<?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
<?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 &amp; Jerry</li>
<li>&lt;b&gt;kalın&lt;/b&gt;</li>
<li>&quot;tırnak&quot;</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
<?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>&lt;b&gt;kalın&lt;/b&gt;</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 istekte `koştu`.
- Ham çıktı sözdiziminde kaçışlama `bağlanmadı`: sınıf `atlandı` 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.

### Form ve Doğrulama

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/form-ve-dogrulama

Kural dizgeleriyle çalışan bir doğrulayıcı kurulur; geçersiz girdide kayıt adımının atlandığı, geçerli girdide koştuğu ve ilk hatada durmanın sonraki kuralı atladığı ölçülür.

Şablon önüne konan her değeri basar; değerin yazılması gereken biçimde olup olmadığına bakmaz. O kontrol formun eyleminde yapılır: kullanıcıdan gelen girdi kurala uymazsa kayıt adımına ulaşmamalıdır. Peki ulaşmadığını nereden bilirsiniz? Bu derste soru, kuralın tutmadığı istekte kayıt adımının ne olduğudur.

Ölçü ekseni önceki dersteki gibi, beklenen parça bu istekte koştu mu sorusudur. Karar sırası `sinifla()` fonksiyonunun kendisidir: `koştu`, `ertelendi`, `atlandı`, `bağlanmadı`. Bu dersin ağırlıklı sınıfı `atlandı`'dır: kayıt adımı eylemin yoluna konmuştur, ama çağrılmaz. Atlanan adım istemciye bir durum koduyla görünür; burada bu kod 422'dir.

**GV4.** Form girdisi, istek gövdesinden çözülmüş bir dizidir; HTTP katmanı bu derste kurulmaz ve eylem doğrudan bir dizi alır. **GV5.** Kural dizgesi `|` ile ayrılmış kurallardan oluşur ve bir kuralın argümanı `:` ile verilir (`en_az:3`). **GV6.** Geçersiz girdi boş (`''`) ya da kısa (`'ab'`) bir alanla gösterilir; kuralların mantığı özel karakterlerden bağımsızdır. **GV7.** Hata iletileri ASCII yazılır ki sütun genişliği bozulmasın.

Ölçüm düzeneği değişmeden gelir.

```php
<?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ı',
    };
}
```

#### Doğrulayıcıyı Kurmak

Doğrulayıcı (validation) alan başına kural dizgesini ayrıştırır, her kuralı sırayla çalıştırır ve başarısız olanlar için bir ileti toplar. İki ayrıntı ölçüme bağlıdır. Birincisi, bir alanın bütün kuralları kural çalışmadan önce yola konur; bu yüzden çalışmayan kural `atlandı` sınıfına düşebilir. İkincisi, kurucudaki seçenek ilk hatada durmayı belirler: durursa aynı alanın sonraki kuralları çağrılmaz.

Form eylemi doğrulayıcıyı çağırır. Girdi geçerli değilse 422 döner, hata iletileri ve eski girdi yanıta konur; eski girdi, formun yeniden gösterilirken alanların boşalmamasını sağlar. Geçerliyse kayıt adımı koşar ve 201 döner. Kayıt adımı eylemin yoluna eylem başlarken konur, çağrısı da yalnızca doğrulama geçtikten sonra yapılır.

```php
<?php
// dogrulama.php — kural dizgeleriyle doğrulayıcı ve yazı kaydeden eylem
declare(strict_types=1);
require_once 'kostu.php';

final class Dogrulayici {
    public array $hatalar = [];

    public function __construct(private bool $ilkHatadaDur = true) {}

    public function dogrula(array $girdi, array $kurallar): bool {
        $this->hatalar = [];
        foreach ($kurallar as $alan => $dizge) {
            $liste = explode('|', $dizge);
            foreach ($liste as $kural) {
                Gunluk::yola("kural:$alan:" . explode(':', $kural)[0]);
            }
            foreach ($liste as $kural) {
                if ($this->ilkHatadaDur && isset($this->hatalar[$alan])) break;
                [$ad, $arguman] = array_pad(explode(':', $kural, 2), 2, null);
                Gunluk::kostu("kural:$alan:$ad");
                $deger = (string) ($girdi[$alan] ?? '');
                $gecti = match ($ad) {
                    'zorunlu' => $deger !== '',
                    'en_az' => mb_strlen($deger) >= (int) $arguman,
                };
                if (!$gecti) {
                    $this->hatalar[$alan][] = match ($ad) {
                        'zorunlu' => "$alan zorunludur",
                        'en_az' => "$alan en az $arguman karakter olmalidir",
                    };
                }
            }
        }
        return $this->hatalar === [];
    }
}

function yaziKaydet(array $girdi, Dogrulayici $dogrulayici, array $kurallar): array {
    Gunluk::yola('kayit');
    if (!$dogrulayici->dogrula($girdi, $kurallar)) {
        return [422, ['hatalar' => $dogrulayici->hatalar, 'eski' => $girdi]];
    }
    Gunluk::kostu('kayit');
    return [201, ['kaydedilen' => $girdi['baslik']]];
}

function yanitYaz(array $yanit): void {
    [$durum, $govde] = $yanit;
    echo "durum $durum\n";
    foreach ($govde['hatalar'] ?? [] as $alan => $iletiler) {
        foreach ($iletiler as $ileti) echo "  hata $alan: $ileti\n";
    }
    foreach ($govde['eski'] ?? [] as $alan => $deger) echo "  eski $alan='$deger'\n";
    if (isset($govde['kaydedilen'])) echo "  kaydedilen {$govde['kaydedilen']}\n";
}
```

#### Geçersiz Girdide Kayıt Adımı

Ölçüm boş bir başlıkla ve iki harfli bir özetle eylemi çağırır. Kurallar: başlık zorunlu, özet en az üç karakter. Beklenen: durum 422, iki hata iletisi, eski girdi yanıtta, kayıt adımı `atlandı`. Kural parçaları ise ikisi de `koştu`, çünkü ikisi de çalıştı ve ikisi de tutmadı.

```php
<?php
// gecersiz.php — geçersiz girdide kayıt adımı atlanır
require_once 'dogrulama.php';

$kurallar = ['baslik' => 'zorunlu', 'ozet' => 'en_az:3'];
Gunluk::yeniIstek();
yanitYaz(yaziKaydet(['baslik' => '', 'ozet' => 'ab'], new Dogrulayici(), $kurallar));
foreach (['kural:baslik:zorunlu', 'kural:ozet:en_az', 'kayit'] as $parca) {
    printf("  %-22s %s\n", $parca, sinifla($parca));
}
```

```
durum 422
  hata baslik: baslik zorunludur
  hata ozet: ozet en az 3 karakter olmalidir
  eski baslik=''
  eski ozet='ab'
  kural:baslik:zorunlu   koştu
  kural:ozet:en_az       koştu
  kayit                  atlandı
```

İki hata iletisi gittiği hâlde kayıt adımı `atlandı`: eylemin yoluna konmuştu, çağrılmadı. Bu atlama sessiz değildir; istemci 422 alır ve hangi alanın neden tutmadığını görür. Kural `koştu` ile `tutmadı` farklı şeylerdir: ölçü eksenimiz kuralın çalışıp çalışmadığıdır, geçip geçmediği değil. Hata iletisi kuralın çalışmasının sonucudur.

#### Karşıt Ölçüm: Geçerli Girdi

Aynı eylem geçerli bir girdiyle çağrılır. Beklenen: durum 201, hata yok, kayıt adımı `koştu`.

```php
<?php
// gecerli.php — geçerli girdide kayıt adımı koşar
require_once 'dogrulama.php';

$kurallar = ['baslik' => 'zorunlu', 'ozet' => 'en_az:3'];
Gunluk::yeniIstek();
yanitYaz(yaziKaydet(['baslik' => 'Ilk yazi', 'ozet' => 'abc'], new Dogrulayici(), $kurallar));
foreach (['kural:baslik:zorunlu', 'kural:ozet:en_az', 'kayit'] as $parca) {
    printf("  %-22s %s\n", $parca, sinifla($parca));
}
```

```
durum 201
  kaydedilen Ilk yazi
  kural:baslik:zorunlu   koştu
  kural:ozet:en_az       koştu
  kayit                  koştu
```

Doğrulamanın kendisi iki istekte de aynı biçimde koştu; yalnızca ardındaki adımın kaderi değişti. Kayıt adımı doğrulamadan bağımsız bir kapı değildir, doğrulamanın sonucuna bağlı bir dallanmadır.

#### Sınırlayıcı Ölçüm

Doğrulayıcı kurucusundaki seçenek, aynı girdide kullanıcıya kaç ileti gideceğini belirler. Özet alanı iki kurala sahip olsun (`zorunlu|en_az:3`) ve girdi boş olsun. İlk hatada durmayan doğrulayıcıda iki kural da çalışır ve iki ileti toplanır. İlk hatada duran doğrulayıcıda `zorunlu` tutmaz, `en_az` çağrılmaz ve tek ileti toplanır.

```php
<?php
// durma.php — ilk hatada durmak sonraki kuralı atlatır
require_once 'dogrulama.php';

$kurallar = ['baslik' => 'zorunlu', 'ozet' => 'zorunlu|en_az:3'];
foreach ([true, false] as $dur) {
    Gunluk::yeniIstek();
    echo $dur ? "ilk hatada durur\n" : "durmaz\n";
    yanitYaz(yaziKaydet(['baslik' => 'Ilk yazi', 'ozet' => ''], new Dogrulayici($dur), $kurallar));
    foreach (['kural:ozet:zorunlu', 'kural:ozet:en_az', 'kayit'] as $parca) {
        printf("  %-22s %s\n", $parca, sinifla($parca));
    }
}
```

```
ilk hatada durur
durum 422
  hata ozet: ozet zorunludur
  eski baslik='Ilk yazi'
  eski ozet=''
  kural:ozet:zorunlu     koştu
  kural:ozet:en_az       atlandı
  kayit                  atlandı
durmaz
durum 422
  hata ozet: ozet zorunludur
  hata ozet: ozet en az 3 karakter olmalidir
  eski baslik='Ilk yazi'
  eski ozet=''
  kural:ozet:zorunlu     koştu
  kural:ozet:en_az       koştu
  kayit                  atlandı
```

Duran doğrulayıcıda `en_az` kuralı `atlandı`, durmayanda `koştu`; yanıtın durumu iki durumda da 422 ve kayıt iki durumda da `atlandı`. Kayıt adımının kaderini kuralların tutup tutmaması belirler, ileti sayısını ise duruş seçimi. Boş bir alana ayrıca "en az üç karakter" demek kullanıcıya yeni bilgi vermez; bu yüzden ilk hatada durmak sık seçilir. Ama bir alanın bütün sorunlarını tek turda göstermek isteyen form, ikinci seçeneği kurar. Seçim bir tasarım kararıdır ve yanıtta görünür.

#### Özet

- Doğrulayıcı kural dizgelerini çözer, hata iletilerini toplar ve eski girdiyi forma geri döndürür.
- Bu dersin ölçü sınıfı `atlandı`'dır: geçersiz girdide kayıt adımı eylemin yoluna konmuş olduğu hâlde çağrılmadı; istemci 422 ve iletiler aldı.
- Karşıt ölçümde geçerli girdi kayıt adımını `koştu` sınıfına getirdi ve durum 201 oldu.
- Kuralın çalışması (`koştu`) kuralın geçmesinden ayrıdır; tutmayan kural da koşmuştur.
- İlk hatada durmak aynı alandaki sonraki kuralı `atlandı` yapar ve kullanıcıya giden ileti sayısını azaltır; kayıt adımının kaderi değişmez.

#### Sonraki Adım

Geçerli girdi kayıt adımına ulaştı. Kayıt bir modelin içinden geçtiğinde, kaydedilirken başka neler çalışır? Bir sonraki ders nesne ve tablo bağlamasını ele alır ve model kaydedilirken hangi parçanın koştuğunu ölçer.

### Nesne ve Tablo Bağlaması

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/nesne-ve-tablo-baglamasi

Tabloyu PHP dizisi olarak tutan küçük bir model sınıfı kurulur; kaydetme kancasının kayıtta koştuğu, izin listesindeki olmayan alanın aşırı atamada bağlanmadığı ve toplu güncellemede kancanın bağlanmadığı ölçülür.

Form doğrulaması geçti ve kayıt adımı koştu. Peki kayıt bir model üzerinden yapıldığında, kaydedilirken yalnızca satır mı yazılır? Modelin kendi kancaları, erişimcileri ve alan kuralları da çalışır; hangilerinin çalıştığını görmek bu dersin konusudur.

Nesne–tablo bağlaması (object–relational mapping) bir sınıfı bir tabloya bağlayan düzenektir: sınıfın bir örneği tablonun bir satırıdır. Bu bağlamanın kaynakta ne doğurduğu M08/K06'daki aynı adlı derste ölçülmüştü; bu derste soru farklıdır: model kaydedilirken hangi parça bu istekte koştu. Karar sırası `sinifla()` fonksiyonunun kendisidir ve bu dersin ağırlıklı sınıfı `koştu`'dur; ölçümler `bağlanmadı` ile karşılaştırılır.

**GV8.** Tablo bir PHP dizisidir, yani veri katmanı bir dizi satırdan ibarettir; gerçek bir veri tabanı bağlantısı yoktur. **GV9.** Alan adları ASCII'dir ve `id` kayıt sırasında tablonun boyundan türetilir. **GV10.** Aşırı atama (mass assignment) karşıtı için `okunma` adlı nötr bir sayaç alanı kullanılır; alan yetki ya da rol taşımaz. **GV11.** Slug üretimi yalnızca ASCII harf ve rakamları korur.

Ölçüm düzeneği değişmeden gelir.

```php
<?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ı',
    };
}
```

#### Modeli Kurmak

Model tabanı üç şey bilir: hangi tabloya yazdığını, hangi alanların toplu doldurulabildiğini ve kaydetme sırasında hangi kancayı çağıracağını. Yaşam döngüsü kancası (lifecycle hook) bir olayın öncesinde ya da sonrasında çağrılan yöntemdir; burada `kaydediliyor` kaydın tabloya yazılmasından hemen önce çağrılır. Erişimci (accessor) bir alanı okurken değeri dönüştüren yöntemdir; burada `okuOzetKisa`, `ozetKisa` adıyla okunur. Sorgu kapsamı (query scope) sık kullanılan bir süzgeci adlandırır; `yayinda()` yalnızca yayınlanmış satırları döndürür.

`doldur()` yalnızca izin listesindeki alanları alır. İzin listesinde olmayan bir alan için ne bir hata ne bir günlük kaydı vardır: alan, parçanın yoluna hiç konmaz. Bu dersin karşıt ölçümü bu sessizliğe dayanır.

```php
<?php
// model.php — tablo dizisi, kapsam, erişimci ve kancalı küçük bir model
declare(strict_types=1);
require_once 'kostu.php';

final class Veri {
    public static array $tablolar = ['yazilar' => []];
    public static function sifirla(): void { self::$tablolar = ['yazilar' => []]; }
}

abstract class Model {
    protected static string $tablo;
    protected static array $doldurulabilir = [];
    public array $ozellikler = [];

    public static function olustur(array $girdi): static {
        return (new static())->doldur($girdi)->kaydet();
    }

    public function doldur(array $girdi): static {
        foreach ($girdi as $alan => $deger) {
            if (in_array($alan, static::$doldurulabilir, true)) {
                Gunluk::yola("alan:$alan");
                Gunluk::kostu("alan:$alan");
                $this->ozellikler[$alan] = $deger;
            }
        }
        return $this;
    }

    public function kaydet(): static {
        Gunluk::yola('kanca:kaydediliyor');
        $this->kaydediliyor();
        $this->ozellikler['id'] = count(Veri::$tablolar[static::$tablo]) + 1;
        Veri::$tablolar[static::$tablo][] = $this->ozellikler;
        return $this;
    }

    protected function kaydediliyor(): void {}

    public static function toplu(array $degerler, array $kosul): int {
        $say = 0;
        foreach (Veri::$tablolar[static::$tablo] as &$satir) {
            if (array_intersect_assoc($kosul, $satir) === $kosul) {
                $satir = $degerler + $satir;
                $say++;
            }
        }
        return $say;
    }

    public function __get(string $ad): mixed {
        $erisimci = 'oku' . ucfirst($ad);
        return method_exists($this, $erisimci) ? $this->$erisimci() : ($this->ozellikler[$ad] ?? null);
    }
}

final class Yazi extends Model {
    protected static string $tablo = 'yazilar';
    protected static array $doldurulabilir = ['baslik', 'ozet', 'yayinda'];

    protected function kaydediliyor(): void {
        Gunluk::kostu('kanca:kaydediliyor');
        $baslik = strtolower((string) $this->ozellikler['baslik']);
        $this->ozellikler['slug'] = trim(preg_replace('/[^a-z0-9]+/', '-', $baslik), '-');
    }

    protected function okuOzetKisa(): string {
        return mb_substr((string) $this->ozellikler['ozet'], 0, 5) . '...';
    }

    public static function yayinda(): array {
        return array_values(array_filter(Veri::$tablolar['yazilar'], fn($s) => $s['yayinda'] ?? false));
    }
}

function raporla(array $parcalar): void {
    foreach ($parcalar as $parca) printf("  %-20s %s\n", $parca, sinifla($parca));
}
```

#### Kaydetme Kancasının Koştuğunu Görmek

Ölçüm iki yazı oluşturur: biri yayında, öteki değil. Beklenen: kaydetme kancası `koştu`, başlıktan `ilk-yazi` slug'ı üretildi, kapsam yalnızca yayındaki satırı döndürür, erişimci özeti kısaltır. İzin listesindeki alanların üçü de `koştu`.

```php
<?php
// gozlem.php — kaydetme kancası, kapsam ve erişimci
require_once 'model.php';

Veri::sifirla();
Gunluk::yeniIstek();
$yazi = Yazi::olustur(['baslik' => 'Ilk Yazi', 'ozet' => 'Kisa ozet metni', 'yayinda' => true]);
Yazi::olustur(['baslik' => 'Taslak', 'ozet' => 'Henuz hazir degil', 'yayinda' => false]);
echo "slug={$yazi->slug}\n";
echo "ozetKisa={$yazi->ozetKisa}\n";
echo "yayinda=" . count(Yazi::yayinda()) . " / toplam=" . count(Veri::$tablolar['yazilar']) . "\n";
raporla(['alan:baslik', 'alan:ozet', 'alan:yayinda', 'kanca:kaydediliyor']);
```

```
slug=ilk-yazi
ozetKisa=Kisa ...
yayinda=1 / toplam=2
  alan:baslik          koştu
  alan:ozet            koştu
  alan:yayinda         koştu
  kanca:kaydediliyor   koştu
```

Kanca, kaydı yazan çağrının içinde kendiliğinden koştu; çağıranın bir şey yapması gerekmedi, slug kendiliğinden üretildi. Bu, kancanın yararı ve aynı zamanda kırılganlığıdır: davranış çağıranın göremediği bir yerde durur. Kapsam ve erişimci ayrı birer koşu kaydı bırakmadı, çünkü bunlar çağrılarak çalışan yöntemlerdir ve ölçülen şey bir adımın isteğe katılıp katılmadığıdır.

#### Karşıt Ölçüm: İzin Listesinde Olmayan Alan

Aşırı atama, bir isteğin gövdesindeki alanların topluca modele aktarılmasıdır; izin listesi bu aktarımın sınırıdır. Ölçüm isteğe izin listesinde olmayan `okunma` alanını ekler. Beklenen: `okunma` alanı `bağlanmadı`, çünkü hiç yola konmadı; değer kayda girmez; hata yoktur.

```php
<?php
// asiri.php — izin listesinde olmayan alan aşırı atamada bağlanmaz
require_once 'model.php';

Veri::sifirla();
Gunluk::yeniIstek();
$yazi = Yazi::olustur(['baslik' => 'Ikinci', 'ozet' => 'Metin', 'yayinda' => true, 'okunma' => 999]);
echo "okunma kayitta " . (array_key_exists('okunma', Veri::$tablolar['yazilar'][0]) ? 'var' : 'yok') . "\n";
raporla(['alan:baslik', 'alan:okunma']);
```

```
okunma kayitta yok
  alan:baslik          koştu
  alan:okunma          bağlanmadı
```

`okunma` istekte vardı, kayıtta yok. Sınıf `atlandı` değil `bağlanmadı`: bir parça yola konup çağrılmazsa `atlandı` olurdu; burada izin listesi alanı yola koyma adımının dışında bıraktı. Hata verilmemesi, aşırı atamada sessizliğin hem koruyucu hem de aldatıcı olabileceği anlamına gelir. Alan neden girmedi sorusunun cevabı çıktıda değil izin listesindedir.

#### Sınırlayıcı Ölçüm

Kanca kayıt yolunda çalışır. Peki kayıt yolunu atlayan bir güncelleme yolu varsa? `toplu()` doğrudan tablo satırlarını değiştirir, `kaydet()` çağrılmaz. Ölçüm önce bir yazı oluşturur, sonra başlığını `toplu()` ile değiştirir. Beklenen: kanca `bağlanmadı` ve slug eski başlıkta kalır.

```php
<?php
// toplu.php — toplu güncelleme kaydetme kancasını çalıştırmaz
require_once 'model.php';

Veri::sifirla();
Yazi::olustur(['baslik' => 'Ilk Yazi', 'ozet' => 'Metin', 'yayinda' => true]);
Gunluk::yeniIstek();
$say = Yazi::toplu(['baslik' => 'Yeni Baslik'], ['id' => 1]);
$satir = Veri::$tablolar['yazilar'][0];
echo "guncellenen=$say\n";
echo "baslik={$satir['baslik']} slug={$satir['slug']}\n";
raporla(['kanca:kaydediliyor']);
```

```
guncellenen=1
baslik=Yeni Baslik slug=ilk-yazi
  kanca:kaydediliyor   bağlanmadı
```

Çıktıda kanca `bağlanmadı`: başlık değişti, slug değişmedi. Slug başlığa bağlı bir türetilmiş değerdir ve kanca atlandığında tutarsızlaşır. Bağlanmayan kanca hata vermez, bu yüzden yanıtta da görünmez. Bu sınıf tehlikelidir: davranışın beklendiği yerde hiçbir iz yoktur. Çözüm, değişikliğin kaydet yolundan geçirilmesidir ya da toplu güncellemenin türetilmiş alanı kendisinin yazmasıdır; hangisinin seçildiği çağıranın kararıdır.

#### Özet

- Model, bir sınıfı bir tabloya bağlar; kapsam, erişimci ve kanca bu bağlamanın üstüne eklenir.
- Bu dersin ölçü sınıfı `koştu`'dur: kaydetme kancası kayıt yolunda kendiliğinden koştu ve başlıktan slug üretti.
- İzin listesinde olmayan alan aşırı atamada `bağlanmadı`: değer kayda girmedi ve hata da vermedi.
- Kayıt yolunu atlayan toplu güncellemede kanca `bağlanmadı` ve türetilmiş alan eski kaldı.
- Sessizlik koruma da olabilir aldatıcı da; neden kayda girmediği çıktıda değil izin listesindedir.

#### Sonraki Adım

Model tek tabloyu ele aldı. Bir yazının yazarı başka tabloda durduğunda, o ilişki sorgusu ne zaman çalışır? Bir sonraki ders ilişkileri ve tembel yüklemeyi ölçer; ilişki sorgusunun ertelendiği sınıfı ele alır.

### İlişkiler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/iliskiler

Bire bir, bire çok ve çoktan çoğa ilişkiler küçük bir modelde kurulur; tembel yüklemede ilişki sorgusunun erişime kadar ertelendiği, istekli yüklemede listeyle birlikte koştuğu ve hiç erişilmeyen ilişkinin ertelenmiş kaldığı ölçülür.

Model kaydedilirken hangi parçanın koştuğunu gördük. Bir yazının yazarı ise başka bir tabloda durur ve ona ulaşan sorgunun ne zaman çalıştığı modelin yazılışından okunmaz. Soru şudur: yazılar listelendiğinde yazarlarını getiren sorgu o anda mı çalışır, yoksa yazara ilk erişildiğinde mi?

Bu dersin ölçü ekseni, ilişki sorgusunun bu istekte hangi koşulla koştuğudur. Karar sırası `sinifla()` fonksiyonunun kendisidir: `koştu`, `ertelendi`, `atlandı`, `bağlanmadı`. Ağırlıklı sınıf `ertelendi`'dir: parça isteğin dışına bırakılmıştır, erişilene kadar çalışmaz. Sorgu sayısı da basılacak, ama bu M10/K03'ten ayrı bir şeydir: orada koşum sayısı ölçülmüştü; burada sayı sınıfı belirlemez. Sınıf, ilişkinin neden ve hangi koşulda çalıştığını söyleyen `sinifla()` çıktısından gelir; sayı yalnızca yanına konan kanıttır.

**GV12.** Tablolar PHP dizileridir ve her `Depo::sec()` çağrısı bir sorgu sayılır; sorgu sayacı veri katmanının kendisinde tutulur. **GV13.** Tembel yüklemede `Gunluk::ertele()` kaydı, nesne oluşturulurken yazılır; yani ilişki, nesne var olduğu andan itibaren ertelenmiş sayılır. **GV14.** On yazı üç yazar arasında dağıtılmıştır ve sorgu önbelleği yoktur. **GV15.** İstekli yüklemede ilişki sorgusu, listenin sorgusunun hemen ardından tek sorguyla çalışır.

Ölçüm düzeneği değişmeden gelir.

```php
<?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ı',
    };
}
```

#### İlişkileri Kurmak

İlişki, bir modelden başka bir modele giden yöntemdir. Bire bir ilişkide bir yazarın tek bir profili vardır. Bire çok ilişkide bir yazarın birçok yazısı vardır ve her yazının tek bir yazarı vardır. Çoktan çoğa ilişkide bir yazının birçok etiketi, bir etiketin birçok yazısı vardır ve ilişki bir ara tabloda (pivot) tutulur. Tembel yükleme (lazy loading) ilişkiyi ilk erişimde sorgular; istekli yükleme (eager loading) ilişkiyi liste sorgusuyla birlikte, tek ek sorguyla getirir.

Aşağıdaki dosya üç ilişki türünü ve iki yükleme biçimini kurar. `Yazi` nesnesi, yazarı önceden yüklenmemişse oluşurken `iliski:yazar` kaydını ertelenmiş olarak yazar; yazara erişildiğinde sorgu çalışır ve kayıt `koştu` olur. Önceden yüklendiyse liste yükleyicisi kaydı hemen `koştu` olarak yazar.

```php
<?php
// iliski.php — ilişki yöntemleri, tembel ve istekli yükleme
declare(strict_types=1);
require_once 'kostu.php';

final class Depo {
    public static int $sorgu = 0;
    public static array $tablolar = [
        'yazarlar'    => [['id' => 1, 'ad' => 'Ayse'], ['id' => 2, 'ad' => 'Mert'], ['id' => 3, 'ad' => 'Deniz']],
        'profiller'   => [['yazar_id' => 1, 'site' => 'ayse.test'], ['yazar_id' => 2, 'site' => 'mert.test']],
        'etiketler'   => [['id' => 1, 'ad' => 'php'], ['id' => 2, 'ad' => 'web']],
        'yazi_etiket' => [['yazi_id' => 1, 'etiket_id' => 1], ['yazi_id' => 1, 'etiket_id' => 2], ['yazi_id' => 2, 'etiket_id' => 1]],
        'yazilar'     => [],
    ];

    public static function sec(string $tablo, callable $kosul): array {
        self::$sorgu++;
        return array_values(array_filter(self::$tablolar[$tablo], $kosul));
    }
}
Depo::$tablolar['yazilar'] = array_map(
    fn($i) => ['id' => $i, 'baslik' => "Yazi $i", 'yazar_id' => ($i % 3) + 1],
    range(1, 10)
);

final class Yazar {
    public function __construct(public array $satir) {}
    public function profil(): ?array {      // bire bir
        return Depo::sec('profiller', fn($p) => $p['yazar_id'] === $this->satir['id'])[0] ?? null;
    }
    public function yazilar(): array {      // bire çok
        return Depo::sec('yazilar', fn($y) => $y['yazar_id'] === $this->satir['id']);
    }
}

final class Yazi {
    private array $yuklu = [];

    public function __construct(public array $satir, ?array $yazar = null) {
        if ($yazar === null) Gunluk::ertele('iliski:yazar');
        else $this->yuklu['yazar'] = $yazar;
    }

    public function yazar(): array {        // yazının tek yazarı
        if (!isset($this->yuklu['yazar'])) {
            Gunluk::kostu('iliski:yazar');
            $this->yuklu['yazar'] = Depo::sec('yazarlar', fn($y) => $y['id'] === $this->satir['yazar_id'])[0];
        }
        return $this->yuklu['yazar'];
    }

    public function etiketler(): array {    // çoktan çoğa, ara tablo üzerinden
        $idler = array_column(Depo::sec('yazi_etiket', fn($r) => $r['yazi_id'] === $this->satir['id']), 'etiket_id');
        return Depo::sec('etiketler', fn($e) => in_array($e['id'], $idler, true));
    }

    public static function liste(array $yukle = []): array {
        $satirlar = Depo::sec('yazilar', fn() => true);
        $yazarlar = null;
        if (in_array('yazar', $yukle, true)) {
            Gunluk::kostu('iliski:yazar');
            $idler = array_unique(array_column($satirlar, 'yazar_id'));
            $yazarlar = array_column(Depo::sec('yazarlar', fn($y) => in_array($y['id'], $idler, true)), null, 'id');
        }
        return array_map(fn($s) => new self($s, $yazarlar[$s['yazar_id']] ?? null), $satirlar);
    }
}

function yeniIstek(): void { Gunluk::yeniIstek(); Depo::$sorgu = 0; }
function raporla(array $parcalar): void {
    foreach ($parcalar as $parca) printf("  %-16s %s\n", $parca, sinifla($parca));
    echo "  sorgu=" . Depo::$sorgu . "\n";
}
```

Önce üç ilişki türünün sonuçlarına bakalım; sınıf yok, yalnızca ilişkilerin çalıştığının kanıtı.

```php
<?php
// turler.php — üç ilişki türü
require_once 'iliski.php';

$yazarlar = array_map(fn($s) => new Yazar($s), Depo::$tablolar['yazarlar']);
echo "profil: " . $yazarlar[0]->profil()['site'] . "\n";
echo "yazar 1 yazi sayisi: " . count($yazarlar[0]->yazilar()) . "\n";
$yazi = Yazi::liste()[0];
echo "yazi 1 etiketleri: " . implode(',', array_column($yazi->etiketler(), 'ad')) . "\n";
```

```
profil: ayse.test
yazar 1 yazi sayisi: 3
yazi 1 etiketleri: php,web
```

#### Tembel Yükleme

Ölçüm on yazıyı listeler ve yazarlarına henüz bakmaz. Beklenen: liste sorgusu çalışmış, ilişki `ertelendi` ve sorgu sayısı 1. Sonra döngü her yazının yazarına erişir. Beklenen: ilişki `koştu` ve sorgu sayısı 1 + 10.

```php
<?php
// tembel.php — tembel yükleme ve N+1 sorunu
require_once 'iliski.php';

yeniIstek();
$yazilar = Yazi::liste();
echo "listelendi: " . count($yazilar) . " yazi\n";
raporla(['iliski:yazar']);
foreach ($yazilar as $yazi) $yazi->yazar();
echo "yazarlara erisildi\n";
raporla(['iliski:yazar']);
```

```
listelendi: 10 yazi
  iliski:yazar     ertelendi
  sorgu=1
yazarlara erisildi
  iliski:yazar     koştu
  sorgu=11
```

Liste çalıştığında ilişki sorgusu henüz yazılmamıştı: `ertelendi`, sorgu sayısı bir. Döngü her yazıda ayrı bir yazar sorgusu çalıştırdı; N+1 sorunu (N+1 problem) budur: bir liste sorgusu ve listedeki her satır için bir sorgu daha. Aynı yazar birden çok kez sorgulanır, çünkü önbellek yoktur. Sayı sorunu gösterir; sınıf ise sorunun nedenini: ilişki erişime kadar ertelenmişti ve erişim bir döngünün içindeydi.

#### Karşıt Ölçüm: İstekli Yükleme

Aynı liste, yazar ilişkisi istenerek yüklenir. Beklenen: ilişki listeyle birlikte `koştu`, erişimden önce bile ve sorgu sayısı 2.

```php
<?php
// istekli.php — istekli yüklemede ilişki listeyle birlikte koşar
require_once 'iliski.php';

yeniIstek();
$yazilar = Yazi::liste(['yazar']);
echo "listelendi: " . count($yazilar) . " yazi\n";
raporla(['iliski:yazar']);
foreach ($yazilar as $yazi) $yazi->yazar();
echo "yazarlara erisildi\n";
raporla(['iliski:yazar']);
```

```
listelendi: 10 yazi
  iliski:yazar     koştu
  sorgu=2
yazarlara erisildi
  iliski:yazar     koştu
  sorgu=2
```

İlişki, hiçbir yazının yazarına bakılmadan `koştu`; döngü sonrası sorgu sayısı değişmedi. Bedeli, yazarlara hiç bakılmasa da sorgunun çalışmasıdır. Tembel yükleme ile istekli yükleme arasındaki seçim, ilişkiye erişileceğinin bilinip bilinmemesine bağlıdır.

#### Sınırlayıcı Ölçüm

Erişilmeyen bir ilişki ertelenmiş kalır mı yoksa bağlanmaz mı? Cevap, kaydın nerede yazıldığına bağlıdır. Burada `Gunluk::ertele()` nesne oluşturulurken yazılır, yani ilişki var olan nesnenin parçası sayılır. Hiçbir yazının yazarına erişilmeyen istekte `iliski:yazar` yine `ertelendi` kalır. Aynı istekte, adı hiçbir yere yazılmayan bir ilişki, örneğin `iliski:profil`, hiçbir kayıt bırakmaz ve `bağlanmadı` sınıfına düşer.

```php
<?php
// erisilmeyen.php — erişilmeyen ilişki ertelenmiş kalır, kaydı olmayan bağlanmaz
require_once 'iliski.php';

yeniIstek();
$yazilar = Yazi::liste();
echo "yazarlara hic erisilmedi\n";
raporla(['iliski:yazar', 'iliski:profil']);
```

```
yazarlara hic erisilmedi
  iliski:yazar     ertelendi
  iliski:profil    bağlanmadı
  sorgu=1
```

`iliski:yazar` `ertelendi`, `iliski:profil` `bağlanmadı`. İkisi de hiç çalışmadı, ama aralarındaki fark büyüktür: ertelenen, ilişkinin bir erişimle çalışabileceğini bilir; bağlanmayanın böyle bir yolu hiç tanımlanmamıştır. Kaydı nesne oluşturulurken değil erişim sırasında yazan bir tasarımda `iliski:yazar` de bu istekte `bağlanmadı` olurdu. Yani sınıf, ilişkinin kendisinin değil, kayıt noktasının seçimine bağlı bir bilgidir; bu seçim dersin başında açıkça verilmiştir.

#### Özet

- Bire bir, bire çok ve çoktan çoğa ilişkiler birer model yöntemidir; çoktan çoğa ilişki ara tabloyu kullanır.
- Bu dersin ölçü sınıfı `ertelendi`'dir: tembel yüklemede ilişki sorgusu, liste sorgusundan sonra bile erişilene kadar çalışmadı.
- Döngüde erişim N+1 sorununu doğurdu; sorgu sayısı sorunu gösterdi, sınıf ise ertelemenin nedenini.
- İstekli yüklemede ilişki listeyle birlikte `koştu`; bedeli, erişilmese de sorgunun çalışmasıdır.
- Hiç erişilmeyen ilişki `ertelendi` kalır; kaydı olmayan ilişki ise `bağlanmadı` olur.

#### Sonraki Adım

İlişkiler tablolara dayanır ve tablolar bir yerden gelir. Şemayı kuran göç ikinci kez çalıştırıldığında ne olur? Bir sonraki ders göçleri ve tohumlamayı ölçer ve ikinci çalışmada göçün `atlandı` sınıfına düştüğünü gösterir.

### Göçler ve Tohumlama

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/gocler-ve-tohumlama

Şemayı sürümleyen göçler ve başlangıç verisini yazan tohumlayıcı kurulur; kayıtlı göçün ikinci çalışmada atlandığı, yeni göçün koştuğu ve çalışmış bir göç dosyasının düzenlenmesinin bağlanmadığı ölçülür.

İlişkiler tablolara dayanır ve tabloların bir yerden gelmesi gerekir. Şema elle kurulursa her ortamda farklı çıkar; göçler (migration) şemayı adım adım ve tekrarlanabilir biçimde kurmak için vardır. Peki aynı göç iki kez çalıştırılırsa tablo iki kez mi kurulur?

Bu dersin ölçü ekseni, göç dosyasının bu çalıştırmada koştu mu sorusudur. Karar sırası `sinifla()` fonksiyonunun kendisidir: `koştu`, `ertelendi`, `atlandı`, `bağlanmadı`. Ağırlıklı sınıf `atlandı`'dır: göç dosyası bulunmuş, yola konmuş, ama kayıtta olduğu için çalıştırılmamıştır.

**GV16.** Şema, çalışma dizinindeki `sema.json` dosyasında tablo adlarından kolon listelerine uzanan bir dizi olarak tutulur; gerçek bir veri tabanı yoktur. **GV17.** Göç kaydı `goc_kaydi.json` dosyasında çalışmış göçlerin adlarını sırayla tutar. **GV18.** Göç dosyaları `goclar/` altında adlarına göre sıralanarak çalışır ve her biri `yukari` ile `asagi` fonksiyonlarını döndürür. **GV19.** Tohumlayıcı yalnızca tablo boşken veri yazar; başlangıç verisi `veri.json` dosyasındadır.

Ölçüm düzeneği değişmeden gelir.

```php
<?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ı',
    };
}
```

#### Göç Çalıştırıcısı

Göç, şemayı bir sürümden ötekine taşıyan dosyadır. `yukari` fonksiyonu değişikliği uygular, `asagi` fonksiyonu geri alır. Çalıştırıcı (migrator) göç dosyalarını sırayla gezer, kayıtta olanları çalıştırmaz, olmayanları çalıştırıp kayda ekler. Geri alma (rollback) kayda eklenen son göçün `asagi` fonksiyonunu çağırır ve kaydı siler. Tohumlama (seeding) şemanın üstüne başlangıç verisini yazar. Göç dosyasındaki kolon ekleme `kolonEkle()` üzerinden geçer ve her ekleme kendini `kolon:<tablo>.<kolon>` adıyla kaydeder.

```php
<?php
// goc.php — göç çalıştırıcı, göç kaydı ve tohumlayıcı
declare(strict_types=1);
require_once 'kostu.php';

function jsonOku(string $dosya): array { return is_file($dosya) ? json_decode(file_get_contents($dosya), true) : []; }
function jsonYaz(string $dosya, array $veri): void { file_put_contents($dosya, json_encode($veri)); }

function kolonEkle(array $sema, string $tablo, string $kolon): array {
    Gunluk::kostu("kolon:$tablo.$kolon");
    $sema[$tablo][] = $kolon;
    return $sema;
}
function kolonSil(array $sema, string $tablo, string $kolon): array {
    $sema[$tablo] = array_values(array_diff($sema[$tablo], [$kolon]));
    return $sema;
}

final class Gocucu {
    public function yukari(): void {
        $kayit = jsonOku('goc_kaydi.json');
        $sema = jsonOku('sema.json');
        $dosyalar = glob('goclar/*.php');
        sort($dosyalar);
        foreach ($dosyalar as $dosya) {
            $ad = basename($dosya, '.php');
            Gunluk::yola("goc:$ad");
            if (in_array($ad, $kayit, true)) continue;
            Gunluk::kostu("goc:$ad");
            $goc = require $dosya;
            $sema = $goc['yukari']($sema);
            $kayit[] = $ad;
        }
        jsonYaz('sema.json', $sema);
        jsonYaz('goc_kaydi.json', $kayit);
    }

    public function geriAl(): void {
        $kayit = jsonOku('goc_kaydi.json');
        $sema = jsonOku('sema.json');
        $ad = array_pop($kayit);
        $goc = require "goclar/$ad.php";
        Gunluk::kostu("asagi:$ad");
        jsonYaz('sema.json', $goc['asagi']($sema));
        jsonYaz('goc_kaydi.json', $kayit);
    }
}

final class Tohumlayici {
    public function calistir(): void {
        Gunluk::yola('tohum:yazilar');
        $veri = jsonOku('veri.json');
        if (!empty($veri['yazilar'])) return;
        Gunluk::kostu('tohum:yazilar');
        $veri['yazilar'] = [['id' => 1, 'baslik' => 'Ilk Yazi'], ['id' => 2, 'baslik' => 'Ikinci Yazi']];
        jsonYaz('veri.json', $veri);
    }
}

function sifirla(): void {
    foreach (['sema.json', 'goc_kaydi.json', 'veri.json'] as $dosya) if (is_file($dosya)) unlink($dosya);
    if (!is_dir('goclar')) mkdir('goclar');
    foreach (glob('goclar/*.php') as $dosya) unlink($dosya);
}
function raporla(array $parcalar): void {
    foreach ($parcalar as $parca) printf("  %-24s %s\n", $parca, sinifla($parca));
}
```

Üç göç dosyası, bir yazar betiğiyle yazılır. İlki `yazilar` tablosunu kurar, ikincisi `slug` kolonunu ekler, üçüncüsü `etiketler` tablosunu kurar; üçüncüsü karşıt ölçümde sonradan eklenir.

```php
<?php
// gocler.php — örnek göç dosyalarını yazar
function gocYaz(string $ad): void {
    $kaynak = match ($ad) {
        '001_yazilar' => <<<'GOC'
<?php
return [
    'yukari' => function (array $sema): array {
        $sema['yazilar'] = [];
        return kolonEkle(kolonEkle($sema, 'yazilar', 'id'), 'yazilar', 'baslik');
    },
    'asagi' => fn(array $sema): array => array_diff_key($sema, ['yazilar' => 1]),
];
GOC,
        '002_slug' => <<<'GOC'
<?php
return [
    'yukari' => fn(array $sema): array => kolonEkle($sema, 'yazilar', 'slug'),
    'asagi' => fn(array $sema): array => kolonSil($sema, 'yazilar', 'slug'),
];
GOC,
        '003_etiketler' => <<<'GOC'
<?php
return [
    'yukari' => function (array $sema): array {
        $sema['etiketler'] = [];
        return kolonEkle(kolonEkle($sema, 'etiketler', 'id'), 'etiketler', 'ad');
    },
    'asagi' => fn(array $sema): array => array_diff_key($sema, ['etiketler' => 1]),
];
GOC,
    };
    file_put_contents("goclar/$ad.php", $kaynak);
}
```

#### Kayıtlı Göçün Atlanması

Ölçüm iki göçü iki kez çalıştırır ve her çalıştırmadan sonra tohumlayıcıyı da çağırır. Beklenen: ilk çalıştırmada iki göç ve tohumlama `koştu`, ikincisinde üçü de `atlandı`; şema ikinci çalıştırmada değişmez.

```php
<?php
// gozlem.php — ikinci çalıştırmada kayıtlı göç atlanır
require_once 'goc.php';
require_once 'gocler.php';

sifirla();
gocYaz('001_yazilar');
gocYaz('002_slug');
foreach ([1, 2] as $n) {
    Gunluk::yeniIstek();
    (new Gocucu())->yukari();
    (new Tohumlayici())->calistir();
    echo "calistirma $n\n";
    raporla(['goc:001_yazilar', 'goc:002_slug', 'tohum:yazilar']);
}
echo "sema: " . json_encode(jsonOku('sema.json')) . "\n";
```

```
calistirma 1
  goc:001_yazilar          koştu
  goc:002_slug             koştu
  tohum:yazilar            koştu
calistirma 2
  goc:001_yazilar          atlandı
  goc:002_slug             atlandı
  tohum:yazilar            atlandı
sema: {"yazilar":["id","baslik","slug"]}
```

İkinci çalıştırmada göç dosyaları bulundu, yola konuldu, kayıtta oldukları için çağrılmadı: `atlandı`. Bu hata değil, göçün yinelemeye dayanıklı olmasını sağlayan mekanizmadır. Tohumlayıcı da aynı biçimde tablo dolu olduğu için atlandı; aksi hâlde iki yazı dört yazıya çıkardı.

#### Karşıt Ölçüm: Yeni Göç ve Geri Alma

Üç göç çalışmış bir şemaya dördüncü göç eklenir. Beklenen: yeni göç `koştu`, öncekiler `atlandı`. Sonra son göç geri alınır ve tekrar çalıştırılır; geri alınan göç kayıttan çıktığı için yeniden `koştu` olmalıdır.

```php
<?php
// yenigoc.php — yeni göç koşar, geri alınan göç yeniden koşar
require_once 'goc.php';
require_once 'gocler.php';

sifirla();
gocYaz('001_yazilar');
gocYaz('002_slug');
(new Gocucu())->yukari();

gocYaz('003_etiketler');
Gunluk::yeniIstek();
(new Gocucu())->yukari();
echo "yeni goc eklendi\n";
raporla(['goc:001_yazilar', 'goc:002_slug', 'goc:003_etiketler']);

Gunluk::yeniIstek();
(new Gocucu())->geriAl();
echo "geri alindi: " . json_encode(array_keys(jsonOku('sema.json'))) . "\n";
raporla(['asagi:003_etiketler']);

Gunluk::yeniIstek();
(new Gocucu())->yukari();
echo "yeniden calisti: " . json_encode(array_keys(jsonOku('sema.json'))) . "\n";
raporla(['goc:002_slug', 'goc:003_etiketler']);
```

```
yeni goc eklendi
  goc:001_yazilar          atlandı
  goc:002_slug             atlandı
  goc:003_etiketler        koştu
geri alindi: ["yazilar"]
  asagi:003_etiketler      koştu
yeniden calisti: ["yazilar","etiketler"]
  goc:002_slug             atlandı
  goc:003_etiketler        koştu
```

Göç çalıştırıcısı yeni dosyayı kendisi buldu; kimse onu adıyla çağırmadı. Geri alma kayıt ve şemayı birlikte geri çevirdi, bu yüzden göç yeniden `koştu`. Atlanan ile koşan arasındaki ayrım dosyada değil kayıttadır.

#### Sınırlayıcı Ölçüm

Bir göç çalıştıktan sonra dosyası düzenlenirse çalıştırıcı bunu fark eder mi? Ölçüm `002_slug` dosyasını yukarıdaki göç çalışmış hâlde iken değiştirir ve ikinci bir kolon, `ozet`, ekler. Çalıştırıcı yeniden çalıştırılır. Beklenen: göç `atlandı`, çünkü kayıtta; düzenleme ise hiçbir koşu kaydı bırakmadığı için `bağlanmadı`. Şemada `ozet` olmaz. Düzeltme yolu, değişikliği yeni bir göç dosyasıyla yapmaktır; ölçümün sonunda bu denenir.

```php
<?php
// duzenleme.php — çalışmış göç dosyasının düzenlenmesi bağlanmaz
require_once 'goc.php';
require_once 'gocler.php';

sifirla();
gocYaz('001_yazilar');
gocYaz('002_slug');
(new Gocucu())->yukari();

file_put_contents('goclar/002_slug.php', <<<'GOC'
<?php
return [
    'yukari' => fn(array $sema): array => kolonEkle(kolonEkle($sema, 'yazilar', 'slug'), 'yazilar', 'ozet'),
    'asagi' => fn(array $sema): array => kolonSil(kolonSil($sema, 'yazilar', 'slug'), 'yazilar', 'ozet'),
];
GOC);
Gunluk::yeniIstek();
(new Gocucu())->yukari();
echo "002 duzenlendi\n";
raporla(['goc:002_slug', 'kolon:yazilar.ozet']);
echo "sema: " . json_encode(jsonOku('sema.json')) . "\n";

file_put_contents('goclar/004_ozet.php', <<<'GOC'
<?php
return [
    'yukari' => fn(array $sema): array => kolonEkle($sema, 'yazilar', 'ozet'),
    'asagi' => fn(array $sema): array => kolonSil($sema, 'yazilar', 'ozet'),
];
GOC);
Gunluk::yeniIstek();
(new Gocucu())->yukari();
echo "004 eklendi\n";
raporla(['goc:002_slug', 'goc:004_ozet', 'kolon:yazilar.ozet']);
echo "sema: " . json_encode(jsonOku('sema.json')) . "\n";
```

```
002 duzenlendi
  goc:002_slug             atlandı
  kolon:yazilar.ozet       bağlanmadı
sema: {"yazilar":["id","baslik","slug"]}
004 eklendi
  goc:002_slug             atlandı
  goc:004_ozet             koştu
  kolon:yazilar.ozet       koştu
sema: {"yazilar":["id","baslik","slug","ozet"]}
```

Düzenlenmiş dosya `atlandı`: bulundu, ama kayıtta olduğu için çağrılmadı. Dosyaya yazılan `ozet` kolonu ise `bağlanmadı`; ne yola konuldu ne çağrıldı ve şemada yok. Hata verilmemesi bu sınıfın tehlikesidir: geliştirici kolonu eklediğini sanır, ortam farklı bir şemada kalır. Yeni bir göç dosyası aynı kolonu `koştu` yaptı ve şemaya ulaştı. Bu yüzden çalışmış bir göç değiştirilmez; şema değişikliği her zaman yeni göçle yapılır.

#### Özet

- Göç, şemayı sürümler; çalıştırıcı kayıtta olmayan göçleri çalıştırır ve kaydı günceller.
- Bu dersin ölçü sınıfı `atlandı`'dır: kayıtlı göç ikinci çalıştırmada bulundu ve yola konuldu, ama çağrılmadı; şema değişmedi.
- Yeni eklenen göç `koştu`; geri alınan göç kayıttan çıktığı için yeniden `koştu`.
- Çalışmış göç dosyasındaki düzenleme `bağlanmadı`: dosya atlandı, eklenen kolon şemaya ulaşmadı ve hata çıkmadı.
- Tohumlayıcı da tablo doluyken `atlandı`; şema değişikliği her zaman yeni göçle yapılır.

#### Sonraki Adım

Şema ve veri hazır. Modele yeni bir kolon eklendiğinde bu kolon API yanıtına kendiliğinden ulaşır mı? Bir sonraki ders sayfalamayı ve kaynak dönüşümünü ölçer; modelde olan ama yanıt tanımında olmayan bir kolonun `bağlanmadı` olduğunu gösterir.

### Sayfalama ve Kaynak Dönüşümü

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/sayfalama-ve-kaynak-donusumu

Atlama tabanlı sayfalama ve modelden yanıt dizisine kaynak dönüşümü kurulur; modele eklenen yeni kolonun dönüşüm tanımında olmadığı için yanıta bağlanmadığı, tanıma eklenince koştuğu ve sayfa sınırı dışındaki isteğin boş liste döndürdüğü ölçülür.

Göç yeni kolonu şemaya ekledi. Peki o kolon API yanıtına kendiliğinden ulaşır mı? Model kolonu bilir, yanıt ise başka bir yerde, bir dönüşüm tanımında kurulur. İkisi arasındaki bağ kendiliğinden kurulmuyorsa, kolon eklendiği hâlde yanıtta hiç görünmeyebilir.

Bu dersin ölçü ekseni, bir kolonun ya da adımın bu yanıtta koştu mu sorusudur. Karar sırası `sinifla()` fonksiyonunun kendisidir: `koştu`, `ertelendi`, `atlandı`, `bağlanmadı`. Ağırlıklı sınıf `bağlanmadı`'dır ve çekirdek derste olduğu gibi en tehlikelisidir: atlanan adım bir durum koduyla, ertelenen adım işçide görünür; bağlanmayan adım hiçbir yerde görünmez ve yanıt 200 döner.

**GV20.** Tablo, yirmi beş satırlık bir PHP dizisidir ve satırlar `id`, `baslik`, `ozet` ile sonradan eklenmiş `okuma_suresi` kolonunu taşır; `okuma_suresi` nötr bir sayıdır. **GV21.** Sayfalama atlama tabanlıdır (offset): sayfa numarası bir'den başlar ve atlanan satır sayısı `(sayfa - 1) * boy` olur. **GV22.** Yanıt durumu, eylemin küçük bir dilimiyle `[durum, gövde]` çifti olarak döner; HTTP katmanı kurulmaz. **GV23.** Kaynak dönüşümü, tanımdaki her alanı sırayla modelden alır.

Ölçüm düzeneği değişmeden gelir.

```php
<?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ı',
    };
}
```

#### Sayfalama ve Kaynak

Sayfalama (pagination) uzun bir listeyi sayfalara böler. Atlama tabanlı sayfalama sayfa numarası ve sayfa boyunu alır, öncesindeki satırları atlar ve sayfa boyu kadarını döndürür; toplam satır sayısı ve son sayfa numarası yanıta konur ki istemci kaç sayfa olduğunu bilsin. Kaynak dönüşümü (resource transformation) modelin satırını yanıt dizisine çeviren katmandır. Koleksiyon kaynağı bu dönüşümü bir sayfadaki her satıra uygular ve sayfa bilgisini üst veri (`meta`) olarak ekler.

Dönüşüm tanımı, hangi alanların yanıta gireceğini adlandırır. Modelde olup tanımda olmayan alan ne yanıta girer ne bir hata üretir. Bu, kaynak dönüşümünün görevi gereğidir: yanıtın biçimi model değiştiğinde kendiliğinden değişmez, yani dönüşüm modelin değişmesinden yanıtı koruyan bir sözleşmedir. Aynı özellik, yeni kolonu unutmaya da imkân verir.

```php
<?php
// sayfalama.php — atlama tabanlı sayfalama ve kaynak dönüşümü
declare(strict_types=1);
require_once 'kostu.php';

function yazilar(): array {
    return array_map(
        fn($i) => ['id' => $i, 'baslik' => "Yazi $i", 'ozet' => "Ozet $i", 'okuma_suresi' => ($i % 5) + 1],
        range(1, 25)
    );
}

function sayfala(array $satirlar, int $sayfa, int $boy): array {
    Gunluk::kostu('sayfalama');
    $toplam = count($satirlar);
    return [
        'satirlar' => array_slice($satirlar, ($sayfa - 1) * $boy, $boy),
        'sayfa' => $sayfa, 'boy' => $boy, 'toplam' => $toplam,
        'son' => (int) ceil($toplam / $boy),
    ];
}

final class Kaynak {
    public function __construct(private array $alanlar) {}

    public function donustur(array $satir): array {
        $cikti = [];
        foreach ($this->alanlar as $alan) {
            Gunluk::yola("alan:$alan");
            Gunluk::kostu("alan:$alan");
            $cikti[$alan] = $satir[$alan];
        }
        return $cikti;
    }

    public function koleksiyon(array $sayfa): array {
        return [
            'veri' => array_map(fn($s) => $this->donustur($s), $sayfa['satirlar']),
            'meta' => ['sayfa' => $sayfa['sayfa'], 'boy' => $sayfa['boy'], 'toplam' => $sayfa['toplam'], 'son' => $sayfa['son']],
        ];
    }
}

function yazilarEylemi(Kaynak $kaynak, int $sayfa, int $boy = 10): array {
    return [200, $kaynak->koleksiyon(sayfala(yazilar(), $sayfa, $boy))];
}

function yanitYaz(array $yanit): void {
    [$durum, $govde] = $yanit;
    $m = $govde['meta'];
    echo "durum $durum\n";
    echo "  sayfa={$m['sayfa']} boy={$m['boy']} toplam={$m['toplam']} son={$m['son']}\n";
    echo "  kayit=" . count($govde['veri']) . "\n";
    if ($govde['veri'] !== []) echo "  anahtarlar=" . implode(',', array_keys($govde['veri'][0])) . "\n";
}
function raporla(array $parcalar): void {
    foreach ($parcalar as $parca) printf("  %-18s %s\n", $parca, sinifla($parca));
}
```

#### Yeni Kolon Yanıta Ulaşıyor mu

Tablo `okuma_suresi` kolonunu taşıyor, ama dönüşüm tanımı yalnızca `id`, `baslik` ve `ozet` alanlarını adlandırıyor. Ölçüm ikinci sayfayı ister. Beklenen: durum 200, on kayıt, toplam yirmi beş, son sayfa üç; sayfalama `koştu`, `baslik` `koştu`, `okuma_suresi` `bağlanmadı`.

```php
<?php
// yenikolon.php — tanımda olmayan kolon yanıta bağlanmaz
require_once 'sayfalama.php';

Gunluk::yeniIstek();
yanitYaz(yazilarEylemi(new Kaynak(['id', 'baslik', 'ozet']), 2));
raporla(['sayfalama', 'alan:baslik', 'alan:okuma_suresi']);
```

```
durum 200
  sayfa=2 boy=10 toplam=25 son=3
  kayit=10
  anahtarlar=id,baslik,ozet
  sayfalama          koştu
  alan:baslik        koştu
  alan:okuma_suresi  bağlanmadı
```

Yanıt 200, sayfa bilgisi doğru, anahtarlar üç. `okuma_suresi` tabloda her satırda var, ama yanıtta yok ve bunun hiçbir izi yok: ne uyarı ne hata. Dönüşüm yeni kolonu bilmediği için kolon ne yola konuldu ne çağrıldı: `bağlanmadı`. İstemci, kolonun eklendiğini bilmeden eski biçimde yanıt almaya devam eder; kolonu ekleyen geliştirici ise testten yalnızca eski alanların bulunduğunu görür ve geçer.

#### Karşıt Ölçüm: Tanıma Eklemek

Aynı istek, tanıma `okuma_suresi` eklenmiş bir kaynakla yapılır. Beklenen: alan `koştu` ve anahtar sayısı dört.

```php
<?php
// tanimli.php — tanıma eklenen kolon yanıta ulaşır
require_once 'sayfalama.php';

Gunluk::yeniIstek();
yanitYaz(yazilarEylemi(new Kaynak(['id', 'baslik', 'ozet', 'okuma_suresi']), 2));
raporla(['sayfalama', 'alan:baslik', 'alan:okuma_suresi']);
```

```
durum 200
  sayfa=2 boy=10 toplam=25 son=3
  kayit=10
  anahtarlar=id,baslik,ozet,okuma_suresi
  sayfalama          koştu
  alan:baslik        koştu
  alan:okuma_suresi  koştu
```

Aynı sayfa, aynı sayfa bilgisi; fark yalnızca tanımdadır. Kolon tanıma eklenince `koştu`. Yanıtın biçimi böylece bilerek değişir: tanımdaki bir satır, yanıt sözleşmesinin bir değişikliğidir ve inceleme sırasında görünür. Model değişikliğinin kendiliğinden yanıtı değiştirmemesi korumadır; maliyeti, bu tanımın elle sürdürülmesidir.

#### Sınırlayıcı Ölçüm

Sayfa sınırı dışındaki istek boş liste mi döndürür, hata mı? Tablo yirmi beş satır, sayfa boyu on; son sayfa üç. Ölçüm karşılaştırma için üçüncü sayfayı, ardından dördüncü ve doksan dokuzuncu sayfayı ister. Sayfalama `array_slice` ile yapıldığı için sınır dışı sayfa özel bir kontrol olmadan çalışır.

```php
<?php
// sinirdisi.php — sayfa sınırı dışındaki istek
require_once 'sayfalama.php';

foreach ([3, 4, 99] as $sayfa) {
    Gunluk::yeniIstek();
    echo "istenen sayfa $sayfa\n";
    yanitYaz(yazilarEylemi(new Kaynak(['id', 'baslik', 'ozet']), $sayfa));
    raporla(['sayfalama', 'alan:baslik']);
}
```

```
istenen sayfa 3
durum 200
  sayfa=3 boy=10 toplam=25 son=3
  kayit=5
  anahtarlar=id,baslik,ozet
  sayfalama          koştu
  alan:baslik        koştu
istenen sayfa 4
durum 200
  sayfa=4 boy=10 toplam=25 son=3
  kayit=0
  sayfalama          koştu
  alan:baslik        bağlanmadı
istenen sayfa 99
durum 200
  sayfa=99 boy=10 toplam=25 son=3
  kayit=0
  sayfalama          koştu
  alan:baslik        bağlanmadı
```

Çıktıda sınır dışı sayfa hata değildir: durum 200 ve kayıt sayısı sıfır. Üst veri istenen sayfa numarasını olduğu gibi geri verir, son sayfa üç kalır. Dikkat çeken, sınır dışı sayfada `alan:baslik` kaydıdır: satır olmadığı için dönüşüm hiç çalışmadı ve alan `bağlanmadı` sınıfına düştü. Aynı alan üçüncü sayfada `koştu`. Yani "bu alan yanıtta koştu mu" sorusunun cevabı, yanıtta satır olup olmadığına bağlıdır. İstemci boş bir listeyi "bu sayfada kayıt yok" diye okuyabilir, ama hata ile ayırt edemez; hata istenirse sayfa numarasını `son` ile karşılaştıran açık bir kontrol gerekir.

#### Özet

- Atlama tabanlı sayfalama sayfa, sayfa boyu, toplam ve son sayfa bilgisini üretir; kaynak dönüşümü modeli yanıt dizisine çevirir.
- Bu dersin ölçü sınıfı `bağlanmadı`'dır: modele eklenen kolon dönüşüm tanımında olmadığı için yanıta ulaşmadı, yanıt 200 döndü ve hiçbir hata görünmedi.
- Tanıma eklenen kolon `koştu`; yanıt biçiminin değişmesi böylece görünür bir karar olur.
- Kaynak dönüşümü, model değişikliğinden yanıtı koruyan bir sözleşmedir; yeni kolonun tanıma elle eklenmesi bu korumanın bedelidir.
- Sayfa sınırı dışındaki istek 200 ve boş liste döndürdü; satır olmadığı için dönüşüm hiç çalışmadı.

#### Sonraki Adım

Yanıt hazır, ama bu yanıtı kimin istediğine henüz bakmadık. Yetkisi olmayan bir kullanıcı aynı listeyi isterse ne olur? Sıradaki konu olan uygulama hizmetlerinin ilk dersi, Kimlik Doğrulama ve Yetkilendirme, bu soruyu ele alır.

## Uygulama Servisleri

Kimlik doğrulama ve yetkilendirme, kuyruklar, olaylar, bildirimler, önbellekleme, dosya depolama, zamanlanmış görevler, yerelleştirme ve test — her biri bir işin istekte mi, sonra mı, hiç mi koştuğunu belirleyen bir düzenek olarak.

### Kimlik Doğrulama ve Yetkilendirme

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/kimlik-dogrulama-ve-yetkilendirme

Parolanın özetle saklanması ve doğrulanması, oturumdaki kullanıcı, yetki kapısı ve yetki politikası; meşru bir kullanıcının başkasının yazısını düzenleme isteğinde kapının 403 vermesi ve eylemin atlanması; tanımsız yeteneğin reddedilmesi ilkesi.

Sayfalama ve Kaynak Dönüşümü dersinde yanıta yeni bir alanın bağlanmadığını gördük: model değişmişti, yanıt 200 dönmüştü, ama değişiklik yanıtın içine ulaşmamıştı. Bu ders uygulama servislerinin ilkidir ve soru şudur: **isteği yapan kişi bu işi yapabilir mi ve yapamıyorsa eylem koştu mu?**

İki ayrı soru var ve çerçeveler ikisini ayrı adımlara koyar. **Kimlik doğrulama (authentication)** isteği yapanın kim olduğunu belirler: kullanıcı adı ve parola doğrudur, oturum bir kullanıcıya bağlıdır. **Yetkilendirme (authorization)** kimliği bilinen kullanıcının bu işi yapıp yapamayacağına karar verir. İlki 302 ile `/giris` sayfasına gönderir, ikincisi 403 ile reddeder. Birinin kimliğini doğrulamış olmak, ona her kaydı düzenleme hakkı vermez.

**US1.** Model ürün adı kullanmaz ve parolaları dışarıdan okumaz: parola `password_hash()` ile özetlenir, `password_verify()` ile doğrulanır; özetin kendisi hiçbir çıktıda basılmaz, çünkü her çağrıda farklı bir değer üretir. Oturum, çekirdekteki `Istek` sınıfının `oturum` alanının küçültülmüş bir biçimidir: bu derste alan oturumdaki kullanıcının kimliğini taşır ve `session_*` fonksiyonları çağrılmaz. Parola kırma, oturum çalma, belirteç taklidi ve yetki aşımı modellenmez; koşan kod savunma tarafındadır.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.

```php
<?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ı',
    };
}
```

Bu ders çekirdeğe üç çağrıyla bağlanır: yola koyduğunda `Gunluk::yola()`, çağırdığında `Gunluk::kostu()`; ertelenen bir parça bu derste yoktur. Çerçevenin tamamı yeniden kurulmaz, yalnız bu dersin dilimi yazılır.

#### Parola Özeti

Kimlik doğrulamanın ilk adımı parolanın saklanış biçimidir. Parola olduğu gibi saklanmaz; `password_hash()` ona bir tuz ekleyip tek yönlü bir özet üretir ve `password_verify()` verilen parolayı özetle karşılaştırır. Tuz her çağrıda yenidir, bu yüzden aynı parolanın iki özeti farklıdır ama ikisi de doğrulanır. Aşağıdaki ölçüm özetin kendisini basmaz, yalnız karşılaştırma sonuçlarını basar.

```php
<?php
// parola.php — aynı parolanın iki özeti
$ilk = password_hash('sifre-ayse', PASSWORD_DEFAULT);
$ikinci = password_hash('sifre-ayse', PASSWORD_DEFAULT);
echo 'iki özet eşit mi: ', $ilk === $ikinci ? 'evet' : 'hayır', "\n";
echo 'ilk özet doğrulanır mı: ', password_verify('sifre-ayse', $ilk) ? 'evet' : 'hayır', "\n";
echo 'ikinci özet doğrulanır mı: ', password_verify('sifre-ayse', $ikinci) ? 'evet' : 'hayır', "\n";
echo 'yanlış parola doğrulanır mı: ', password_verify('sifre-burak', $ilk) ? 'evet' : 'hayır', "\n";
```

```
iki özet eşit mi: hayır
ilk özet doğrulanır mı: evet
ikinci özet doğrulanır mı: evet
yanlış parola doğrulanır mı: hayır
```

Özetlerin eşit olmaması, özet karşılaştırmasının `===` ile değil `password_verify()` ile yapılması gerektiğini söyler. `KimlikDeposu::giris()` bu yüzden kullanıcı adını eşitlikle, parolayı doğrulama fonksiyonuyla sınar. Depoda yalnız `parolaOzeti` alanı bulunur; düz parola hiçbir nesnede tutulmaz.

#### Kapı ve Politika

`yetki.php` dilimin tamamıdır ve dört parçadan oluşur. `KimlikDeposu` parolayı özet olarak saklar ve `giris()` ile doğrular. **Yetki kapısı (gate)**, yani `Kapi`, bir yeteneğin adını (`yazi.duzenle`) bir **yetki politikasına (policy)** bağlar; politika kullanıcıyı ve hedef nesneyi alıp doğru ya da yanlış döndüren küçük bir fonksiyondur. `YaziPolitikasi::duzenle()` kuralı tek satırla söyler: yazıyı yalnız sahibi düzenler.

`YaziUygulamasi::duzenle()` üç beklenen parçayı yola koyar: `kimlik` (oturumdaki kullanıcıyı bulma), `kapi` (izin sorusu) ve `duzenle` (başlığı değiştiren eylem). Her adım çağrıldığında `Gunluk::kostu()` ile işaretlenir. Kimlik yoksa istek 302 ile döner, kapı reddederse 403 ile döner ve eylem hiç çağrılmaz.

```php
<?php
// yetki.php — kimlik doğrulama, yetki kapısı ve yetki politikası
declare(strict_types=1);
require_once 'kostu.php';

final class Kullanici {
    public function __construct(public int $id, public string $ad, public string $parolaOzeti) {}
}

final class Yazi {
    public function __construct(public int $id, public int $sahipId, public string $baslik) {}
}

final class Istek {
    public function __construct(public ?int $oturum = null, public array $govde = []) {}
}

final class KimlikDeposu {
    private array $kullanicilar = [];
    public function kaydet(int $id, string $ad, string $parola): void {
        $this->kullanicilar[$id] = new Kullanici($id, $ad, password_hash($parola, PASSWORD_DEFAULT));
    }
    public function giris(string $ad, string $parola): ?Kullanici {
        foreach ($this->kullanicilar as $kullanici) {
            if ($kullanici->ad === $ad && password_verify($parola, $kullanici->parolaOzeti)) return $kullanici;
        }
        return null;
    }
    public function bul(int $id): ?Kullanici { return $this->kullanicilar[$id] ?? null; }
}

final class Kapi {
    private array $politikalar = [];
    public function __construct(private bool $tanimsizGecer = false) {}
    public function tanimla(string $yetenek, Closure $politika): void { $this->politikalar[$yetenek] = $politika; }
    public function izinVerir(string $yetenek, Kullanici $kullanici, object $hedef): bool {
        if (!isset($this->politikalar[$yetenek])) return $this->tanimsizGecer;
        return ($this->politikalar[$yetenek])($kullanici, $hedef);
    }
}

final class YaziPolitikasi {
    public static function duzenle(Kullanici $kullanici, Yazi $yazi): bool {
        return $kullanici->id === $yazi->sahipId;
    }
}

final class YaziUygulamasi {
    public function __construct(private KimlikDeposu $kimlik, private Kapi $kapi, public array $yazilar) {}
    public function duzenle(Istek $istek, int $yaziId, string $yetenek = 'yazi.duzenle'): array {
        Gunluk::yeniIstek();
        foreach (['kimlik', 'kapi', 'duzenle'] as $parca) Gunluk::yola($parca);
        Gunluk::kostu('kimlik');
        $kullanici = $istek->oturum === null ? null : $this->kimlik->bul($istek->oturum);
        if ($kullanici === null) return [302, '/giris'];
        Gunluk::kostu('kapi');
        $yazi = $this->yazilar[$yaziId];
        if (!$this->kapi->izinVerir($yetenek, $kullanici, $yazi)) return [403, 'yetki yok'];
        Gunluk::kostu('duzenle');
        $yazi->baslik = $istek->govde['baslik'];
        return [200, 'guncellendi'];
    }
}
```

```php
<?php
// kapi.php — iki kullanıcı, bir yazı, üç istek
require_once 'yetki.php';

$kimlik = new KimlikDeposu();
$kimlik->kaydet(1, 'ayse', 'sifre-ayse');
$kimlik->kaydet(2, 'burak', 'sifre-burak');
echo 'giriş doğru parola: ', $kimlik->giris('burak', 'sifre-burak')?->ad ?? 'yok', "\n";
echo 'giriş yanlış parola: ', $kimlik->giris('burak', 'sifre-ayse')?->ad ?? 'yok', "\n";

$kapi = new Kapi();
$kapi->tanimla('yazi.duzenle', YaziPolitikasi::duzenle(...));
$uygulama = new YaziUygulamasi($kimlik, $kapi, [10 => new Yazi(10, 1, 'Merhaba')]);

function olc(string $ad, array $yanit): void {
    printf("%s -> %d %s\n", $ad, $yanit[0], $yanit[1]);
    foreach (['kimlik', 'kapi', 'duzenle'] as $parca) printf("  %-8s %s\n", $parca, sinifla($parca));
}

olc('oturumsuz', $uygulama->duzenle(new Istek(null, ['baslik' => 'Yeni']), 10));
olc('burak', $uygulama->duzenle(new Istek(2, ['baslik' => 'Yeni']), 10));
olc('ayse', $uygulama->duzenle(new Istek(1, ['baslik' => 'Yeni']), 10));
echo 'başlık: ', $uygulama->yazilar[10]->baslik, "\n";
```

```
giriş doğru parola: burak
giriş yanlış parola: yok
oturumsuz -> 302 /giris
  kimlik   koştu
  kapi     atlandı
  duzenle  atlandı
burak -> 403 yetki yok
  kimlik   koştu
  kapi     koştu
  duzenle  atlandı
ayse -> 200 guncellendi
  kimlik   koştu
  kapi     koştu
  duzenle  koştu
başlık: Yeni
```

İlk satırlar `password_verify()` sonucudur: doğru parola kullanıcıyı döndürür, başka birinin parolası döndürmez. Asıl gözlem istek satırlarındadır. Oturumsuz istekte `kimlik` koştu, kapı ve eylem yoldaydı ama çağrılmadı: ikisi de **atlandı**, durum kodu 302. İkinci istek bu dersin gözlemidir: Burak oturum açmış, kimliği doğrulanmış, meşru bir kullanıcıdır; Ayşe'nin yazısını düzenlemek ister. `kimlik` ve `kapi` koştu, kapı politikayı çalıştırdı, politika yanlış döndürdü, durum 403 oldu ve `duzenle` **atlandı**. Son satırdaki başlığın `Merhaba` değil `Yeni` olması, değişikliğin yalnız üçüncü istekten geldiğini söyler.

Atlanan parça kendini durum koduyla ele verir. Burak'ın isteğinde 403 görmek, eylemin neden koşmadığını açıklar; hata fırlatılmadı, çünkü ret beklenen bir sonuçtur.

#### Karşıt: Yazının Sahibi

Üçüncü istek aynı kod yolundan geçer ve tek fark kullanıcının yazının sahibi olmasıdır. Politika doğru döndürür, kapı geçirir, `duzenle` **koştu** ve durum 200 olur. Kapıda hiçbir şey değişmedi; değişen, politikaya verilen kullanıcı ile yazının `sahipId` alanının eşleşmesidir. Kapı bir engel değil bir karardır ve karar her istekte yeniden verilir.

Burada ayrımı koymak gerekir. `kimlik` her üç istekte de koştu: kimlik doğrulama başarıldı ya da başarısız olduysa bile adım çağrıldı. Oturumsuz istekte kapının atlanmasıyla Burak'ın isteğinde eylemin atlanması farklı nedenlerdendir, ama sınıf aynıdır: yol vardı, önündeki adım yanıtı erken verdi. Sınıfı sebep değil `sinifla()` belirler.

#### Sınırlayıcı Ölçüm

Kapıya hiç tanımlanmamış bir yetenek sorulursa ne olur? Gerçek uygulamalarda bu bir yazım hatasıyla olur: politika `yazi.duzenle` adına bağlanmış, eylem `yazi.duzenel` diye sorar. Model iki kapı kurar: biri tanımsız yeteneği reddeder (`Kapi` varsayılanı), öteki geçirir (`tanimsizGecer: true`). Aynı yazım hatalı yetenek adı hem yazının sahibi hem sahibi olmayan kullanıcı için sorulur.

```php
<?php
// tanimsiz.php — kapıya tanımlanmamış bir yetenek sorulur
require_once 'yetki.php';

function kur(bool $tanimsizGecer): YaziUygulamasi {
    $kimlik = new KimlikDeposu();
    $kimlik->kaydet(1, 'ayse', 'sifre-ayse');
    $kimlik->kaydet(2, 'burak', 'sifre-burak');
    $kapi = new Kapi($tanimsizGecer);
    $kapi->tanimla('yazi.duzenle', YaziPolitikasi::duzenle(...));
    return new YaziUygulamasi($kimlik, $kapi, [10 => new Yazi(10, 1, 'Merhaba')]);
}

foreach ([false, true] as $gecer) {
    foreach ([[1, 'ayse'], [2, 'burak']] as [$oturum, $ad]) {
        $uygulama = kur($gecer);
        $yanit = $uygulama->duzenle(new Istek($oturum, ['baslik' => 'Yeni']), 10, 'yazi.duzenel');
        printf("%-10s %-6s -> %d  kapi %-8s duzenle %s\n", $gecer ? 'geciriyor' : 'reddediyor', $ad,
            $yanit[0], sinifla('kapi'), sinifla('duzenle'));
    }
}
```

```
reddediyor ayse   -> 403  kapi koştu   duzenle atlandı
reddediyor burak  -> 403  kapi koştu   duzenle atlandı
geciriyor  ayse   -> 200  kapi koştu   duzenle koştu
geciriyor  burak  -> 200  kapi koştu   duzenle koştu
```

Sonuç iki yönlüdür. Reddeden kapıda yazı sahibi bile 403 alır: hata yüksek sesle görünür ve geliştirici ilk denemede ad uyuşmazlığını fark eder. Geçiren kapıda ise Burak'ın isteği 200 ile döner ve eylem koşar; kapı koştu, ama hiçbir politika çalışmadı, yani kapı varlığı ile korumayı aynı şey sanmak yanıltır. **Tanımsız yetenek reddedilmelidir** ilkesi buradan çıkar: bilinmeyen bir soruya "evet" demek, korumanın sessizce kalkması demektir ve yanıtta hiçbir iz bırakmaz.

#### Özet

- Bu dersin ölçü sınıfı **atlandı**: Burak'ın isteğinde `kimlik` ve `kapi` koştu, `duzenle` yoldaydı ama kapı 403 verdi ve eylem çağrılmadı.
- Kimlik doğrulama kim olduğunu, yetkilendirme ne yapabileceğini belirler; ilki 302 ile, ikincisi 403 ile kendini ele verir.
- Karşıtı: yazının sahibinde politika doğru döndürdü, `duzenle` **koştu** ve durum 200 oldu.
- Sınırlayıcı ölçümde tanımsız yetenek reddeden kapıda iki kullanıcı da 403 aldı; geçiren kapıda ikisi de 200 aldı ve eylem koştu. Tanımsız yetenek reddedilmelidir.
- Parola `password_hash()` ile saklanır ve `password_verify()` ile doğrulanır; özet çıktıya yazılmaz.

#### Sonraki Adım

Kapı reddedince eylem atlandı ve yanıt hemen döndü. Peki eylem koşacaksa ve içindeki bir iş yanıtı geciktirecekse, bu işin yanıtı beklemesi şart mı? Kuyruklar ve İşler dersi uzun bir işi isteğin dışına bırakır ve yanıtın onu beklemediğini ölçer.

### Kuyruklar ve İşler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/kuyruklar-ve-isler

İş sınıfı, kuyruğa koyma ve işçi; kuyruğa konan işin yanıt anında ertelenmesi, eşzamanlı sürücüde aynı işin koşması; ilk denemede başarısız olan işin yeniden denenmesi ve deneme hakkı bitince başarısız işler listesine düşmesi.

Kimlik Doğrulama ve Yetkilendirme dersinde kapı eylemi atladı ve yanıt hemen döndü. Kapı geçirdiğinde ise eylem koşar ve eylemin içinde yanıtı geciktirecek bir iş olabilir: bir görselin küçük resmini üretmek, bir raporu hazırlamak. Bu ders şunu sorar: **bu iş yanıtı bekletmeli mi ve beklemiyorsa kim, ne zaman koşturur?**

Önce adlandırma. Bu derste model **aynı süreçtedir**: ayrı bir sunucu, ayrı bir süreç ya da ağ yoktur. "Kuyruk" bir dizidir ve **işçi (worker)** bu diziden işleri alıp çalıştıran `isle()` çağrısının adıdır. İsteğin yanıtı döndükten sonra bu çağrı yapılır. Gerçek bir sistemde işçi ayrı bir süreç olurdu; burada ayrım yalnız yanıtın işi bekleyip beklemediğidir. Benzer bir soruyu JVM, Eşzamanlılık ve Başarım kursunda (M08/K04) görmüştük, ama orada adımı kimin yürüttüğü arandı: hangi iş parçacığının çalıştırdığı ölçüldü. Burada soru o değil; yanıtın işi bekleyip beklemediği.

**US2.** Kuyruk bir dizidir, işçi senkron bir döngüdür. Zaman aşımı, eşzamanlı işçiler, kilitleme ve iş sırasının kalıcılığı modellenmez; kuyruk sürücüsü adı kullanılmaz. Her deneme bir sayaçla izlenir ve `sleep` çağrılmaz: yeniden deneme aralığı gerçek zaman değil, deneme numarasıdır. İş içinde fırlatılan istisna işçide yakalanır ve yanıta ulaşmaz.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.

```php
<?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ı',
    };
}
```

Bu ders çekirdeğin üç çağrısını kullanır. Küçük resim işi yola konur (`yola()`), kuyruğa gönderilirken `ertele()` ile isteğin dışına bırakıldığı işaretlenir, eşzamanlı sürücüde ise `kostu()` ile çağrıldığı işaretlenir.

#### İş Sınıfı, Sürücü ve İşçi

`isler.php` dilimin tamamıdır. `IsSinifi` bir işin sözleşmesidir: bir `ad()`, bir `calistir()` ve kaç kez denendiğini sayan `deneme` alanı. `KucukResimIsi` tek bir yazı kimliği alır ve `calistir()` çağrıldığında kimliği bir diziye ekler; diziye yazılmış olması işin koştuğunun kanıtıdır.

İşin nasıl gönderildiğine **sürücü (driver)** karar verir. `Surucu` arayüzünün tek yöntemi `gonder()` vardır. `EszamanliSurucu` işi kuyruğa koymaz, `Gunluk::kostu()` ile işaretleyip hemen çalıştırır. `BellekSurucusu` ise `Gunluk::ertele()` ile işaretleyip işi bir diziye ekler ve yanıtı döndürür; iş orada bekler. Bekleyen işleri `isle()` çalıştırır: her işi diziden çeker, denemeyi bir artırır, `calistir()` der ve istisnayı yakalarsa deneme hakkı kalmışsa işi dizinin sonuna geri koyar, kalmamışsa `basarisiz` listesine atar.

`YazilarDenetleyicisi::kaydet()` ise sürücüyü bilmez. Yalnız `Surucu` arayüzüne konuşur: bir iş nesnesi üretir, `gonder()` der ve 201 döndürür. Denetleyicinin kodu iki sürücüde de aynıdır; değişen, sürücünün işi ne yaptığıdır.

```php
<?php
// isler.php — iş sınıfı, iki sürücü ve işçi
declare(strict_types=1);
require_once 'kostu.php';

abstract class IsSinifi {
    public int $deneme = 0;
    abstract public function ad(): string;
    abstract public function calistir(): void;
}

final class KucukResimIsi extends IsSinifi {
    public static array $uretilen = [];
    public function __construct(private int $yaziId) {}
    public function ad(): string { return 'kucukResim'; }
    public function calistir(): void { self::$uretilen[] = $this->yaziId; }
}

interface Surucu {
    public function gonder(IsSinifi $is): void;
}

final class EszamanliSurucu implements Surucu {
    public function gonder(IsSinifi $is): void {
        Gunluk::kostu($is->ad());
        $is->calistir();
    }
}

final class BellekSurucusu implements Surucu {
    private array $bekleyen = [];
    public array $basarisiz = [];
    public function __construct(private int $denemeHakki = 3) {}
    public function gonder(IsSinifi $is): void {
        Gunluk::ertele($is->ad());
        $this->bekleyen[] = $is;
    }
    public function bekleyen(): int { return count($this->bekleyen); }
    public function isle(): array {            // işçi: istekten bağımsız koşar
        $gunluk = [];
        while ($is = array_shift($this->bekleyen)) {
            $is->deneme++;
            try {
                $is->calistir();
                $gunluk[] = "{$is->ad()} deneme {$is->deneme}: koştu";
            } catch (Throwable $hata) {
                $gunluk[] = "{$is->ad()} deneme {$is->deneme}: " . get_class($hata);
                if ($is->deneme < $this->denemeHakki) $this->bekleyen[] = $is;
                else $this->basarisiz[] = $is;
            }
        }
        return $gunluk;
    }
}

final class YazilarDenetleyicisi {
    public function __construct(private Surucu $surucu) {}
    public function kaydet(int $yaziId): array {
        Gunluk::yeniIstek();
        Gunluk::yola('kaydet');
        Gunluk::yola('kucukResim');
        Gunluk::kostu('kaydet');
        $this->surucu->gonder(new KucukResimIsi($yaziId));
        return [201, 'kaydedildi'];
    }
}
```

#### Kuyruğa Konan İş

İlk ölçüm iki parça sorar: `kaydet` (denetleyicinin kendi adımı) ve `kucukResim` (kuyruğa konan iş). Sürücü kuyruktur. Yanıt döndükten sonra `isle()` çağrılır ve işçi çalışır. Karşıt ölçüm aynı denetleyiciyi eşzamanlı sürücüyle koşturur.

```php
<?php
// kuyruk.php — aynı iş iki sürücüyle
require_once 'isler.php';

function olc(string $ad, Surucu $surucu): void {
    KucukResimIsi::$uretilen = [];
    $yanit = (new YazilarDenetleyicisi($surucu))->kaydet(7);
    printf("%s -> %d %s\n", $ad, $yanit[0], $yanit[1]);
    foreach (['kaydet', 'kucukResim'] as $parca) printf("  %-11s %s\n", $parca, sinifla($parca));
    echo '  yolda mı: ', in_array('kucukResim', Gunluk::$yol, true) ? 'evet' : 'hayır', "\n";
    echo '  yanıt anında üretilen: ', count(KucukResimIsi::$uretilen), "\n";
}

$kuyruk = new BellekSurucusu();
olc('kuyruk', $kuyruk);
echo '  bekleyen iş: ', $kuyruk->bekleyen(), "\n";
foreach ($kuyruk->isle() as $satir) echo '  işçi: ', $satir, "\n";
echo '  işçiden sonra üretilen: ', count(KucukResimIsi::$uretilen), "\n";

olc('eşzamanlı', new EszamanliSurucu());
```

```
kuyruk -> 201 kaydedildi
  kaydet      koştu
  kucukResim  ertelendi
  yolda mı: evet
  yanıt anında üretilen: 0
  bekleyen iş: 1
  işçi: kucukResim deneme 1: koştu
  işçiden sonra üretilen: 1
eşzamanlı -> 201 kaydedildi
  kaydet      koştu
  kucukResim  koştu
  yolda mı: evet
  yanıt anında üretilen: 1
```

Bu dersin gözlemi ilk blokta. Yanıt 201 olarak döndü, ama `kucukResim` **ertelendi**: yola konmuştu, isteğin içinde çağrılmadı, isteğin dışına bırakıldı. "Yanıt anında üretilen" sayısı 0'dır, yani küçük resim henüz yoktur; bekleyen iş sayısı 1'dir. İşçi `isle()` ile çalıştığında iş **ilk denemede koştu** ve üretilen sayı 1 oldu. Yanıtı alan istemci küçük resmi beklemedi.

Karar sırası burada işe yarar. `kucukResim` yolda olduğu için `yolda mı` satırı `evet` der; bu satır tek başına işin koştuğunu söylemez. `sinifla()` önce `kostu` listesine bakar, orada bulamaz; sonra `ertelenen` listesine bakar ve bulur. "Yolda" olmak ile "koşmak" ayrı gözlemlerdir ve ertelenen parça tam aralarındadır.

#### Karşıt: Eşzamanlı Sürücü

İkinci blok aynı denetleyiciyi `EszamanliSurucu` ile koşturur. Aynı iş, aynı `gonder()` çağrısı; `kucukResim` bu kez **koştu** ve yanıt anında üretilen sayı 1'dir. İstemci yanıtını işin bitmesinden sonra alır. Kod aynıdır, tek değişen sürücüdür. Bu, kuyruğun işi küçültmediğini, yalnız işin koştuğu anı isteğin dışına taşıdığını gösterir.

Ayrımı bir cümleyle koymak gerekirse: M08/K04'te adımı kimin yürüttüğünü aradık; burada adımı kimin yürüttüğü değil, yanıtın onu bekleyip beklemediği önemlidir. Eşzamanlı sürücüde yanıt bekler, kuyrukta beklemez.

#### Sınırlayıcı Ölçüm

Kuyruğun işi ertelemesi bir bedeli de getirir: iş yanıttan sonra koştuğu için hata istemciye ulaşmaz, işçide kalır. Bu yüzden yeniden deneme ve başarısız işler listesi gerekir. Aşağıdaki ölçüm iki iş kurar. Birincisi yalnız ilk denemede istisna fırlatır; ikincisi ilk beş denemede fırlatır. Deneme hakkı 3'tür.

```php
<?php
// deneme.php — ilk denemede başarısız olan iş
require_once 'isler.php';

final class KirilganIs extends IsSinifi {
    public function __construct(private int $basarisizDeneme) {}
    public function ad(): string { return 'kirilgan'; }
    public function calistir(): void {
        if ($this->deneme <= $this->basarisizDeneme) throw new RuntimeException('geçici hata');
    }
}

foreach ([[1, 3], [5, 3]] as [$basarisiz, $hak]) {
    Gunluk::yeniIstek();
    $kuyruk = new BellekSurucusu($hak);
    $kuyruk->gonder(new KirilganIs($basarisiz));
    echo "ilk $basarisiz deneme başarısız, hak $hak\n";
    echo '  istekte: ', sinifla('kirilgan'), "\n";
    foreach ($kuyruk->isle() as $satir) echo '  işçi: ', $satir, "\n";
    echo '  başarısız işler: ', count($kuyruk->basarisiz), "\n";
}
```

```
ilk 1 deneme başarısız, hak 3
  istekte: ertelendi
  işçi: kirilgan deneme 1: RuntimeException
  işçi: kirilgan deneme 2: koştu
  başarısız işler: 0
ilk 5 deneme başarısız, hak 3
  istekte: ertelendi
  işçi: kirilgan deneme 1: RuntimeException
  işçi: kirilgan deneme 2: RuntimeException
  işçi: kirilgan deneme 3: RuntimeException
  başarısız işler: 1
```

İki iş de istekte **ertelendi**: yanıt hiçbir durumda hatayı görmedi. İlk iş birinci denemede `RuntimeException` fırlattı, işçi onu yakaladı, hakkı bitmediği için dizinin sonuna geri koydu ve ikinci denemede iş **koştu**. Başarısız işler listesi boş kaldı. İkinci iş üç denemede de başarısız oldu. Üçüncü denemeden sonra hak kalmadığı için geri konmadı ve `basarisiz` listesine düştü: listede 1 iş vardır. Beşinci deneme hiç yapılmadı.

Sayılar şunu söyler: yeniden deneme geçici hatalar içindir ve hata sürerse sonsuza dek denemez; sınırlı bir hak vardır ve hak bitince iş görünür bir yere, başarısız işler listesine düşer. O liste boş değilse birinin bakması gerekir. Yanıtın 201 dönmesi yalnız yazının kaydedildiğini söyler; küçük resim işinin başarılı olduğunu göstermez, o iş yalnız kuyruğa konmuştur.

#### Özet

- Bu dersin ölçü sınıfı **ertelendi**: denetleyici küçük resim işini kuyruğa koydu, yanıt 201 döndü, iş yanıt anında çağrılmadı ve işçide koştu.
- Model aynı süreçtedir; işçi `isle()` çağrısının adıdır ve yanıttan sonra çalışır.
- Karşıt: eşzamanlı sürücüde aynı iş **koştu** ve yanıt onu bekledi; denetleyici kodu değişmedi, yalnız sürücü değişti.
- Sınırlayıcı ölçümde ilk denemede başarısız olan iş ikinci denemede koştu; üç denemede de başarısız olan iş başarısız işler listesine düştü.
- Kuyruğa konmuş olmak başarılı olmak değildir: 201 yazının kaydedildiğini söyler, kuyruktaki işin sonucunu söylemez.

#### Sonraki Adım

Küçük resim işi kuyruğa girdi, ama denetleyici onu adıyla bildi: `new KucukResimIsi(...)`. Yeni bir yan etki gerektiğinde, örneğin yazı kaydedilince bir arama dizinini güncellemek, denetleyiciye yeni bir satır eklemek gerekir. Olaylar ve Dinleyiciler dersi bu bağı gevşetir: denetleyici bir olay yayar, dinleyici onu işler. Peki olay adı dinleyicinin kayıtlı olduğu adla uyuşmazsa ne olur?

### Olaylar ve Dinleyiciler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/olaylar-ve-dinleyiciler

Olay, dinleyici ve olay yayınlayıcı; yan etkilerin denetleyiciden ayrılması; kayıtlı ad ile yayılan ad uyuşmadığında dinleyicinin bağlanmaması, kuyruklu dinleyicinin ertelenmesi, olay adının sınıf adıyla verilmesinin uyuşmazlığı kayıt anında yakalaması.

Kuyruklar ve İşler dersinde denetleyici küçük resim işini kuyruğa koydu, ama işin sınıfını adıyla bildi: `new KucukResimIsi(...)`. Yazı kaydedilince arama dizinini güncellemek, bir sayacı artırmak, bir günlük satırı yazmak da gerekirse denetleyici her yeni yan etkiyle uzar. Bu ders şunu sorar: **denetleyici olanı duyurur, kimin ilgileneceğini bilmezse, ilgilenenler yine de çağrılır mı?**

**Olay (event)** olmuş bir şeyin adıdır ve taşıdığı veridir: `YaziKaydedildi` olayı yazının kimliğini taşır. **Dinleyici (listener)**, bir olay adına bağlanmış ve olay yayılınca çağrılan fonksiyondur. **Olay yayınlayıcı (dispatcher)** adlara bağlı dinleyicileri tutar ve `yay()` çağrılınca eşleşenleri çağırır. Denetleyici yalnız olayı yayar; dizin güncellemesi denetleyiciden çıkar, dinleyiciye gider. Aradaki bağ **gevşektir**: denetleyici dinleyicinin var olduğunu bilmez, dinleyici denetleyiciyi bilmez. Bu gevşekliğin bir bedeli vardır ve bu dersin ölçümü onu gösterir: ad uyuşmazsa kimse hata vermez.

**US3.** Olay adı ile dinleyicinin kayıtlı olduğu ad dizge karşılaştırmasıyla eşleşir; eşleşme tam eşitliktir, önek ya da joker yoktur. Olay yayılırken dinleyici bulunamazsa hata fırlatılmaz: hiç dinleyicisi olmayan bir olay meşrudur. Kuyruklu dinleyici, Kuyruklar ve İşler dersindeki gibi aynı süreçtedir; `isle()` yayınlayıcıda bekleyenleri koşturur. Dinleyici sırası kayıt sırasıdır; olayı durdurma ve öncelik modellenmez.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.

```php
<?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ı',
    };
}
```

Bu ders çekirdeğin dört çağrısını da kullanır ve dinleyici için sıra önemlidir: yayınlayıcı bir olay yayınca o ada kayıtlı dinleyicileri yola koyar, eşzamanlı olanı `kostu()` ile işaretleyip çağırır, kuyruklu olanı `ertele()` ile işaretleyip kuyruğa bırakır. Hiçbir dinleyici yola konmazsa, ad hiçbir listede yoktur ve `sinifla()` **bağlanmadı** der.

#### Olay, Yayınlayıcı ve Dinleyici

`olaylar.php` dilimin tamamıdır. `YaziKaydedildi` yazı kimliğini taşıyan değiştirilemez bir olaydır. `Dizin` arama dizininin küçültülmüş biçimidir: yalnız eklenen kimliklerin listesi. `Yayinlayici` üç iş yapar. `dinle()` bir olay adına bir dinleyici ekler; `yay()` o ada kayıtlı dinleyicileri sırayla çağırır ya da kuyruğa koyar; `isle()` kuyruktakileri koşturur ve kaç tane koşturduğunu döndürür. `dinleSinif()` ise `dinle()`'nin bir koruması olan halidir: olay adı olarak verilen sınıf yoksa kayıt anında `InvalidArgumentException` fırlatır. Neden gerektiğini son bölümde göreceğiz.

`YazilarDenetleyicisi::kaydet()` yazıyı kaydeder (bu derste kaydetmek `kaydet` parçasının koşması demektir), olayı yayar ve 201 döndürür. Hangi ad altında yayacağı dışarıdan verilir; bu, bir sonraki ölçümde adın kasıtlı olarak uyuşmamasını sağlar.

```php
<?php
// olaylar.php — olay, yayınlayıcı ve dinleyici
declare(strict_types=1);
require_once 'kostu.php';

final class YaziKaydedildi {
    public function __construct(public readonly int $yaziId) {}
}

final class Dizin {
    public static array $kayitlar = [];
}

final class Yayinlayici {
    private array $dinleyiciler = [];   // olay adı => [[ad, fonksiyon, kuyruklu], ...]
    private array $kuyruk = [];

    public function dinle(string $olay, string $ad, callable $islev, bool $kuyruklu = false): void {
        $this->dinleyiciler[$olay][] = [$ad, $islev, $kuyruklu];
    }

    public function dinleSinif(string $sinif, string $ad, callable $islev, bool $kuyruklu = false): void {
        if (!class_exists($sinif)) {
            throw new InvalidArgumentException("olay sinifi yok: $sinif");
        }
        $this->dinle($sinif, $ad, $islev, $kuyruklu);
    }

    public function yay(string $olay, object $yuk): void {
        foreach ($this->dinleyiciler[$olay] ?? [] as [$ad, $islev, $kuyruklu]) {
            Gunluk::yola($ad);
            if ($kuyruklu) {
                Gunluk::ertele($ad);
                $this->kuyruk[] = [$islev, $yuk];
            } else {
                Gunluk::kostu($ad);
                $islev($yuk);
            }
        }
    }

    public function isle(): int {           // işçi: bekleyen dinleyicileri koşturur
        $sayi = count($this->kuyruk);
        while ($is = array_shift($this->kuyruk)) { [$islev, $yuk] = $is; $islev($yuk); }
        return $sayi;
    }
}

final class YazilarDenetleyicisi {
    public function __construct(private Yayinlayici $olaylar, private string $olayAdi) {}
    public function kaydet(int $yaziId): array {
        Gunluk::yeniIstek();
        Gunluk::yola('kaydet');
        Gunluk::kostu('kaydet');
        $this->olaylar->yay($this->olayAdi, new YaziKaydedildi($yaziId));
        return [201, 'kaydedildi'];
    }
}

function dizineEkle(YaziKaydedildi $olay): void { Dizin::$kayitlar[] = $olay->yaziId; }
```

#### Dinleyici Bağlanmadı

İlk ölçüm iki kayıt kurar. İkisinde de `dizin` dinleyicisi vardır ve denetleyici `yazi.kaydedildi` adıyla olay yayar. Birincisinde dinleyici `yazi.olusturuldu` adına kayıtlıdır; ikincisinde `yazi.kaydedildi` adına.

```php
<?php
// bagla.php — dinleyici kayıtlı adla yayılan ad uyuşmuyor
require_once 'olaylar.php';

function olc(string $kayitAdi, string $yayinAdi): void {
    Dizin::$kayitlar = [];
    $olaylar = new Yayinlayici();
    $olaylar->dinle($kayitAdi, 'dizin', 'dizineEkle');
    $yanit = (new YazilarDenetleyicisi($olaylar, $yayinAdi))->kaydet(7);
    printf("kayıt %s, yayın %s -> %d\n", $kayitAdi, $yayinAdi, $yanit[0]);
    foreach (['kaydet', 'dizin'] as $parca) printf("  %-7s %s\n", $parca, sinifla($parca));
    echo '  dizine eklenen: ', count(Dizin::$kayitlar), "\n";
}

olc('yazi.olusturuldu', 'yazi.kaydedildi');
olc('yazi.kaydedildi', 'yazi.kaydedildi');
```

```
kayıt yazi.olusturuldu, yayın yazi.kaydedildi -> 201
  kaydet  koştu
  dizin   bağlanmadı
  dizine eklenen: 0
kayıt yazi.kaydedildi, yayın yazi.kaydedildi -> 201
  kaydet  koştu
  dizin   koştu
  dizine eklenen: 1
```

Bu dersin gözlemi ilk kayıtta. Yanıt 201, `kaydet` koştu ve `dizin` **bağlanmadı**: dinleyici çalışmadı, dizine hiçbir şey eklenmedi, ama hiçbir hata da çıkmadı. Dinleyici yayınlayıcıya kayıtlıydı ve bir fonksiyon olarak sağlamdı; yalnız yayılan ada bağlı değildi. `sinifla()` önce `kostu`, sonra `ertelenen`, sonra `yol` listesine baktı ve dinleyiciyi hiçbirinde bulamadı: yayınlayıcı o adda dinleyici bulamadığı için `dizin`'i yola hiç koymadı. Atlanan ile bağlanmayan burada ayrılır. Atlanan parça yoldaydı ve çağrılmadı; bağlanmayan parça yola hiç girmedi.

İkinci kayıt karşıt ölçümdür. Ad uyuşunca aynı dinleyici **koştu** ve dizine bir kayıt eklendi. Kod aynıdır; değişen yalnız iki dizgenin eşitliğidir. Bu, gevşek bağın bedelinin doğrudan ölçümüdür: derleyici ya da çalışma zamanı bir dizge eşitsizliğini hata saymaz.

#### Karşıt: Kuyruklu ve Eşzamanlı Dinleyici

Dinleyicinin yanıtı bekletip bekletmediği ayrı bir sorudur. Kuyruklar ve İşler dersindeki sürücü ayrımı burada dinleyicinin bir işaretine iner: `kuyruklu` bayrağı. Aynı dinleyici bu kez ad uyuşarak kaydedilir ve iki biçimde ölçülür.

```php
<?php
// kuyruklu.php — aynı dinleyici eşzamanlı ve kuyruklu
require_once 'olaylar.php';

foreach ([false, true] as $kuyruklu) {
    Dizin::$kayitlar = [];
    $olaylar = new Yayinlayici();
    $olaylar->dinle('yazi.kaydedildi', 'dizin', 'dizineEkle', $kuyruklu);
    $yanit = (new YazilarDenetleyicisi($olaylar, 'yazi.kaydedildi'))->kaydet(7);
    echo $kuyruklu ? "kuyruklu dinleyici" : "eşzamanlı dinleyici", " -> {$yanit[0]}\n";
    echo '  dizin ', sinifla('dizin'), ', yanıt anında eklenen: ', count(Dizin::$kayitlar), "\n";
    echo '  işçi koşturdu: ', $olaylar->isle(), ', sonra eklenen: ', count(Dizin::$kayitlar), "\n";
}
```

```
eşzamanlı dinleyici -> 201
  dizin koştu, yanıt anında eklenen: 1
  işçi koşturdu: 0, sonra eklenen: 1
kuyruklu dinleyici -> 201
  dizin ertelendi, yanıt anında eklenen: 0
  işçi koşturdu: 1, sonra eklenen: 1
```

Eşzamanlı dinleyicide `dizin` **koştu**, yanıt anında dizinde bir kayıt vardır ve işçinin yapacağı bir şey kalmamıştır: `isle()` sıfır iş koşturdu. Kuyruklu dinleyicide `dizin` **ertelendi**: yanıt anında dizin boştur, `isle()` bir iş koşturur ve ancak sonra kayıt görünür. Sayılar iki yönden tutarlıdır: iki durumda da sonunda dizinde bir kayıt vardır, fark kaydın ne zaman göründüğündedir.

#### Sınırlayıcı Ölçüm

Uyuşmazlığın ilk ölçümdeki gibi sessiz kalması, ad bir dizge olduğu içindir. Olay adı olarak **sınıf adı** verilirse bu risk kayıt anında yakalanabilir mi? Aşağıdaki ölçüm `dinleSinif()` ile iki kayıt dener: doğru yazılmış `YaziKaydedildi` ve bir harfi yanlış `YaziKaydedilid`. Ardından aynı yanlış adı dizge olarak `dinle()` ile kaydeder ve doğru adı yayar.

```php
<?php
// sinif.php — olay adı dizge ya da sınıf adı
require_once 'olaylar.php';

foreach (['YaziKaydedildi', 'YaziKaydedilid'] as $ad) {
    $olaylar = new Yayinlayici();
    try {
        $olaylar->dinleSinif($ad, 'dizin', 'dizineEkle');
        echo "$ad: kayıt geçti\n";
    } catch (InvalidArgumentException $hata) {
        echo "$ad: kayıt reddedildi (", $hata->getMessage(), ")\n";
    }
}

Gunluk::yeniIstek();
$olaylar = new Yayinlayici();
$olaylar->dinle('YaziKaydedilid', 'dizin', 'dizineEkle');   // dizge: sessizce kabul edilir
$olaylar->yay('YaziKaydedildi', new YaziKaydedildi(7));
echo 'dizge adıyla kayıt, istekte dizin: ', sinifla('dizin'), "\n";
```

```
YaziKaydedildi: kayıt geçti
YaziKaydedilid: kayıt reddedildi (olay sinifi yok: YaziKaydedilid)
dizge adıyla kayıt, istekte dizin: bağlanmadı
```

Sınıf adıyla kayıt, yanlış yazılan adı **kayıt anında** reddetti: `dinleSinif()` sınıfın var olup olmadığına baktı ve `YaziKaydedilid` yoksa istisna fırlattı. Bu kayıt yanıt döngüsünden önce, uygulama kurulurken olur; geliştirici hatayı ilk çalıştırmada görür, ilk kullanıcı isteğinde değil. Aynı yanlış ad dizge olarak verilince kayıt sessizce kabul edildi ve doğru adla yayılan olayda `dizin` **bağlanmadı**: ilk ölçümdeki durumun aynısı.

Bir sınır var. Sınıf adı yazım hatasını yakalar; yanlış ama var olan bir sınıfı seçmeyi yakalamaz. Ayrıca koruma `dinleSinif()`'in yaptığı denetimden gelir: sınıf adı da bir dizgedir ve onu denetleyen kod yazılmadıysa dizgeden farkı kalmaz. Olay adını dizge yerine sınıf adıyla vermek uyuşmazlığı çalışma anından kayıt anına çeker ve bu kazanç denetimin kurulmuş olmasına bağlıdır.

#### Özet

- Bu dersin ölçü sınıfı **bağlanmadı**: dinleyici `yazi.olusturuldu` adına kayıtlıydı, olay `yazi.kaydedildi` adıyla yayıldı, `dizin` çağrılmadı ve yanıt yine de 201 döndü.
- Olay yayınlayıcı adlara bağlı dinleyicileri çağırır; denetleyici yan etkilerin sayısını ve adını bilmez, ama bunun bedeli sessiz uyuşmazlıktır.
- Karşıt: ad uyuşunca dinleyici **koştu**; kuyruklu işaretlenince **ertelendi** ve kayıt yalnız `isle()` sonrasında göründü.
- Sınırlayıcı ölçümde olay adını sınıf adıyla veren kayıt, yanlış yazılan adı kayıt anında reddetti; dizge adı aynı hatayı sessizce kabul etti.
- Atlanan yoldaydı ve çağrılmadı; bağlanmayan yola hiç girmedi.

#### Sonraki Adım

Yazı kaydedilince dizin güncellendi, ama kullanıcıya haber vermek ayrı bir yan etki: bir e-posta mı, uygulama içi bir uyarı mı, bir kısa mesaj mı? Bildirimler dersi tek bir olaya birden çok yolla yanıt verir ve alıcının hangi bildirim kanalını seçtiğini ölçer.

### Bildirimler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/bildirimler

Bildirim sınıfı, bildirim kanalı ve alıcının kanal tercihi; seçilen eşzamanlı kanalların koşması, seçilmeyen kanalın bağlanmaması; bildirim kuyruklu işaretlenince aynı kanalların ertelenmesi.

Olaylar ve Dinleyiciler dersinde yazı kaydedilince dizin güncellendi; kullanıcıya haber vermek ayrı bir yan etkidir. Haber verme yolu birden çok olabilir ve herkes aynısını istemez. Bu ders şunu sorar: **bir bildirim hangi yollarla gider ve alıcının seçmediği yol çağrılır mı?**

**Bildirim sınıfı (notification)**, ne söylendiğini ve hangi **bildirim kanallarından** gidebileceğini tanımlar. **Bildirim kanalı (channel)**, bildirimi bir yere ileten parçadır: posta, veritabanı ve kısa mesaj bu derste üç kanaldır. **Alıcının kanal tercihi** hangilerinin kullanılacağını söyler. Bildirim sınıfı üç kanalı da sayar; alıcı birini ya da ikisini seçer. Bildirici bu ikisinin kesişimini yola koyar. Kesişimin dışında kalan kanal ne çağrılır ne de yola girer.

**US4.** Üç kanalın hepsi bir dizi kaydıdır: posta kanalı bir posta sunucusuna, kısa mesaj kanalı bir operatöre bağlanmaz, her kanal gönderilen metni kendi dizisine ekler ve ölçülen, o dizide bir kayıt olup olmadığıdır. Model, önceki dersler gibi aynı süreçtedir. Başarısız gönderim, yeniden gönderme ve sınırlama modellenmez. Bildirimin kuyruklu olması bildirim sınıfının bir alanıdır; bildirici bu alana bakar.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.

```php
<?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ı',
    };
}
```

Bu derste her kanal bir parçadır: `posta`, `veritabani`, `kisamesaj`. Bildirici alıcının seçtiği kanalı yola koyar ve eşzamanlıysa `kostu()` ile çağırır, kuyrukluysa `ertele()` ile kuyruğa bırakır. Seçilmeyen kanal için hiçbir çağrı yapılmaz.

#### Bildirim, Kanal ve Alıcı

`bildirim.php` dilimin tamamıdır. `Alici` bir ad ve kanal tercihleri taşır. `YorumBildirimi` bir yorum başlığı, üç kanalın adı (`kanallar()`) ve `kuyruklu` bayrağı taşır. `BildirimKanali` arayüzü iki yöntemden oluşur, `ad()` ve `gonder()`; `DiziKanali` bunu bir dizi kaydıyla gerçekler. `Bildirici::gonder()` bildirimin kanallarını gezer; alıcının tercihinde olmayanı atlar (`continue`), olanı yola koyar ve ya çağırır ya kuyruğa bırakır. `isle()` kuyruktakileri koşturur. `YorumDenetleyicisi::yorumYap()` yorumu kaydeder ve bildiriciye bırakır.

```php
<?php
// bildirim.php — bildirim sınıfı, kanallar ve alıcı tercihi
declare(strict_types=1);
require_once 'kostu.php';

final class Alici {
    public function __construct(public readonly string $ad, public readonly array $tercihler) {}
}

final class YorumBildirimi {
    public function __construct(public readonly string $baslik, public readonly bool $kuyruklu = false) {}
    public function kanallar(): array { return ['posta', 'veritabani', 'kisamesaj']; }
    public function metin(): string { return "Yeni yorum: {$this->baslik}"; }
}

interface BildirimKanali {
    public function ad(): string;
    public function gonder(Alici $alici, YorumBildirimi $bildirim): void;
}

final class DiziKanali implements BildirimKanali {   // her kanal bir dizi kaydıdır
    public array $kayitlar = [];
    public function __construct(private string $ad) {}
    public function ad(): string { return $this->ad; }
    public function gonder(Alici $alici, YorumBildirimi $bildirim): void {
        $this->kayitlar[] = $alici->ad . ': ' . $bildirim->metin();
    }
}

final class Bildirici {
    private array $kanallar = [];
    private array $kuyruk = [];
    public function kanalEkle(BildirimKanali $kanal): void { $this->kanallar[$kanal->ad()] = $kanal; }
    public function kanal(string $ad): DiziKanali { return $this->kanallar[$ad]; }

    public function gonder(Alici $alici, YorumBildirimi $bildirim): void {
        foreach ($bildirim->kanallar() as $ad) {
            if (!in_array($ad, $alici->tercihler, true)) continue;   // alıcı seçmedi
            Gunluk::yola($ad);
            if ($bildirim->kuyruklu) {
                Gunluk::ertele($ad);
                $this->kuyruk[] = [$this->kanallar[$ad], $alici, $bildirim];
            } else {
                Gunluk::kostu($ad);
                $this->kanallar[$ad]->gonder($alici, $bildirim);
            }
        }
    }

    public function isle(): int {            // işçi: kuyruklu bildirimleri koşturur
        $sayi = count($this->kuyruk);
        while ($is = array_shift($this->kuyruk)) { [$kanal, $alici, $bildirim] = $is; $kanal->gonder($alici, $bildirim); }
        return $sayi;
    }
}

final class YorumDenetleyicisi {
    public function __construct(private Bildirici $bildirici) {}
    public function yorumYap(Alici $alici, YorumBildirimi $bildirim): array {
        Gunluk::yeniIstek();
        Gunluk::yola('yorumYap');
        Gunluk::kostu('yorumYap');
        $this->bildirici->gonder($alici, $bildirim);
        return [201, 'yorum yapildi'];
    }
}

function bildiriciKur(): Bildirici {
    $bildirici = new Bildirici();
    foreach (['posta', 'veritabani', 'kisamesaj'] as $ad) $bildirici->kanalEkle(new DiziKanali($ad));
    return $bildirici;
}
```

#### Alıcının Seçtiği Kanal

İlk ölçüm iki alıcı kurar. Ayşe posta ve veritabanını seçmiştir; Burak yalnız kısa mesajı. İkisi için de aynı yorum bildirimi gönderilir.

```php
<?php
// tercih.php — iki alıcı, iki farklı kanal tercihi
require_once 'bildirim.php';

$alicilar = [
    new Alici('ayse', ['posta', 'veritabani']),
    new Alici('burak', ['kisamesaj']),
];

foreach ($alicilar as $alici) {
    $bildirici = bildiriciKur();
    $yanit = (new YorumDenetleyicisi($bildirici))->yorumYap($alici, new YorumBildirimi('Merhaba'));
    echo "{$alici->ad} (", implode(', ', $alici->tercihler), ") -> {$yanit[0]}\n";
    foreach (['posta', 'veritabani', 'kisamesaj'] as $ad) {
        printf("  %-11s kayıt: %d  %s\n", $ad, count($bildirici->kanal($ad)->kayitlar), sinifla($ad));
    }
}
```

```
ayse (posta, veritabani) -> 201
  posta       kayıt: 1  koştu
  veritabani  kayıt: 1  koştu
  kisamesaj   kayıt: 0  bağlanmadı
burak (kisamesaj) -> 201
  posta       kayıt: 0  bağlanmadı
  veritabani  kayıt: 0  bağlanmadı
  kisamesaj   kayıt: 1  koştu
```

Bu dersin gözlemi ilk alıcıda: seçilen iki bildirim kanalı da eşzamanlı olduğu için **koştu** ve her birinde bir kayıt vardır. Yanıt 201'dir. Ayşe'nin seçmediği `kisamesaj` ise **bağlanmadı**: bildirim sınıfı o kanalı sayıyordu, ama bildirici onu yola hiç koymadı, çünkü alıcının tercihinde yoktu. Kayıt sayısı 0'dır ve bu, kısa mesajın başarısız olduğunu değil, hiç denenmediğini söyler.

İkinci alıcı karşıttır ve aynı kuralı tersinden gösterir. Burak yalnız `kisamesaj`'ı seçmiştir; `kisamesaj` **koştu**, posta ve veritabanı **bağlanmadı**. Aynı bildirim sınıfı, aynı bildirici, aynı denetleyici; değişen yalnız alıcının tercihidir. Seçilmeyen kanalın atlanmadığına dikkat etmek gerekir: atlanan yola girmiş ve çağrılmamış olandır, ama seçilmeyen kanal yola hiç konmadı.

#### Sınırlayıcı Ölçüm

Bildirim kuyruklu işaretlenirse aynı kanallar ne olur? Ayşe'ye aynı yorum bildirimi gönderilir, ama bu kez `kuyruklu: true` ile. Ölçüm yanıtın hemen ardından kanalları, işçi çalıştıktan sonra yine kanalları sayar.

```php
<?php
// kuyruklu_bildirim.php — bildirim kuyruklu işaretlenirse
require_once 'bildirim.php';

$alici = new Alici('ayse', ['posta', 'veritabani']);
$bildirici = bildiriciKur();
$yanit = (new YorumDenetleyicisi($bildirici))->yorumYap($alici, new YorumBildirimi('Merhaba', kuyruklu: true));
echo "kuyruklu bildirim -> {$yanit[0]}\n";
foreach (['posta', 'veritabani', 'kisamesaj'] as $ad) {
    printf("  %-11s kayıt: %d  %s\n", $ad, count($bildirici->kanal($ad)->kayitlar), sinifla($ad));
}
echo '  işçi koşturdu: ', $bildirici->isle(), "\n";
foreach (['posta', 'veritabani', 'kisamesaj'] as $ad) {
    printf("  %-11s kayıt: %d\n", $ad, count($bildirici->kanal($ad)->kayitlar));
}
```

```
kuyruklu bildirim -> 201
  posta       kayıt: 0  ertelendi
  veritabani  kayıt: 0  ertelendi
  kisamesaj   kayıt: 0  bağlanmadı
  işçi koşturdu: 2
  posta       kayıt: 1
  veritabani  kayıt: 1
  kisamesaj   kayıt: 0
```

Yanıt aynıdır, 201. Seçilen iki kanal artık **ertelendi**: yola kondular, isteğin dışına bırakıldılar ve yanıt anında kayıt sayıları 0'dır. Seçilmeyen kanal yine **bağlanmadı**. `isle()` iki iş koşturdu ve sonrasında posta ve veritabanında birer kayıt vardır; kısa mesajda hâlâ 0 kayıt var, çünkü onun kuyruğa girmesini gerektiren bir seçim yoktu. Bildirimin kuyruklu olması tercih kuralını değiştirmedi; yalnız seçilen kanalların koştuğu anı yanıtın ardına taşıdı.

Bu, Kuyruklar ve İşler dersindeki ayrımın bir kez daha görünümüdür: kuyruğa konmuş olmak, kayıt ancak işçi çalıştığında vardır demektir. Bildirimin gittiğine inanan bir uygulama, yanıtı gördüğü anda değil, işçinin ardından kayda bakmalıdır.

#### Özet

- Bu dersin ölçü sınıfı **koştu**: Ayşe'nin seçtiği posta ve veritabanı kanalları eşzamanlıydı, yanıt içinde çağrıldı ve her birinde bir kayıt bıraktı.
- Bildirim sınıfı kanalları sayar, alıcının tercihi seçer; bildirici ikisinin kesişimini yola koyar.
- Karşıt: Ayşe'nin seçmediği `kisamesaj` ve Burak'ın seçmediği posta ile veritabanı **bağlanmadı**; kayıt sayısı 0'dır ve kanal hiç denenmemiştir.
- Sınırlayıcı ölçümde bildirim kuyruklu işaretlenince seçilen kanallar **ertelendi**, `isle()` iki iş koşturdu ve kayıtlar ancak sonra göründü.
- Seçilmeyen kanal atlanmış değildir: atlanan yola girer, seçilmeyen girmez.

#### Sonraki Adım

Bildirim kanalları her istekte aynı sorguyu yinelemek zorunda kalır: alıcının tercihi, yorum sayısı, bir listenin sıralaması. Aynı sonucu tekrar hesaplamanın bir bedeli var ve bazen onu atlamak yeterlidir. Önbellekleme dersi hesaplanmış bir değeri bir anahtar altında saklar ve isabet ederse hesaplamanın atlanıp atlanmadığını ölçer.

### Önbellekleme

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/onbellekleme

Anahtar tabanlı önbellek, yaşam süresi, hatirla fonksiyonu ve geçersiz kılma; isabette hesaplamanın atlanması, geçersiz kılmadan sonra koşması; yaşam süresi dolunca yeniden hesaplama ve geçersiz kılma unutulursa güncel kaydın yanıta bağlanmaması.

Bildirimler dersinde bildirim kanalları her istekte yeniden çalıştı; her istek aynı soruyu baştan sormak zorundaydı. Bir değerin hesaplanması pahalıysa ve değer sık sık değişmiyorsa, sonucu saklamak ve ikinci istekte hesaplamayı atlamak daha ucuzdur. Bu ders şunu sorar: **kayıt değiştiğinde saklanan değer de değişir mi?**

**Önbellek (cache)** bir değeri bir **anahtar** altında saklayan yerdir. **Yaşam süresi (TTL)**, saklanan değerin ne kadar geçerli olduğunu söyler; süre dolunca değer bayattır ve yeniden hesaplanır. Dersin merkezindeki fonksiyon `hatirla(anahtar, sure, hesapla)`'dır: anahtar önbellekte geçerliyse değeri döndürür ve `hesapla` fonksiyonunu çağırmaz (**isabet**); değilse çağırır, sonucu saklar ve döndürür. Kayıt değiştiğinde saklanan değeri silmeye **geçersiz kılma (invalidation)** denir. Önbellek hızı verir, ama bayatlık riskini de beraberinde getirir; ve risk, geçersiz kılmanın unutulduğu yerde somutlaşır.

**US5.** Önbellek bir dizidir ve bir sunucu, bir ağ ya da bir dosya değildir. Saat bir değişkendir (`$simdi`); `sleep` çağrılmaz ve süre geçişi bu değişkeni artırarak taklit edilir, böylece ölçüm gerçek zamandan bağımsızdır. Yaşam süresi tam sınırda dolmuş sayılır: bitiş anı geldiğinde kayıt artık geçerli değildir. Önbellek tutarlılığı, aynı anda gelen iki isteğin birbirini ezmesi ve boyut sınırı modellenmez.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.

```php
<?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ı',
    };
}
```

Bu derste ölçülen parça `hesapla`'dır, yani önbelleğe verilen hesaplama fonksiyonu. `hatirla()` her çağrıda onu yola koyar; isabet olursa fonksiyon çağrılmaz ve parça **atlandı** sınıfında kalır, isabet olmazsa `kostu()` ile işaretlenir. İkinci bir parça, `guncel`, son bölümde yanıtın depodaki güncel kaydı taşıyıp taşımadığını ölçer.

#### Önbellek, Yaşam Süresi ve Geçersiz Kılma

`onbellek.php` dilimin tamamıdır. `Depo` kalıcı kaydın küçültülmüş biçimidir: yazı kimliğinden başlığa bir dizi ve gerçeğin kaynağı odur. `Onbellek` anahtar başına bir değer ve bir bitiş anı tutar. `hatirla()` hesaplamayı yola koyar, girdiye bakar, bitiş anı şimdiden büyükse değeri döndürür; değilse fonksiyonu çağırır, değeri `simdi + sure` bitişiyle saklar. `unut()` girdiyi siler, yani geçersiz kılar. `YazilarDenetleyicisi::goster()` başlığı `hatirla()` ile alır ve kaç kez gerçekten hesaplandığını sayar. `guncelMi()` ölçüm yardımcısıdır: yanıttaki başlık depodakiyle aynıysa `guncel` parçasını yola koyar ve çağırır; aynı değilse hiçbir şey yapmaz.

```php
<?php
// onbellek.php — anahtar tabanlı önbellek, yaşam süresi, geçersiz kılma
declare(strict_types=1);
require_once 'kostu.php';

final class Depo {                         // kalıcı kayıt: gerçeğin kaynağı
    public static array $baslik = [7 => 'Eski baslik'];
}

final class Onbellek {
    public int $simdi = 0;                 // saat bir değişkendir; sleep çağrılmaz
    private array $girdiler = [];          // anahtar => [değer, bitiş]

    public function hatirla(string $anahtar, int $sure, callable $hesapla): mixed {
        Gunluk::yola('hesapla');
        $girdi = $this->girdiler[$anahtar] ?? null;
        if ($girdi !== null && $girdi[1] > $this->simdi) {
            return $girdi[0];              // isabet: hesaplama çağrılmaz
        }
        Gunluk::kostu('hesapla');
        $deger = $hesapla();
        $this->girdiler[$anahtar] = [$deger, $this->simdi + $sure];
        return $deger;
    }

    public function unut(string $anahtar): void {   // geçersiz kılma
        unset($this->girdiler[$anahtar]);
    }
}

final class YazilarDenetleyicisi {
    public static int $hesaplama = 0;
    public function __construct(private Onbellek $onbellek) {}
    public function goster(int $id): array {
        Gunluk::yeniIstek();
        Gunluk::yola('goster');
        Gunluk::kostu('goster');
        $baslik = $this->onbellek->hatirla("yazi.$id", 60, function () use ($id) {
            self::$hesaplama++;
            return Depo::$baslik[$id];
        });
        return [200, $baslik];
    }
}

function guncelMi(array $yanit, int $id): void {   // yanıt, depodaki güncel kaydı taşıyor mu
    if ($yanit[1] === Depo::$baslik[$id]) { Gunluk::yola('guncel'); Gunluk::kostu('guncel'); }
}
```

#### İsabette Hesaplama Atlandı

İlk ölçüm üç istek yapar. Birincisi önbellek boştur. İkincisi on birim sonra gelir, hâlâ süre içindedir. Üçüncüsü yazının başlığı depoda değiştirilip önbellekteki girdi geçersiz kılındıktan sonra gelir.

```php
<?php
// isabet.php — ilk istek, isabet, geçersiz kılma
require_once 'onbellek.php';

$onbellek = new Onbellek();
$denetleyici = new YazilarDenetleyicisi($onbellek);

function istek(string $etiket, YazilarDenetleyicisi $d, Onbellek $o): void {
    $yanit = $d->goster(7);
    printf("%s (saat %d) -> %d %s\n", $etiket, $o->simdi, $yanit[0], $yanit[1]);
    echo '  hesapla: ', sinifla('hesapla'), ', toplam hesaplama: ', YazilarDenetleyicisi::$hesaplama, "\n";
}

istek('ilk istek', $denetleyici, $onbellek);
$onbellek->simdi = 10;
istek('ikinci istek', $denetleyici, $onbellek);
Depo::$baslik[7] = 'Yeni baslik';
$onbellek->unut('yazi.7');
$onbellek->simdi = 20;
istek('geçersiz kılmadan sonra', $denetleyici, $onbellek);
```

```
ilk istek (saat 0) -> 200 Eski baslik
  hesapla: koştu, toplam hesaplama: 1
ikinci istek (saat 10) -> 200 Eski baslik
  hesapla: atlandı, toplam hesaplama: 1
geçersiz kılmadan sonra (saat 20) -> 200 Yeni baslik
  hesapla: koştu, toplam hesaplama: 2
```

Bu dersin gözlemi ikinci istekte. Önbellek girdisi geçerliydi, `hatirla()` değeri döndürdü ve `hesapla` yola konduğu hâlde çağrılmadı: **atlandı**. Toplam hesaplama sayısı 1'de kaldı. Yanıt aynı başlığı taşıdı, `Eski baslik`; istemci farkı görmedi, ama iş yapılmadı. Aynı `hesapla` ilk istekte **koştu**: önbellek boştu, hesaplama çağrıldı, sonuç saklandı ve toplam 1 oldu.

Üçüncü istek karşıttır. Başlık depoda değişti ve `unut('yazi.7')` girdiyi sildi. Yanıt `Yeni baslik` taşıdı, `hesapla` yeniden **koştu** ve toplam hesaplama 2 oldu. Aynı anahtar, aynı denetleyici; hesaplamanın koşup koşmayacağını yalnız girdinin var olup olmadığı belirledi. Geçersiz kılma, önbelleğe değişikliği haber veren tek yoldur.

#### Sınırlayıcı Ölçüm

Geçersiz kılma unutulursa ne olur ve yaşam süresi bunu düzeltir mi? İkinci ölçüm üç istek yapar. İlki değeri saklar. Ardından depoda başlık değişir ama girdi silinmez. İkinci istek süre dolmadan gelir. Üçüncüsü süre dolunca gelir. Her istekte `guncel` parçası, yanıtın depodaki güncel kaydı taşıyıp taşımadığını ölçer.

```php
<?php
// bayat.php — yaşam süresi dolunca ve geçersiz kılma unutulunca
require_once 'onbellek.php';

$onbellek = new Onbellek();
$denetleyici = new YazilarDenetleyicisi($onbellek);

function olc(string $etiket, YazilarDenetleyicisi $d, Onbellek $o): void {
    $yanit = $d->goster(7);
    guncelMi($yanit, 7);
    printf("%s (saat %d) -> %s\n", $etiket, $o->simdi, $yanit[1]);
    echo '  hesapla: ', sinifla('hesapla'), ', güncel kayıt: ', sinifla('guncel'), "\n";
}

olc('önbelleğe al', $denetleyici, $onbellek);
Depo::$baslik[7] = 'Yeni baslik';          // geçersiz kılma unutuldu
$onbellek->simdi = 30;
olc('süre dolmadan', $denetleyici, $onbellek);
$onbellek->simdi = 60;
olc('süre dolunca', $denetleyici, $onbellek);
echo 'toplam hesaplama: ', YazilarDenetleyicisi::$hesaplama, "\n";
```

```
önbelleğe al (saat 0) -> Eski baslik
  hesapla: koştu, güncel kayıt: koştu
süre dolmadan (saat 30) -> Eski baslik
  hesapla: atlandı, güncel kayıt: bağlanmadı
süre dolunca (saat 60) -> Yeni baslik
  hesapla: koştu, güncel kayıt: koştu
toplam hesaplama: 2
```

İlk istekte `hesapla` **koştu** ve yanıt depodaki kaydı taşıdığı için `guncel` de koştu. Sonra başlık depoda değişti ve girdi silinmedi. İkinci istekte, saat 30'da, önbellek geçerli girdiyi döndürdü: `hesapla` **atlandı** ve yanıt `Eski baslik` taşıdı, yani depodaki `Yeni baslik` yanıta ulaşmadı. `guncel` parçası bu yüzden yola hiç girmedi ve **bağlanmadı**. Yanıt 200'dür ve hata yoktur; bayat değer sessizce döner. Bu, önbelleğin en tehlikeli hâlidir: yanıt başarılı görünür ve eksik olan hiçbir yerde belirtilmez.

Üçüncü istek saat 60'ta geldi ve yaşam süresi tam dolmuştu. `hesapla` yeniden **koştu**, yanıt `Yeni baslik` taşıdı ve `guncel` de koştu. Toplam hesaplama sayısı 2'dir. Yaşam süresi bayatlığı düzeltti, ama ancak sürenin sonunda. Süre ne kadar uzunsa unutulan geçersiz kılmanın bedeli o kadar büyüktür. Geçersiz kılma ile yaşam süresi birbirinin yerine geçmez: geçersiz kılma değişikliği hemen yansıtır, yaşam süresi unutulan geçersiz kılmaya karşı bir üst sınırdır.

#### Özet

- Bu dersin ölçü sınıfı **atlandı**: ikinci istekte önbellek isabet etti, `hesapla` yoldaydı ve çağrılmadı, toplam hesaplama sayısı 1'de kaldı.
- `hatirla(anahtar, sure, hesapla)` geçerli girdiyi döndürür, değilse hesaplar ve saklar; geçersiz kılma girdiyi siler.
- Karşıt: geçersiz kılmadan sonra `hesapla` yeniden **koştu**, yanıt yeni başlığı taşıdı ve toplam 2 oldu.
- Sınırlayıcı ölçümde yaşam süresi dolunca `hesapla` yeniden **koştu**; geçersiz kılma unutulunca güncel kayıt yanıta **bağlanmadı** ve bayat değer 200 ile döndü.
- Yaşam süresi geçersiz kılmanın yerine geçmez; yalnız unutulan geçersiz kılmanın bayatlık süresini sınırlar.

#### Sonraki Adım

Önbellekteki değer, kaynağındaki değişiklikten haberdar olmadığı sürece bayat kaldı. Peki saklanan şey bir değer değil de bir dosyaysa ve dosyanın yazıldığı disk değişirse ne değişir: yükleme kodu mu, yanıt mı, dosyanın kendisi mi? Dosya Depolama dersi bir dosyayı iki ayrı diske yazar ve okur ve disk değiştiğinde bunlardan hangisinin değiştiğini ölçer.

### Dosya Depolama

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/dosya-depolama

Disk soyutlaması: aynı arayüz arkasında yerel disk ve bellek diski; aynı yükleme çağrısının iki diskte koşması, yapılandırmada adı geçmeyen diskin bağlanmaması ve disk değişiminin yanıta değil dosyanın ömrüne yansıması.

Önbellekleme dersinde saklanan değer, kaynağındaki değişiklikten habersiz kaldığı sürece bayat kaldı. O ders değerleri bir dizide tuttu; bir dosya ise değerden farklı bir şeydir: boyutu büyüktür, yanıtın dışında yaşamalıdır ve nereye yazıldığı uygulamanın yaşamı boyunca değişebilir. Bir geliştirici yüklenen dosyayı bugün çalışma dizinine yazar, yarın nesne depolama diye bilinen ayrı bir hizmete taşımak ister. Bu ders şunu sorar: **depolamanın yeri değiştiğinde yükleme kodu da değişir mi?**

**Disk** bu derste bir dosyanın yazılıp okunabildiği yerin adıdır; ürün adı değildir. **Disk soyutlaması** denetleyicinin yalnız `yaz`, `oku` ve `var` sözleşmesini bilmesi, dosyanın nereye gittiğini bilmemesi demektir. Sözleşmeyi bir **arayüz (interface)** taşır ve her disk onu gerçekler. Dersin iki diski vardır: çalışma dizinindeki bir klasöre yazan **yerel disk** ve dosyaları bir dizide tutan **bellek diski**. Bellek diski gerçek bir nesne depolamanın modelidir: dosya bir yol adı altında saklanır, ama dizin ağacı, izin ve sürücü yoktur; yalnız ad ve içerik vardır. Hangi diskin kullanılacağını bir **yapılandırma** söyler: adlar diske eşlenir ve varsayılan ad ayrıca seçilir.

**US6.** Yerel disk, çalışma dizininin altında bu ders için açılan bir klasördür; ders bitince silinir ve yol çıktıya basılmaz. Bellek diski bir dizidir ve süreç bitince kaybolur. Bulut depolama, ağ gecikmesi, kimlik bilgisi, eşzamanlı yazma ve dosya izinleri modellenmez; bellek diski yalnızca nesne depolamanın "ad altında içerik" modelini taşır. Bir dosyanın yazılması bir yöntem çağrısıdır ve başarısız yazma modellenmez.

**US7.** Çözümleme yapılandırmadaki adlarla sınırlıdır. İstenen ad yapılandırmada yoksa varsayılan diske düşülmez; `Depolama` bir hata verir ve denetleyici bu hatayı 500 yanıtına çevirir. Sessiz bir yedek disk, dosyanın beklenmedik bir yerde yaşamasına yol açacağı için bu derste kasıtlı olarak yoktur.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır. Bu derste `yola()` çağrısı bir diskin yapılandırmadan çözüldüğünü, `kostu()` çağrısı ise o diskin `yaz()` yönteminin çalıştığını işaretler.

```php
<?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ı',
    };
}
```

#### Disk Arayüzü ve İki Disk

`depolama.php` dört şey tanımlar. `Disk` arayüzü sözleşmeyi verir. `YerelDisk` dosyayı çalışma dizinindeki klasöre `file_put_contents` ile yazar; `temizle()` yöntemi arayüzün parçası değildir ve yalnız dersin kendi artığını silmek içindir. `BellekDiski` aynı sözleşmeyi bir diziyle gerçekler. `Depolama` adlarla diskleri eşler ve `disk()` çağrısında adı çözümler: ad verilmezse varsayılan kullanılır, ad yapılandırmada yoksa `InvalidArgumentException` fırlatılır. Son olarak `YuklemeDenetleyicisi` bir dosyayı ve içeriğini alır, isteğin yoluna `yukle` parçasını koyar, kendi işaretini bırakır ve dosyayı seçilen diske yazar.

Denetleyicinin gövdesinde hangi diskin kullanıldığını söyleyen tek bir ifade yoktur. `disk($disk)` çağrısı bir `Disk` döndürür, `yaz()` yöntemi sözleşmeden gelir. Disk değiştiğinde değişen şey, `Depolama` nesnesine hangi adın verildiğidir; denetleyici bu bilginin yalnız dışarıdan geldiğini görür.

```php
<?php
// depolama.php — disk arayüzü, yerel disk, bellek diski ve yapılandırma
declare(strict_types=1);
require_once 'kostu.php';

interface Disk {
    public function yaz(string $yol, string $icerik): void;
    public function oku(string $yol): string;
    public function var(string $yol): bool;
}

final class YerelDisk implements Disk {
    public function __construct(private string $kok) {
        if (!is_dir($kok)) mkdir($kok, 0777, true);
    }
    public function yaz(string $yol, string $icerik): void {
        Gunluk::kostu('yerel');
        file_put_contents($this->kok . '/' . $yol, $icerik);
    }
    public function oku(string $yol): string { return file_get_contents($this->kok . '/' . $yol); }
    public function var(string $yol): bool { return is_file($this->kok . '/' . $yol); }
    public function temizle(): void {
        foreach (glob($this->kok . '/*') as $dosya) unlink($dosya);
        rmdir($this->kok);
    }
}

final class BellekDiski implements Disk {
    private array $dosyalar = [];
    public function yaz(string $yol, string $icerik): void {
        Gunluk::kostu('bellek');
        $this->dosyalar[$yol] = $icerik;
    }
    public function oku(string $yol): string { return $this->dosyalar[$yol]; }
    public function var(string $yol): bool { return isset($this->dosyalar[$yol]); }
}

final class Depolama {
    private array $diskler = [];
    public function __construct(private string $varsayilan) {}
    public function ekle(string $ad, Disk $disk): void { $this->diskler[$ad] = $disk; }
    public function disk(?string $ad = null): Disk {
        $ad ??= $this->varsayilan;
        if (!isset($this->diskler[$ad])) {
            throw new InvalidArgumentException("disk yapilandirmada yok: $ad");
        }
        Gunluk::yola($ad);
        return $this->diskler[$ad];
    }
}

final class YuklemeDenetleyicisi {
    public function __construct(private Depolama $depolama) {}
    public function yukle(string $dosya, string $icerik, ?string $disk = null): array {
        Gunluk::yeniIstek();
        Gunluk::yola('yukle');
        Gunluk::kostu('yukle');
        try {
            $this->depolama->disk($disk)->yaz($dosya, $icerik);
        } catch (InvalidArgumentException $hata) {
            return [500, 'disk yok'];
        }
        return [201, 'yuklendi'];
    }
}
```

#### Aynı Çağrı İki Diskte Koştu

Sürücü aynı `yukle()` çağrısını üç kez yapar: ad verilmeden, `bellek` adıyla ve yapılandırmada adı geçmeyen `uzak` adıyla. Her çağrıdan sonra üç parçanın sınıfı yazılır. Birinci çağrıda varsayılan disk `yerel`'dir; denetleyici çağrılırken disk adını bilmez, yine de dosya yerel diske gider. İkinci çağrıda aynı denetleyici, aynı kod, aynı dosya adı ve aynı içerikle `bellek` diskine yazar. İki çağrı da 201 döndürür ve denetleyicide tek satır değişmemiştir.

```php
<?php
// disk.php — aynı yukle() çağrısı iki diskte, üçüncü disk yapılandırmada yok
require_once 'depolama.php';

$yerel = new YerelDisk('depo-ders06');
$depolama = new Depolama('yerel');
$depolama->ekle('yerel', $yerel);
$depolama->ekle('bellek', new BellekDiski());
$denetleyici = new YuklemeDenetleyicisi($depolama);

foreach ([null, 'bellek', 'uzak'] as $ad) {
    $yanit = $denetleyici->yukle('rapor.txt', 'merhaba', $ad);
    printf("yukle(disk: %s) -> %d %s\n", $ad ?? 'varsayılan', $yanit[0], $yanit[1]);
    foreach (['yerel', 'bellek', 'uzak'] as $parca) printf("  %-6s %s\n", $parca, sinifla($parca));
}
echo 'yerel diskte okunan: ', $yerel->oku('rapor.txt'), "\n";
$yerel->temizle();
```

```
yukle(disk: varsayılan) -> 201 yuklendi
  yerel  koştu
  bellek bağlanmadı
  uzak   bağlanmadı
yukle(disk: bellek) -> 201 yuklendi
  yerel  bağlanmadı
  bellek koştu
  uzak   bağlanmadı
yukle(disk: uzak) -> 500 disk yok
  yerel  bağlanmadı
  bellek bağlanmadı
  uzak   bağlanmadı
yerel diskte okunan: merhaba
```

Birinci çağrıda `yerel` **koştu**, çünkü hem yola konmuş hem çağrılmıştır; `bellek` ise yapılandırmada tanımlı olduğu hâlde bu istekte çözülmediği için **bağlanmadı**. İkinci çağrıda roller yer değiştirir. Bu iki çıktı birlikte dersin ölçü sınıfını verir: aynı `yaz()` çağrısı iki diskte de **koştu**. Ölçüm "yazıldı" demez, "çalıştı" der; dosyanın gerçekten oraya ulaşıp ulaşmadığını sürücünün son satırı, yerel diskten okunan `merhaba`, ayrıca gösterir. Üçüncü çağrının sonucu ise bir sonraki bölümün konusudur.

#### Okuma Tarafı da Aynı Sözleşmeden Geçer

Yazma tarafı ölçüldü; okuma tarafında da aynı soru sorulabilir. `okuma.php` iki diske aynı iki satırlık içeriği yazar, sonra her diskten içeriği okur ve iki ayrı dosya adı için `var()` çağırır: yazılmış olan `ozet.txt` ve hiç yazılmamış `eksik.txt`. Arayüz yalnız bayt sayısını ve bir doğru ya da yanlış değerini vaat eder; ölçüm, iki diskin bu vaadi aynı biçimde tutup tutmadığına bakar.

```php
<?php
// okuma.php — iki diskte aynı yaz, oku ve var çağrıları
require_once 'depolama.php';

$diskler = ['yerel' => new YerelDisk('depo-ders06c'), 'bellek' => new BellekDiski()];
foreach ($diskler as $ad => $disk) {
    $disk->yaz('ozet.txt', "satir bir\nsatir iki");
    printf("%-6s oku: %d bayt, ozet.txt var: %s, eksik.txt var: %s\n", $ad,
        strlen($disk->oku('ozet.txt')),
        $disk->var('ozet.txt') ? 'evet' : 'hayır',
        $disk->var('eksik.txt') ? 'evet' : 'hayır');
}
$diskler['yerel']->temizle();
```

```
yerel  oku: 19 bayt, ozet.txt var: evet, eksik.txt var: hayır
bellek oku: 19 bayt, ozet.txt var: evet, eksik.txt var: hayır
```

İki satır aynıdır: içerik 19 bayt olarak döner, yazılan dosya vardır ve yazılmayan dosya yoktur. `var()` yöntemi var olmayan bir dosya için hata atmaz, yanlış döndürür; böylece okumadan önce varlığı sormak iki diskte de güvenlidir. Aynı sözleşmenin dışında kalan şey `oku()` yönteminin var olmayan bir dosyada ne yaptığıdır: arayüz bunu tanımlamaz ve iki disk bu durumda farklı davranabilir. Bu yüzden ders, okumadan önce `var()` sorulmasını sözleşmenin parçası sayar ve bu varsayımı yazılı tutar.

#### Karşıt: Yapılandırmada Adı Geçmeyen Disk Bağlanmadı

Üçüncü çağrıda `uzak` adı verildi. `Depolama::disk()` adı yapılandırmada bulamadı, hatayı yola işaret koymadan önce fırlattı, denetleyici de hatayı 500 yanıtına çevirdi. Çıktının son bölümünde üç parçanın hiçbiri koşmadı: `yerel` ve `bellek` bu istekte hiç çözülmediği için, `uzak` ise yapılandırmada olmadığı için yola bile girmedi. Üçü de **bağlanmadı**.

Bunun neden `atlandı` olmadığını görmek önemlidir. Bir parça, yolda olduğu hâlde çağrılmadığında atlanır. `uzak` bu istekte yola hiç konmadı; çünkü çözümleme, ad geçerli olmadan işaret bırakmaz. Yani sapma, kodun bir dalı atlanarak değil, bağlantının kendisi kurulamadığı için oluştu. Aynı hatanın "disk var ama yazma başarısız oldu" biçimi bu dersin modelinde yoktur; orada `yola` işareti bırakılır ve `yaz()` içindeki `kostu` işareti eksik kalırdı, bu durum **atlandı** olarak okunurdu.

Karşıtın pratik anlamı bir yapılandırma hatasının sessizce başka yere yazmak yerine yüksek sesle başarısız olmasıdır. Dosya hiçbir diskte yoktur ve istemci 500 alır; sessiz bir varsayılan diske düşmek ise yüklenen dosyanın beklenmedik bir yerde yaşamasına yol açardı. Burada ölçüm yalnız "bir parça bağlanmadı" der; hatanın bir yapılandırma hatası mı, bir yazım hatası mı olduğunu söylemez.

#### Sınırlayıcı Ölçüm

Önceki iki bölüm "denetleyici diskten habersiz" diyor; bu bölüm bunun neyi kapsamadığını ölçer. Disk değişince denetleyici değişmedi, ama dosyanın ömrü değişti. `kalicilik.php` iki diski aynı iş için kullanır: dosyayı yazar, sonra aynı türden **yeni bir disk örneği** kurar ve dosyanın orada olup olmadığına bakar. Yeni örnek, yeni bir istekte yeniden kurulmuş bir bileşeni temsil eder.

```php
<?php
// kalicilik.php — aynı arayüz, farklı ömür
require_once 'depolama.php';

foreach (['yerel', 'bellek'] as $tur) {
    $kur = fn(): Disk => $tur === 'yerel' ? new YerelDisk('depo-ders06b') : new BellekDiski();
    $birinci = $kur();
    $birinci->yaz('not.txt', 'kalici mi');
    $ikinci = $kur();              // yeni örnek: aynı arayüz, sıfırdan kurulmuş
    printf("%-6s birinci var: %s, yeni örnekte var: %s\n", $tur,
        $birinci->var('not.txt') ? 'evet' : 'hayır',
        $ikinci->var('not.txt') ? 'evet' : 'hayır');
}
(new YerelDisk('depo-ders06b'))->temizle();
```

```
yerel  birinci var: evet, yeni örnekte var: evet
bellek birinci var: evet, yeni örnekte var: hayır
```

Yerel diskte yeni örnek dosyayı **görür**, çünkü dosya klasörde durur ve iki örnek aynı klasöre bakar. Bellek diskinde yeni örnek boştur; dosya birinci örneğin dizisinde kalmıştır. Arayüz iki durumda da aynıdır: `var()` yöntemi iki diskte çağrılır ve ikisi de bir doğru ya da yanlış döndürür. Fark arayüzün dışındadır, diskin ömründedir.

Bunun sonucu, Önbellekleme dersinin Sonraki Adım'ında sorulan soruya yanıttır: disk değişince yanıt değişmedi, ama yanıtın dayandığı dosyanın bir sonraki istekte var olup olmadığı değişti. Soyutlama çağrı biçimini örter, çağrının kalıcılık sözleşmesini örtmez. Denetleyicinin yükleme kodu disk değişiminden etkilenmedi; ancak yüklenen dosyayı bir sonraki istekte okuyan kod bellek diskiyle yanlış sonuç alırdı. Bu ölçüm yalnız iki diskin kalıcılığını karşılaştırır; gerçek nesne depolamanın tutarlılık ve gecikme davranışını, ağ kopmasını ya da büyük dosyayı ölçmez.

#### Özet

- Bu dersin ölçü sınıfı **koştu**: aynı `yukle()` çağrısı yerel diskte de bellek diskinde de `yaz()` yöntemini çalıştırdı ve denetleyici disk değişiminden habersiz kaldı.
- `Disk` arayüzü `yaz`, `oku` ve `var` sözleşmesini verir; `Depolama` adları diske eşler ve varsayılanı ayrı tutar.
- Karşıt: yapılandırmada adı geçmeyen `uzak` diski yola girmedi ve **bağlanmadı**; denetleyici 500 döndürdü, dosya hiçbir diske gitmedi.
- Sınırlayıcı ölçümde yeni disk örneği yerel diskteki dosyayı gördü, bellek diskindeki dosyayı **görmedi**; soyutlama çağrı biçimini örter, kalıcılığı örtmez.
- Disk değişince denetleyici değişmez, ama dosyayı sonradan okuyan kodun varsayımı değişebilir.

#### Sonraki Adım

Yüklenen dosya artık bir diske yazılıyor ve hangi diske yazıldığı denetleyiciyi ilgilendirmiyor. Ama bu işlerin hepsi hâlâ bir isteğin içinde, yanıt gelmeden önce yapılıyor. Peki bazı işler hiçbir isteğe bağlı olmadan, her dakika ya da her gün kendiliğinden tekrar etmeliyse, kim çalıştıracak ve istek açısından bu iş ne zaman koşmuş sayılacak? Zamanlanmış Görevler dersi yinelenen bir görevi tanımlar, zamanlayıcının tikinde koşturur ve isteğin bu görevi beklemeden yanıt verip vermediğini ölçer.

### Zamanlanmış Görevler

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/zamanlanmis-gorevler

Yinelenen görev tanımı, sıklık sözcükleri ve zamanlayıcı tiki; istekte kaydedilen görevin tikte koşması, zamanı gelmemiş görevin tikte atlanması ve tik aralığının görev sıklığına koyduğu sınır.

Dosya Depolama dersinde yüklenen dosya bir diske yazıldı ve bu iş yanıt gelmeden önce, isteğin içinde bitti. Her iş böyle değildir. Eski dosyaları temizlemek, her gece bir özet üretmek ya da her dakika bir sayacı sıfırlamak gibi işler hiçbir kullanıcı isteğine bağlı değildir; bir istek gelmese de belli aralıklarla çalışmaları gerekir. Bu ders şunu sorar: **tekrarlanan bir iş, hangi isteğin içinde koşmuş sayılır?**

**Zamanlanmış görev (scheduled task)**, bir **sıklık** ile tanımlanan yinelenen bir iştir: her dakika, her saat, her gün. Görevi çalıştıran şey bir istek değil, düzenli aralıklarla çalışan bir **zamanlayıcıdır (scheduler)**; her çalışmasına **tik** denir. Zamanlayıcı her tikte kayıtlı görevlere bakar ve zamanı gelmiş olanları koşturur. Bir istek yalnızca görevi kaydedebilir; görevin çalışma anı isteğin dışında, sonraki bir tiktedir. Kuyruklar ve İşler dersindeki iş de isteğin dışında koşmuştu, ama oradaki iş bir istekten doğmuştu ve bir kez çalışıyordu. Burada iş bir kez tanımlanır ve kendisini tekrar eder.

**US8.** Saat bir değişkendir: tik, geçerli an olarak bir tam sayı alır (saniye) ve `sleep` ya da gerçek saat çağrılmaz. Zamanlayıcının kendisi bir döngü, bir süreç ya da bir sistem aracı değildir; `tik()` yöntemi sürücü tarafından elle çağrılır. Bir görev, kaydedildiği anda ya da bir önceki çalışma anında başlayan bir aralığın sonunda çalışır; ilk çalışma kayıttan bir aralık sonradır, kayıt anında değildir.

**US9.** Üç sıklık sözcüğü tanımlıdır: `her_dakika` 60 saniye, `her_saat` 3600 saniye, `her_gun` 86400 saniye. Başka bir sözcük `InvalidArgumentException` fırlatır. Takvim saati, saat dilimi, ay ve gün adı, kümeleme ve aynı anda çalışan iki zamanlayıcının görevi iki kez koşturması modellenmez. Kaçırılan bir tiki telafi eden bir mekanizma da yoktur; bu varsayımın sonucu Sınırlayıcı Ölçüm bölümünde ölçülür.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır. Bu derste "istek" iki yerde olur: görevi kaydeden gerçek istek ve zamanlayıcının her tiki. `ertele()` kayıt anında, `yola()` ve `kostu()` ise tikte çağrılır.

```php
<?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ı',
    };
}
```

#### Görev Tanımı ve Zamanlayıcı

`zamanlayici.php` üç sınıf tanımlar. `Sayac` bir işin kaç kez çalıştığını sayar; ölçülen şey işin gövdesi değil, işin çalışıp çalışmadığıdır. `Zamanlayici::kaydet()` görevi bir ad, bir sıklık sözcüğü ve bir fonksiyonla alır, sıklığı saniyeye çevirir, ilk çalışma anını `simdi + aralik` olarak hesaplar ve görevi isteğin dışına bırakılanlar listesine yazar. `Zamanlayici::tik()` yeni bir ölçüm turu açar, her görevi yola koyar ve geçerli an görevin `sonraki` değerine ulaşmamışsa göreve dokunmadan sonrakine geçer. Ulaşmışsa sonraki çalışma anını `simdi + aralik` olarak günceller, görevi koştu olarak işaretler ve fonksiyonu çağırır. `RaporDenetleyicisi` ise bir istekte `rapor` görevini her dakika çalışacak biçimde kaydeder ve 202 döndürür.

Yanıtın 202 olması anlamlıdır: istek bir şeyi kabul etti, ama yapmadı. Denetleyici görevi koşturmaz ve koşturmasını beklemez; yanıt, görev henüz çalışmadan dönmüştür. Bu, Kuyruklar ve İşler dersindeki yanıtla aynı yapıdadır, tek fark çalışma anını bir kuyruk işçisinin değil bir saat değerinin belirlemesidir.

```php
<?php
// zamanlayici.php — görev tanımı, sıklık sözcükleri ve zamanlayıcı tiki
declare(strict_types=1);
require_once 'kostu.php';

final class Sayac {
    public array $say = [];
    public function artir(string $ad): void { $this->say[$ad] = ($this->say[$ad] ?? 0) + 1; }
    public function oku(string $ad): int { return $this->say[$ad] ?? 0; }
}

final class Zamanlayici {
    private const ARALIK = ['her_dakika' => 60, 'her_saat' => 3600, 'her_gun' => 86400];
    private array $gorevler = [];

    public function kaydet(string $ad, string $siklik, callable $is, int $simdi): void {
        if (!isset(self::ARALIK[$siklik])) {
            throw new InvalidArgumentException("bilinmeyen siklik: $siklik");
        }
        $aralik = self::ARALIK[$siklik];
        $this->gorevler[$ad] = ['aralik' => $aralik, 'sonraki' => $simdi + $aralik, 'is' => $is];
        Gunluk::ertele($ad);
    }

    public function tik(int $simdi): void {
        Gunluk::yeniIstek();
        foreach (array_keys($this->gorevler) as $ad) {
            Gunluk::yola($ad);
            if ($simdi < $this->gorevler[$ad]['sonraki']) {
                continue;
            }
            $this->gorevler[$ad]['sonraki'] = $simdi + $this->gorevler[$ad]['aralik'];
            Gunluk::kostu($ad);
            ($this->gorevler[$ad]['is'])();
        }
    }
}

final class RaporDenetleyicisi {
    public function __construct(private Zamanlayici $zamanlayici, private Sayac $sayac) {}
    public function kaydet(int $simdi): array {
        Gunluk::yeniIstek();
        $this->zamanlayici->kaydet('rapor', 'her_dakika', fn() => $this->sayac->artir('rapor'), $simdi);
        return [202, 'kaydedildi'];
    }
}
```

#### İstekte Kaydedilen Görev Tikte Koştu

Sürücü isteği saat 0'da yapar, sonra zamanlayıcıyı saat 60'ta tikler. İstekten hemen sonra sınıf ve sayaç okunur, tikten sonra yine okunur. İki okuma arasındaki tek fark, zamanlayıcının çalışmış olmasıdır.

```php
<?php
// erteleme.php — istekte kaydedilen görev tikte koşar
require_once 'zamanlayici.php';

$sayac = new Sayac();
$zamanlayici = new Zamanlayici();
$denetleyici = new RaporDenetleyicisi($zamanlayici, $sayac);

$yanit = $denetleyici->kaydet(0);
printf("istek (saat 0)  -> %d %s\n", $yanit[0], $yanit[1]);
printf("  rapor %s, işin çalışma sayısı: %d\n", sinifla('rapor'), $sayac->oku('rapor'));

$zamanlayici->tik(60);
printf("tik (saat 60)\n");
printf("  rapor %s, işin çalışma sayısı: %d\n", sinifla('rapor'), $sayac->oku('rapor'));
```

```
istek (saat 0)  -> 202 kaydedildi
  rapor ertelendi, işin çalışma sayısı: 0
tik (saat 60)
  rapor koştu, işin çalışma sayısı: 1
```

İstekten hemen sonra `rapor` **ertelendi**: görev kaydedildi ama bu istekte çağrılmadı; sayaç 0'dır. Tikten sonra aynı parça **koştu**: görev yola girdi, zamanı gelmişti ve fonksiyon çağrıldı; sayaç 1'dir. Dersin ölçü sınıfı bu yüzden **ertelendi** olarak okunur: istek açısından iş yapılmadı, yalnızca yapılacağı kayda geçti. "Ertelendi" ile "koştu" arasında kalan süre, görevin sıklığından küçük olamaz; ilk çalışma bir aralık sonradır. Ölçüm, işin sonunda çalışıp çalışmadığını söylemez; yalnızca hangi turda çalıştığını söyler.

#### Karşıt: Zamanı Gelmemiş Görev Tikte Atlandı

Zamanlayıcının her tiki görevi çalıştırmaz. Sürücü aynı kayıttan sonra zamanlayıcıyı saat 30, 59, 60 ve 61'de tikler. Her tikten sonra `rapor` sınıfı ve toplam çalışma sayısı yazılır.

```php
<?php
// atlama.php — zamanı gelmemiş görev tikte atlanır
require_once 'zamanlayici.php';

$sayac = new Sayac();
$zamanlayici = new Zamanlayici();
(new RaporDenetleyicisi($zamanlayici, $sayac))->kaydet(0);

foreach ([30, 59, 60, 61] as $saat) {
    $zamanlayici->tik($saat);
    printf("tik (saat %2d)  rapor %-10s toplam: %d\n", $saat, sinifla('rapor'), $sayac->oku('rapor'));
}
```

```
tik (saat 30)  rapor atlandı   toplam: 0
tik (saat 59)  rapor atlandı   toplam: 0
tik (saat 60)  rapor koştu     toplam: 1
tik (saat 61)  rapor atlandı   toplam: 1
```

Saat 30'da ve 59'da görev yola girdi ama `sonraki` değerine ulaşmadığı için çağrılmadı: **atlandı**. Saat 60'ta zamanı geldi ve **koştu**. Saat 61'de görev yine yola girdi, ama `sonraki` değeri artık 120'dir ve bu tikte de **atlandı**. Toplam 1'de kaldı. İlk üç satır, dersin gözleminin karşıtıdır: aynı kod, aynı kayıt, ama tikin saati farklı olduğu için farklı sınıf.

Buradaki "atlandı" ile Dosya Depolama dersindeki "bağlanmadı" arasındaki fark, yolun varlığıdır. Burada görev her tikte yola girer; çünkü zamanlayıcı onu her seferinde değerlendirir. Atlanması, değerlendirilmediği anlamına gelmez; değerlendirildiği ve zamanı gelmediği anlamına gelir. Bu ayrım önemlidir: bir tikte görev yola girmemişse, kaydı yoktur ya da zamanlayıcı hiç çalışmamıştır.

#### Sınırlayıcı Ölçüm

Zamanlayıcı bir tiki kaçırırsa ne olur? `siklik.php` üç görevi, üç sıklık sözcüğüyle, iki saatlik bir tik akışında çalıştırır; akışı iki farklı tik aralığıyla iki kez kurar. Birinci akışta zamanlayıcı her 60 saniyede, ikincisinde her 300 saniyede tiklenir. Görevler kayıt anı 0'dan başlar.

```php
<?php
// siklik.php — üç sıklık sözcüğü, iki saatlik tik akışı, iki ayrı tik aralığı
require_once 'zamanlayici.php';

foreach ([60, 300] as $tikAraligi) {
    $sayac = new Sayac();
    $zamanlayici = new Zamanlayici();
    foreach (['her_dakika' => 'dakikalik', 'her_saat' => 'saatlik', 'her_gun' => 'gunluk'] as $siklik => $ad) {
        $zamanlayici->kaydet($ad, $siklik, fn() => $sayac->artir($ad), 0);
    }
    for ($saat = $tikAraligi; $saat <= 7200; $saat += $tikAraligi) {
        $zamanlayici->tik($saat);
    }
    printf("tik aralığı %3d sn: dakikalik %3d, saatlik %d, gunluk %d\n", $tikAraligi,
        $sayac->oku('dakikalik'), $sayac->oku('saatlik'), $sayac->oku('gunluk'));
}
```

```
tik aralığı  60 sn: dakikalik 120, saatlik 2, gunluk 0
tik aralığı 300 sn: dakikalik  24, saatlik 2, gunluk 0
```

Her 60 saniyede tiklenen zamanlayıcıda `dakikalik` görev 120 kez, `saatlik` 2 kez, `gunluk` hiç çalışmadı: iki saatte 120 dakika, 2 saat ve günün tamamlanmasına çok var. Her 300 saniyede tiklenen zamanlayıcıda `dakikalik` görev yalnız 24 kez çalıştı; bunun nedeni görevin her dakika değil, her tikte en çok bir kez çalışabilmesidir. İki saatte 24 tik vardır ve her tikte görev en fazla bir kez koşar. Kaçırılan 96 koşu telafi edilmedi: bu derste kaçırılan bir tiki geri alan bir mekanizma yoktur. `saatlik` görev iki akışta da 2 kez çalıştı, çünkü 300 saniye 3600'ü böler ve tikler saat başlarına denk gelir.

Bu ölçümün sonucu şudur: sıklık sözcüğü görevin en çok ne kadar sık koşabileceğini belirler; fiilen koşma sıklığını ise zamanlayıcının tik aralığı sınırlar. Ölçüm yalnız bu modeldeki sonucu verir. Başka bir zamanlayıcı kaçırılan koşuları sırayla telafi edebilir ya da bir tikte aynı görevi birden çok kez koşturabilir; o davranış burada ölçülmemiştir ve sürücü yalnızca bu dersin modeline aittir.

Tik aralığının öteki ucu da ölçülebilir: iki tik aynı anda gelirse ne olur? `cift.php` aynı kayıttan sonra zamanlayıcıyı iki kez saat 60'ta tikler. Birinci tikte görev koşar ve `sonraki` değeri 120'ye taşınır; ikinci tikte geçerli an hâlâ 60'tır ve 60, 120'den küçüktür.

```php
<?php
// cift.php — aynı anda iki tik: ikinci tik görevi yeniden koşturmaz
require_once 'zamanlayici.php';

$sayac = new Sayac();
$zamanlayici = new Zamanlayici();
(new RaporDenetleyicisi($zamanlayici, $sayac))->kaydet(0);

foreach ([1, 2] as $tur) {
    $zamanlayici->tik(60);
    printf("%d. tik (saat 60)  rapor %-10s toplam: %d\n", $tur, sinifla('rapor'), $sayac->oku('rapor'));
}
```

```
1. tik (saat 60)  rapor koştu     toplam: 1
2. tik (saat 60)  rapor atlandı   toplam: 1
```

İkinci tikte görev yola girdi ama çağrılmadı: **atlandı**, toplam 1'de kaldı. Aynı anda gelen çift tik görevi iki kez koşturmaz; çünkü çalışma anı görevin kendi `sonraki` değerinde tutulur, tik sayısında değil. Bu koruma yalnız tek bir zamanlayıcı nesnesi için geçerlidir: `sonraki` değeri o nesnenin içinde durur ve iki ayrı zamanlayıcı süreci aynı görevi aynı anda koşturabilir. US9 bu durumu kapsam dışı bıraktı ve bu bölümdeki ölçüm de onu kapsamaz.

#### Özet

- Bu dersin ölçü sınıfı **ertelendi**: istekte kaydedilen `rapor` görevi istek içinde çağrılmadı, sayaç 0 kaldı ve görev saat 60 tikinde koştu.
- Zamanlanmış görev bir sıklık sözcüğüyle (`her_dakika`, `her_saat`, `her_gun`) tanımlanır ve ilk çalışma kayıttan bir aralık sonradır.
- Karşıt: zamanı gelmemiş görev saat 30, 59 ve 61 tiklerinde yola girdi ama çağrılmadı ve **atlandı**; saat 60'ta **koştu**.
- Sınırlayıcı ölçümde 60 saniyelik tik akışı `dakikalik` görevi 120 kez, 300 saniyelik akış 24 kez koşturdu; kaçırılan koşular telafi edilmedi.
- Aynı anda gelen çift tik görevi iki kez koşturmadı; ikinci tikte görev **atlandı**.
- Görevin sıklığı bir hedeftir, bir garanti değildir; fiilen koşma sıklığını zamanlayıcının tik aralığı sınırlar.

#### Sonraki Adım

Zamanlanmış görevler, kullanıcının beklemediği işleri zamana bağlar. Ama zaman, bir uygulamanın yanıtını biçimlendiren tek değişken değildir. Bu dersin görevi bir kullanıcıya bildirim gönderiyor olsaydı, bildirimin hangi dilde yazılacağı da bir seçim olurdu. Peki istenen dilde bir metin yoksa ne olur: hata mı, boş bir yanıt mı, başka bir dilden bir metin mi? Yerelleştirme dersi dil dosyalarından, yer tutuculardan ve çoğul biçimlerden metin kurar ve istenen dilde bulunmayan anahtarın yanıta nasıl bağlandığını ölçer.

### Yerelleştirme

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/yerellestirme

Dil dizileri, yer tutucu, çoğul biçim ve yedek dil; istenen dilde anahtar yokken o dilin metninin bağlanmaması ve yedek dilin hatasız devreye girmesi, anahtar varken istenen dilin koşması ve yedek dilin eksik çeviriyi gizlemesi.

Zamanlanmış Görevler dersinde bir görevin ne zaman çalışacağını sorduk; çalışan görev bir kullanıcıya bildirim gönderiyorsa, bildirimin hangi dilde yazılacağı da bir seçimdir. Yanıtın metni koda gömülü olursa her dil için ayrı bir kod yolu gerekir. Metni kodun dışına, dile göre ayrılmış bir tabloya almak işi tersine çevirir: kod aynı kalır, değişen tablodur. Bu ders şunu sorar: **istenen dilde bir metin yoksa yanıt ne olur?**

**Yerelleştirme (localization)** bir uygulamanın metinlerini kullanıcının diline göre seçmektir. Metinler bir **dil dosyasında** durur: bu derste dil dosyası, anahtarları metne eşleyen bir PHP dizisidir. Metnin içindeki değişken kısımlar **yer tutucu (placeholder)** ile işaretlenir: `:ad` gibi bir parça, çağrı sırasında gelen değerle değiştirilir. Bir metnin sayıya göre değişen biçimlerine **çoğul biçim (plural form)** denir: "1 item" ve "3 items" aynı anahtarın iki biçimidir. İstenen dilde bir anahtar yoksa ikinci olarak bakılan dile **yedek dil (fallback language)** denir.

**US10.** Dil dosyası bir PHP dizisidir ve iki dil tanımlıdır: `tr` ve `en`. Yedek dil `tr`'dir ve tek düzeydir: istenen dilde anahtar yoksa yalnız yedek dile bakılır, yedeğin de yedeği yoktur. Anahtarlar düz adlardır; `genel.bilinmeyen` gibi bir ad nokta içerse de iç içe bir yapıyı ifade etmez. Dil kodu bölge parçası taşımaz, tarih, sayı ve para birimi biçimlendirmesi modellenmez, `en` dosyasında `indirim` anahtarı bilerek eksik bırakılmıştır.

**US11.** Çoğul biçim yalnızca iki durumu ayırır: sayı tam olarak 1 ise `bir`, değilse `cok`. Bu, İngilizce için doğrudur; çoğul kurallarının dilden dile çok daha karmaşık olduğu (sıfırın, ikinin ya da belli sayı sonlarının ayrı biçim alabildiği diller vardır) dersin modelinde yoktur. Türkçe sayıdan sonra ismi çoğullamaz, bu yüzden `tr` dosyasındaki iki biçim aynıdır. Yer tutucu `:ad` biçimindedir ve metindeki ilgili parça düz bir dize değişimiyle doldurulur; değer biçimlendirmesi ve kaçış modellenmez.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır. Bu derste bir parça bir metindir ve adı `dil/anahtar` biçimindedir: `en/indirim`, "İngilizce dosyadaki `indirim` metni bu istekte yanıta girdi mi" sorusunu ölçer. Metin kullanıldığında hem `yola()` hem `kostu()` çağrılır; kullanılmadığında hiçbiri çağrılmaz.

```php
<?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ı',
    };
}
```

#### Dil Dizileri, Yer Tutucu ve Yedek Dil

`yerellestirme.php` üç parça tanımlar. `Ceviri::al()` istenen dili ve yedek dili sırayla dener: ilk bulduğunda parçayı yola koyar, koştu olarak işaretler ve metni doldurur; ikisinde de bulamazsa anahtarın kendisini döndürür. `doldur()` metin bir diziyse sayıya göre `bir` ya da `cok` biçimini seçer, sonra yer tutucuları `strtr` ile değiştirir. `dilDizileri()` iki dil dosyasını verir. `SepetDenetleyicisi` ise bir dil, bir ad, bir sayı ve bir anahtar listesi alır, her anahtarı çevirir ve parçaları tek bir yanıta birleştirir.

Bu yapıda denetleyici hiçbir dilin metnini bilmez. Dil seçimi denetleyiciye bir dize olarak gelir ve çözümleme tamamen `Ceviri`'dedir. Yedek dil `Ceviri`'ye kurulurken verilir; yani yedeği değiştirmek denetleyiciyi değiştirmez.

```php
<?php
// yerellestirme.php — dil dizileri, yer tutucu, çoğul biçim ve yedek dil
declare(strict_types=1);
require_once 'kostu.php';

final class Ceviri {
    public function __construct(private array $diller, private string $yedek) {}

    public function al(string $dil, string $anahtar, array $degerler = []): string {
        foreach ([$dil, $this->yedek] as $aday) {
            if (!isset($this->diller[$aday][$anahtar])) {
                continue;
            }
            Gunluk::yola("$aday/$anahtar");
            Gunluk::kostu("$aday/$anahtar");
            return $this->doldur($this->diller[$aday][$anahtar], $degerler);
        }
        return $anahtar;
    }

    private function doldur(string|array $metin, array $degerler): string {
        if (is_array($metin)) {
            $metin = ($degerler['sayi'] ?? 0) === 1 ? $metin['bir'] : $metin['cok'];
        }
        $yerTutucular = [];
        foreach ($degerler as $ad => $deger) {
            $yerTutucular[':' . $ad] = (string) $deger;
        }
        return strtr($metin, $yerTutucular);
    }

    public function anahtarlar(string $dil): array { return array_keys($this->diller[$dil]); }
}

function dilDizileri(): array {
    return [
        'tr' => [
            'karsilama' => 'Merhaba, :ad',
            'sepet' => ['bir' => ':sayi ürün', 'cok' => ':sayi ürün'],
            'indirim' => 'Sepette indirim var',
        ],
        'en' => [
            'karsilama' => 'Hello, :ad',
            'sepet' => ['bir' => ':sayi item', 'cok' => ':sayi items'],
        ],
    ];
}

final class SepetDenetleyicisi {
    public function __construct(private Ceviri $ceviri) {}
    public function goster(string $dil, string $ad, int $sayi, array $anahtarlar): array {
        Gunluk::yeniIstek();
        $satirlar = [];
        foreach ($anahtarlar as $anahtar) {
            $satirlar[] = $this->ceviri->al($dil, $anahtar, ['ad' => $ad, 'sayi' => $sayi]);
        }
        return [200, implode(' | ', $satirlar)];
    }
}
```

#### İstenen Dilin Metni Bağlanmadı

Sürücü `en` dilinde yalnız `indirim` anahtarını ister. `en` dosyasında bu anahtar bilerek yoktur. Yanıttan sonra iki parçanın sınıfı yazılır: İngilizce dosyadaki `indirim` metni ve Türkçe dosyadaki aynı metin.

```php
<?php
// yedekdil.php — istenen dilde anahtar yok: yedek dil devreye girer
require_once 'yerellestirme.php';

$denetleyici = new SepetDenetleyicisi(new Ceviri(dilDizileri(), 'tr'));
$yanit = $denetleyici->goster('en', 'Ada', 3, ['indirim']);
printf("goster(dil: en, indirim) -> %d %s\n", $yanit[0], $yanit[1]);
foreach (['en/indirim', 'tr/indirim'] as $parca) {
    printf("  %-11s %s\n", $parca, sinifla($parca));
}
```

```
goster(dil: en, indirim) -> 200 Sepette indirim var
  en/indirim  bağlanmadı
  tr/indirim  koştu
```

İstek hata vermedi; 200 döndü ve yanıt Türkçe metni taşıdı. `en/indirim` **bağlanmadı**: İngilizce dosyada böyle bir metin yoktur, bu yüzden yola hiç girmedi. `tr/indirim` ise **koştu**: yedek dil devreye girdi ve metni yanıta soktu. Dersin ölçü sınıfı bu yüzden **bağlanmadı** olarak okunur, ama bu bir hata değil, bir yoldur: kullanıcı, istediği dilde olmayan bir metin yerine başka bir dilde bir metin görür ve uygulama çalışmaya devam eder.

Burada "atlandı" demek yanlış olurdu. Atlanan bir parça yola konmuş ama çağrılmamış bir parçadır; `en/indirim` ise yola hiç konmadı, çünkü çözümleme dosyada bulamadığı bir metni yola koymaz. Metin bağlanamadı, koşulun sonucuyla geçilmedi.

#### Karşıt: Anahtar Varken İstenen Dil Koştu

Aynı denetleyiciye bu kez iki dilde de bulunan `karsilama` ve `sepet` anahtarları verilir. Sürücü üç istek yapar: `en` dilinde bir ürünle, `en` dilinde üç ürünle ve `tr` dilinde üç ürünle. Her istekten sonra iki dildeki `karsilama` metninin sınıfı yazılır.

```php
<?php
// anahtarvar.php — anahtar istenen dilde varken o dilin metni koşar
require_once 'yerellestirme.php';

$denetleyici = new SepetDenetleyicisi(new Ceviri(dilDizileri(), 'tr'));
foreach ([['en', 1], ['en', 3], ['tr', 3]] as [$dil, $sayi]) {
    $yanit = $denetleyici->goster($dil, 'Ada', $sayi, ['karsilama', 'sepet']);
    printf("goster(dil: %s, sayi: %d) -> %d %s\n", $dil, $sayi, $yanit[0], $yanit[1]);
    foreach (['en/karsilama', 'tr/karsilama'] as $parca) {
        printf("  %-13s %s\n", $parca, sinifla($parca));
    }
}
```

```
goster(dil: en, sayi: 1) -> 200 Hello, Ada | 1 item
  en/karsilama  koştu
  tr/karsilama  bağlanmadı
goster(dil: en, sayi: 3) -> 200 Hello, Ada | 3 items
  en/karsilama  koştu
  tr/karsilama  bağlanmadı
goster(dil: tr, sayi: 3) -> 200 Merhaba, Ada | 3 ürün
  en/karsilama  bağlanmadı
  tr/karsilama  koştu
```

Anahtar istenen dilde olduğunda istenen dilin metni **koştu**, yedek dilinki ise **bağlanmadı**. Yani `indirim` örneğinin tersi: yedek dil çözümlemeye hiç girmedi, çünkü ilk denenen dil yeterliydi. Yanıtlar iki şeyi birlikte gösterir. Çoğul biçim sayıya göre seçilir: `en` dilinde 1 ürün `1 item`, 3 ürün `3 items` oldu; `tr` dilinde ise `3 ürün`, çünkü Türkçe dosyada iki biçim aynıdır. Yer tutucu da her istekte doldu: `:ad` her dilde `Ada` oldu, `:sayi` ise çağrıdan gelen sayıyı aldı.

İki dilin sınıfları birbirinin aynasıdır: bir dil koşunca öbürü bağlanmaz. Bu, yedek dilin yalnızca istenen dil başarısız olduğunda devreye girdiği anlamına gelir.

#### Sınırlayıcı Ölçüm

Yedek dil kullanıcıyı hatadan korur, ama aynı zamanda eksik çeviriyi gizler. `eksikdil.php` önce Türkçe dosyadaki bütün anahtarları `en` diliyle ister ve hangilerinin İngilizce dosyada bağlanmadığını, yani yedekten geldiğini listeler. Sonra hiçbir dilde bulunmayan bir anahtar ister.

```php
<?php
// eksikdil.php — yedek dil eksik çeviriyi gizler; hiçbir dilde olmayan anahtar adıyla döner
require_once 'yerellestirme.php';

$ceviri = new Ceviri(dilDizileri(), 'tr');
$denetleyici = new SepetDenetleyicisi($ceviri);
$anahtarlar = $ceviri->anahtarlar('tr');
$denetleyici->goster('en', 'Ada', 2, $anahtarlar);
$eksik = array_filter($anahtarlar, fn(string $k) => sinifla("en/$k") === 'bağlanmadı');
printf("en isteği: %d anahtar, yedekten gelen: %s\n", count($anahtarlar), implode(', ', $eksik));

$yanit = $denetleyici->goster('en', 'Ada', 2, ['genel.bilinmeyen']);
printf("hiçbir dilde yok -> %d %s\n", $yanit[0], $yanit[1]);
foreach (['en/genel.bilinmeyen', 'tr/genel.bilinmeyen'] as $parca) {
    printf("  %-19s %s\n", $parca, sinifla($parca));
}
```

```
en isteği: 3 anahtar, yedekten gelen: indirim
hiçbir dilde yok -> 200 genel.bilinmeyen
  en/genel.bilinmeyen bağlanmadı
  tr/genel.bilinmeyen bağlanmadı
```

İlk satır, 200 yanıtının arkasında bir çeviri borcu bulunduğunu gösterir: üç anahtarın biri yedekten geldi. Yanıt dışarıdan tam görünür; eksik ancak ölçümle, yani hangi parçanın bağlanmadığına bakarak fark edilir. Bu bir test ya da denetim kurallarının konusudur: yedek dil kullanıcı deneyimini korur, ama bir çevirinin eksik olduğunu söylemez.

İkinci kısım yedeğin de yetmediği durumu gösterir. Anahtar iki dilde de olmadığı için `Ceviri` anahtarın adını döndürdü; yanıt yine 200'dür ve iki parça da **bağlanmadı**. Kullanıcı yanıtta `genel.bilinmeyen` yazısını görür. Bu modelde hata atılmaz; başka bir uygulama hata atmayı ya da boş metin döndürmeyi seçebilir. Ölçüm bu seçimlerin hangisinin doğru olduğunu söylemez, yalnız bu modelde ne olduğunu gösterir.

#### Özet

- Bu dersin ölçü sınıfı **bağlanmadı**: `en` dilinde `indirim` anahtarı yoktu, `en/indirim` yola girmedi, yedek dil `tr/indirim` metnini hatasız yanıta soktu.
- Dil dosyası bir PHP dizisidir; metin yer tutucuyla (`:ad`) ve sayıya göre `bir` ya da `cok` biçimiyle kurulur.
- Karşıt: anahtar istenen dilde varken o dilin metni **koştu** ve yedek dilinki **bağlanmadı**; çoğul biçim `1 item` ve `3 items` olarak ayrıldı.
- Sınırlayıcı ölçümde yedek dil eksik çeviriyi gizledi: üç anahtarın biri yedekten geldi, yanıt yine 200 kaldı.
- Hiçbir dilde olmayan anahtar adıyla döndü ve iki dilin metni de **bağlanmadı**.

#### Sonraki Adım

Bu kursta her derste bir parçanın koşup koşmadığını ölçtük: kimlik doğrulama, kuyruk, olay, bildirim, önbellek, dosya, zamanlayıcı ve dil. Her biri farklı bir yoldan geldi ve her birinin kendine ait bir karşıtı vardı. Peki bu ölçümleri bir sürücü ve elle okunan bir çıktı yerine, her değişiklikten sonra kendiliğinden doğrulayan bir şeye çevirebilir miyiz? Kursun son dersi, bunların hepsinin bir istekten yanıta ve tek tek birimler düzeyinde nasıl doğrulanacağını sorar.

### Test

Source: https://academia.sh/tr/kurslar/web-uygulama-cercevesi/test

Özellik testi ve birim testi, küçük bir doğrulama fonksiyonu; sahte kuyrukta işin kaydedilip koşmaması, kuyruk boşaltılınca işin koşması ve sahte kuyruğun işin gövdesini ölçmemesi.

Yerelleştirme dersinde yedek dil, eksik bir çeviriyi hatasız bir yanıtın arkasına sakladı. Önceki derslerde de aynı şey yaşandı: kuyruğa konan iş yanıttan sonra koşar, yanıt ise işi beklemeden döner. Peki testin içinde, gerçek bir işçi yokken, işin koştuğunu nasıl bilebiliriz? Bu ders şunu sorar: **bir test işin kaydedildiğini doğruluyorsa, işin koştuğunu da doğrulamış olur mu?**

**Özellik testi (feature test)** bir isteği uygulamaya verir ve yanıtı, yanında bırakılan izleri doğrular. **Birim testi (unit test)** tek bir sınıfı ya da fonksiyonu, çevresinden ayırarak sınar. **Sahte nesne (fake)** gerçek bir bağımlılığın yerine konan ve yaptıklarını kaydeden basit bir benzeridir: bu derste sahte kuyruk işi çalıştırmaz, yalnız saklar.

**US12.** Test kitaplığı kullanılmaz. Ders, `Test` sınıfında iki fonksiyonlu bir doğrulayıcı kurar: `dogrula()` koşul yanlışsa bir hata fırlatır, `calistir()` bir testi çalıştırıp geçti ya da kaldı olarak yazar. Eşitlik, dizi karşılaştırması, ayrıntılı fark çıktısı ve test keşfi yoktur; her test elle çağrılır.

**US13.** Kuyruk gerçek değildir: `SahteKuyruk` işi bir diziye koyar ve `isle()` çağrılana kadar koşturmaz. `koy()` işi `ertele()` ile işaretler, `isle()` ise her işi `kostu()` ile işaretleyip çalıştırır. Gerçek bir sürücü, yeniden deneme, gecikme ve hata yönetimi modellenmez. İş küçük bir hesaptır: genişliği dörde bölen `KucukResimIsi` ve bilerek yanlış çalışan `HataliKucukResimIsi`.

#### Ölçüm Çekirdeği

Kurs boyunca her derste aynı ölçüm kullanılır. `kostu.php` beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. `Gunluk` üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. `sinifla()` bunlara sırayla bakar. **Karar sırası `sinifla()`'nın kendisidir:** koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır. Bu derste ölçülen parça `kucukresim` işidir: iş kaydedilince `yola()` ve `ertele()`, kuyruk boşaltılınca `kostu()` çağrılır.

```php
<?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ı',
    };
}
```

#### Sınanan Kod

`uygulama.php` sınanan her şeyi tutar: `Is` arayüzü, iki iş, `SahteKuyruk` ve `YuklemeDenetleyicisi`. Denetleyici bir genişlik alır, işi kuyruğa koyar ve 201 ile döner. Hangi iş sınıfının kullanılacağı kurucuya verilir; varsayılan doğru olandır. `test.php` ise doğrulayıcıdır.

```php
<?php
// uygulama.php — sınanan kod: iş arayüzü, sahte kuyruk ve yükleme denetleyicisi
declare(strict_types=1);
require_once 'kostu.php';

interface Is {
    public function ad(): string;
    public function calistir(): int;
}

final class KucukResimIsi implements Is {
    public function __construct(private int $genislik) {}
    public function ad(): string { return 'kucukresim'; }
    public function calistir(): int { return intdiv($this->genislik, 4); }
}

final class HataliKucukResimIsi implements Is {
    public function __construct(private int $genislik) {}
    public function ad(): string { return 'kucukresim'; }
    public function calistir(): int { return $this->genislik * 4; }   // yanlış yön
}

final class SahteKuyruk {
    public array $isler = [];
    public function koy(Is $is): void {
        $this->isler[] = $is;
        Gunluk::ertele($is->ad());
    }
    public function isle(): array {
        $sonuclar = [];
        foreach ($this->isler as $is) {
            Gunluk::kostu($is->ad());
            $sonuclar[] = $is->calistir();
        }
        $this->isler = [];
        return $sonuclar;
    }
}

final class YuklemeDenetleyicisi {
    public function __construct(private SahteKuyruk $kuyruk, private string $isSinifi = KucukResimIsi::class) {}
    public function yukle(int $genislik): array {
        Gunluk::yeniIstek();
        Gunluk::yola('kucukresim');
        $this->kuyruk->koy(new ($this->isSinifi)($genislik));
        return [201, 'yuklendi'];
    }
}
```

```php
<?php
// test.php — kitaplık yok: iki fonksiyonlu küçük bir doğrulayıcı
declare(strict_types=1);

final class Test {
    public static int $gecen = 0;
    public static int $kalan = 0;

    public static function dogrula(bool $kosul, string $mesaj): void {
        if (!$kosul) {
            throw new RuntimeException($mesaj);
        }
    }

    public static function calistir(string $ad, callable $test): void {
        try {
            $test();
            self::$gecen++;
            $durum = 'gecti';
        } catch (RuntimeException $hata) {
            self::$kalan++;
            $durum = 'kaldi — ' . $hata->getMessage();
        }
        printf("%-28s %s\n", $ad, $durum);
    }

    public static function ozet(): void {
        printf("toplam: %d gecti, %d kaldi\n", self::$gecen, self::$kalan);
    }
}
```

#### Özellik Testi: İş Kaydedildi, Koşmadı

`ozellik.php` isteği denetleyiciye verir ve üç şeyi doğrular: yanıt 201 mü, iş kuyruğa girdi mi, iş yanıt sırasında koşmadı mı. Son satır `kucukresim` parçasının sınıfını yazar.

```php
<?php
// ozellik.php — özellik testi: istekten yanıta, sahte kuyrukla
require_once 'uygulama.php';
require_once 'test.php';

$kuyruk = new SahteKuyruk();
$yanit = (new YuklemeDenetleyicisi($kuyruk))->yukle(800);

Test::calistir('yanit 201 doner', fn() => Test::dogrula($yanit[0] === 201, "durum {$yanit[0]}"));
Test::calistir('is kuyruga girdi', fn() => Test::dogrula(count($kuyruk->isler) === 1, 'kuyruk bos'));
Test::calistir('is yanitta calismadi', fn() => Test::dogrula(sinifla('kucukresim') === 'ertelendi', 'sinif ' . sinifla('kucukresim')));
Test::ozet();
printf("kucukresim: %s\n", sinifla('kucukresim'));
```

```
yanit 201 doner              gecti
is kuyruga girdi             gecti
is yanitta calismadi         gecti
toplam: 3 gecti, 0 kaldi
kucukresim: ertelendi
```

Üç test geçti ve iş **ertelendi**: kaydedildi, ama koşmadı. Testin doğruladığı şey tam olarak budur. "İş kuyruğa girdi" cümlesi doğru ve "iş koştu" cümlesi bu testte hiç denenmedi. Sahte kuyruk işi çalıştırmadığı için özellik testi, işin ne yaptığı hakkında bir şey söyleyemez.

#### Karşıt: Kuyruk Boşaltılınca İş Koştu

`birim.php` aynı isteği yapar, sonra `isle()` ile kuyruğu boşaltır. Bu kez `kucukresim` **koştu**. İşin sonucu da doğrulanır: genişlik 800 iken sonuç 200 olmalıdır. Son test işi kuyruktan bağımsız, doğrudan sınar; birim testi budur.

```php
<?php
// birim.php — karşıt: kuyruk isle() ile boşaltılınca iş koşar; birim testi işi doğrudan sınar
require_once 'uygulama.php';
require_once 'test.php';

$kuyruk = new SahteKuyruk();
(new YuklemeDenetleyicisi($kuyruk))->yukle(800);
$sonuclar = $kuyruk->isle();

Test::calistir('isle() isi calistirdi', fn() => Test::dogrula(sinifla('kucukresim') === 'koştu', 'sinif ' . sinifla('kucukresim')));
Test::calistir('kucuk resim genisligi', fn() => Test::dogrula($sonuclar === [200], 'sonuc ' . json_encode($sonuclar)));
Test::calistir('is tek basina', fn() => Test::dogrula((new KucukResimIsi(100))->calistir() === 25, 'sonuc yanlis'));
Test::ozet();
printf("kucukresim: %s\n", sinifla('kucukresim'));
```

```
isle() isi calistirdi        gecti
kucuk resim genisligi        gecti
is tek basina                gecti
toplam: 3 gecti, 0 kaldi
kucukresim: koştu
```

Aynı iş, aynı denetleyici, aynı istek: yalnız `isle()` çağrıldığında sınıf ertelendiden koştuya geçti. Yani sınıfı değiştiren isteğin kendisi değil, kuyruğun boşaltılmasıdır. Üçüncü test kuyruğa hiç dokunmaz: `KucukResimIsi` doğrudan kurulur, `calistir()` çağrılır ve sonuç 25 olarak okunur.

#### Sınırlayıcı Ölçüm

Sahte kuyruk, işin gövdesini ölçmez. `sinir.php` denetleyiciye bilerek yanlış çalışan `HataliKucukResimIsi` verir. Önce aynı özellik testi, sonra aynı işin birim testi koşar.

```php
<?php
// sinir.php — sahte kuyruk işin gövdesini ölçmez: hatalı iş özellik testini geçer
require_once 'uygulama.php';
require_once 'test.php';

$kuyruk = new SahteKuyruk();
$yanit = (new YuklemeDenetleyicisi($kuyruk, HataliKucukResimIsi::class))->yukle(800);

echo "özellik testi\n";
Test::calistir('yanit 201 doner', fn() => Test::dogrula($yanit[0] === 201, "durum {$yanit[0]}"));
Test::calistir('is kuyruga girdi', fn() => Test::dogrula(count($kuyruk->isler) === 1, 'kuyruk bos'));
Test::calistir('is yanitta calismadi', fn() => Test::dogrula(sinifla('kucukresim') === 'ertelendi', 'sinif yanlis'));

echo "birim testi\n";
$sonuc = (new HataliKucukResimIsi(800))->calistir();
Test::calistir('kucuk resim genisligi', fn() => Test::dogrula($sonuc === 200, "beklenen 200, gelen $sonuc"));
Test::ozet();
```

```
özellik testi
yanit 201 doner              gecti
is kuyruga girdi             gecti
is yanitta calismadi         gecti
birim testi
kucuk resim genisligi        kaldi — beklenen 200, gelen 3200
toplam: 3 gecti, 1 kaldi
```

Özellik testinin üç doğrulaması hatalı işle de geçti, çünkü hiçbiri işin gövdesine bakmıyor: genişlik 800 iken iş 3200 döndürüyor, ama sahte kuyruk onu hiç çalıştırmadı. Hatayı yalnız birim testi yakaladı. Bir özellik testi yolun bağlandığını doğrular; yolun sonunda doğru işin yapıldığını doğrulamaz. İkisi birbirinin yerine geçmez.

Bu ayrımın bir bedeli de vardır. İşi gerçekten koşturan bir özellik testi, kuyruğu `isle()` ile boşaltmak zorunda kalır ve iş artık yanıttan sonra değil, testin içinde koşar. Böyle bir test ertelemenin kendisini ölçmez, yalnız işin sonucunu ölçer. Hangi sorunun sorulduğuna göre ikisinden biri seçilir.

#### Özet

- Bu dersin ölçü sınıfı **ertelendi**: sahte kuyrukta `kucukresim` işi kaydedildi (`ertele()`), ama özellik testinde koşmadı.
- Test kitaplığı olmadan iki fonksiyonlu bir doğrulayıcı (`dogrula()` ve `calistir()`) özellik ve birim testlerini yazmaya yetti.
- Karşıt: `isle()` çağrılınca aynı iş **koştu**; sınıfı değiştiren istek değil, kuyruğun boşaltılmasıdır.
- Sınırlayıcı ölçümde bilerek yanlış çalışan iş özellik testini geçti, birim testi 800 yerine 3200 sonucunu yakaladı.
- Özellik testi yolun bağlandığını, birim testi işin doğru yaptığını doğrular; biri ötekinin yerini tutmaz.

#### Kurs Kapanışı

Bu kursta 21 derste bir parçanın bir istekte koşup koşmadığını ölçtük: çerçevenin temelleri, görünümler ve veri, uygulama hizmetleri. Her ders kendi ölçü sınıfını ve bir karşıt ölçümünü taşıdı. Aşağıdaki tablo hepsini tek yerde toplar; sınıf ve karşıt hücreleri kaynak dersin özetinden alınmıştır.

| Ders | Ölçülen parça | Sınıf | Karşıt ölçüm |
|---|---|---|---|
| Neden Çerçeve | `kaydet` eylemi, `onbellek` dinleyicisi | koştu | Çekirdek 4 istek 12 parça: 4 koştu, 3 atlandı, 3 ertelendi, 2 bağlanmadı |
| İstek–Yanıt Yaşam Döngüsü | `yanittanSonra()` ile bırakılan iş | ertelendi | Aynı iş eylemde doğrudan çağrılınca koştu |
| Yönlendirme | Gölgelenen `form` rotası | bağlanmadı | Sıra değişince ya da `{id:\d+}` ile daraltılınca `form` koştu |
| Denetleyiciler | Eylem, bağımlılık çözüldü | koştu | Tanımsız `duzenle` eylemi 404, parça bağlanmadı |
| Ara Katmanlar | `dogrulama`, `kaydet` | atlandı | Oturumlu istekte dört parça da koştu |
| Nesne Kabı ve Cepheler | Ezilen `DosyaYazici` kaydı | bağlanmadı | Tek kayıtlı kapta `DosyaYazici` koştu |
| Şablon Motoru | Kaçışlama adımı | koştu | Ham çıktı sözdiziminde kaçışlama bağlanmadı |
| Form ve Doğrulama | Kayıt adımı, geçersiz girdi 422 | atlandı | Geçerli girdide kayıt koştu, 201 |
| Nesne ve Tablo Bağlaması | Kaydetme kancası | koştu | İzin listesinde olmayan alan bağlanmadı |
| İlişkiler | Tembel yüklemede ilişki sorgusu | ertelendi | İstekli yüklemede ilişki listeyle birlikte koştu |
| Göçler ve Tohumlama | Kayıtlı göç, ikinci çalıştırma | atlandı | Yeni eklenen göç koştu |
| Sayfalama ve Kaynak Dönüşümü | Modele eklenen kolonun yanıta ulaşması | bağlanmadı | Dönüşüm tanımına eklenen kolon koştu |
| Kimlik Doğrulama ve Yetkilendirme | `duzenle` eylemi, kapı 403 | atlandı | Yazının sahibinde `duzenle` koştu, 200 |
| Kuyruklar ve İşler | Küçük resim işi | ertelendi | Eşzamanlı sürücüde iş koştu |
| Olaylar ve Dinleyiciler | `dizin` dinleyicisi, ad uyuşmadı | bağlanmadı | Ad uyuşunca dinleyici koştu |
| Bildirimler | Seçilen posta ve veritabanı kanalları | koştu | Seçilmeyen kanallar bağlanmadı |
| Önbellekleme | `hesapla`, ikinci istekte isabet | atlandı | Geçersiz kılmadan sonra `hesapla` yeniden koştu |
| Dosya Depolama | `yukle()` ve `yaz()`, iki disk | koştu | Adı geçmeyen `uzak` disk bağlanmadı, 500 |
| Zamanlanmış Görevler | `rapor` görevi | ertelendi | Saat 60'ta koştu, 30/59/61'de atlandı |
| Yerelleştirme | `en/indirim` metni | bağlanmadı | Anahtar varken istenen dil koştu |
| Test | Sahte kuyruktaki iş | ertelendi | `isle()` çağrılınca iş koştu |

Derslerin dağılımı 6 koştu, 5 ertelendi, 5 atlandı, 5 bağlanmadı olarak okunur. Bu, 21 dersin her birinin kendi ölçü sınıfını sayar. Neden Çerçeve dersindeki çekirdeğin dağılımı ise ayrı bir şeyi sayar: o dersin 12 beklenen parçasından 4 koştu, 3 ertelendi, 3 atlandı, 2 bağlanmadı; yani 4 istekte 4 parça koştu, 8 parça koşmadı.

Tablodaki dağılım, bir uygulamada beklenen çoğu parçanın bir istekte koşmadığını gösterir; yanıt yine de 200 ya da 201 olabilir. Hangi sınıfın hangi yoldan geldiğini bilmek, bir test yazarken neyi doğruladığını bilmeye yarar. Kursun bıraktığı soru, bu uygulamanın kalitesinin ölçülmesidir: kodun biçimi, tekrarı ve hata sınıfları gibi başka bir konu alanı.

