---
title: 'Web ile PHP: Oturum, Veri ve Güvenlik'
source: 'https://academia.sh/tr/kurslar/php-web'
language: tr
updated: '2026-10-21T05:03:36+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Bir sonucun sunucuya, ortama, istemciye ya da üçüncü bir tarafa ait olduğunu her dış taraftan tek bir sapma uygulayarak ölçer'
    - 'Oturum ve çerez tabanlı durumu imza, oturum kimliği yenileme ve çerez nitelikleriyle güvenli kurar'
    - 'Veritabanına hazır deyim ve parametreli sorgularla erişir, işlem ve göçle veri bütünlüğünü korur'
    - 'Girdi doğrulama, çıktı kaçışlama, sahtecilik belirteci ve parola karmasıyla yaygın web açıklarına savunma uygular'
    - 'Bir savunmanın sonucu dış bir taraftan sunucuya taşıdığını önce/sonra koşumuyla gösterir'
---

# Web ile PHP: Oturum, Veri ve Güvenlik

Bir web uygulamasının ürettiği her sonucun bir sahibi vardır. Bu sonucu kim değiştirebiliyor? Kimi sonucu yalnız sunucudaki kod belirler, kimi dağıtıldığı ortamla değişir, kimi isteği gönderen istemcinin elindedir, kimi uygulamanın çağırdığı ya da onu çağıran bir üçüncü tarafa bağlıdır. Kurs çerezleri, oturumları, yapılandırmayı, veritabanı erişimini, işlemleri, göçleri, veri biçimlerini, HTTP istemcisini ve yaygın web açıklarına karşı savunmaları tek bir soruyla ölçer — bu sonucu kim değiştirebiliyor?

## Durum ve Oturum

Çerezler, oturumlar, yapılandırma ve ortam değişkenleri — istekler arasında taşınan durumun hangisinin istemcide, hangisinin sunucuda, hangisinin dağıtım ortamında durduğunu belirleyen düzenekler.

### Çerezler

Source: https://academia.sh/tr/kurslar/php-web/cerezler

İstekler arasında durumu istemcide taşıyan çerezin Set-Cookie başlığı ve imzalı biçimi; bir sonucu kimin değiştirebildiğini sınıflayan dört taraf ve on sonuçlu ölçüm.

Nesneye Dayalı PHP ve Standartlar kursu kodu birden çok dosyaya ve yazara böldü ve bir tanım değişince kullananın ne yaşadığını ölçtü. Bu kurs kodu web'e çıkarır: istek bir tarayıcıdan gelir, ayar dağıtıldığı ortamdan, fiyat bir dış servisten. Kodu yazan artık sonucun tek sahibi değildir. Bu kurs her düzeneği tek soruyla ölçer: **bu sonucu kim değiştirebiliyor?**

İlk durak, web'in durumu nerede taşıdığıdır. HTTP durumsuzdur: sunucu her isteği öncekinden bağımsız işler (HTTP Ailesi dersleri). Bir sepetin ya da bir kullanıcının istekler arasında hatırlanması için durum bir yerde durmak zorundadır. En eski yer **çerez**dir (cookie): sunucu yanıtta bir `Set-Cookie` başlığı gönderir, istemci değeri saklar ve aynı siteye giden her istekte geri yollar. Durum istemcide durur ve her istekle sunucuya döner.

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

Ölçümün kendisi `taraf.php` dosyasında durur ve kurs boyunca değişmez. Her derste aynı dosya yeniden verilir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

`DIS_TARAFLAR` kodun dışındaki üç tarafı karar sırasıyla sayar: **istemci**, **üçüncü taraf**, **ortam**. `sina()` bir sonucu hesaplayan fonksiyonu alır, onu önce sapmasız çağırır (`$taban`), sonra her dış tarafın sapmasıyla ayrı ayrı çağırır ve sonucun tabandan farklı olup olmadığını kaydeder. `taraf()` bu kayıtlara sırayla bakar ve değiştirebilen ilk dış tarafı verir. `tarafRaporu()` sonucu adlandırır, sapmaları ve sonucu basar.

**DO1.** Karar sırası `taraf()`'ın kendisidir: önce istemci, sonra üçüncü taraf, sonra ortam; hiçbiri sonucu değiştiremediyse **sunucu**. Sıra saldırı yüzeyinin genişliğidir. İstemci isteği gönderen herkestir, üçüncü taraf yalnız uygulamanın bağlandığı taraflardır, ortam yalnız dağıtımı yapanın elindedir. Sunucu sapma listesinde yoktur, çünkü kod her sonucu değiştirebilir; sorulan, kodun dışında kimin değiştirebildiğidir. Taraf bir etiket değil bir kuraldır: geliştirici sonucu adlandırır, her ilgili dış taraftan tek bir sapma tanımlar ve taraf bu iki girdiden çıkar.

| Taraf | Tanım |
|---|---|
| **sunucu** | Sonucu yalnız uygulamanın kodu belirler; hiçbir dış taraf tek başına değiştiremez |
| **ortam** | Aynı kod, dağıtıldığı ortam değişince (ortam değişkeni, bağlantı ayarı, çalışma zamanı sürümü, veritabanının durumu) farklı sonuç verir |
| **istemci** | İsteği gönderen taraf (çerez, form alanı, sorgu parametresi, sunduğu belirteç) sonucu değiştirir |
| **üçüncü taraf** | Uygulamanın çağırdığı bir servis ya da kullanıcının tarayıcısına istek gönderten başka bir site sonucu değiştirir |

İki ayrım baştan yapılmalıdır. Nesneye Dayalı PHP ve Standartlar kursunda bir tanım değişikliği öncesi ve sonrasıyla ölçüldü; burada tanım sabittir, değişen dış bir tarafın girdisidir. C ile Sistem Programlama kursunda bir değişikliği gören taraf, gözlemci arandı; burada yön terstir ve değişikliği yapan taraf aranır.

#### On Sonuç, Dört Taraf

`sonuclar.php` kursun on sonucunu bir kez ölçer. Bu dosya yalnız bu derste yazılır; sonraki dersler `taraf.php`'yi çeker ve yalnız kendi sonucunu kurar.

```php
<?php
// sonuclar.php — on sonuç, dört taraf
declare(strict_types=1);
require 'taraf.php';

putenv('UYG_VERITABANI=uygulama.db');
function ortamda(?string $deger, callable $f): mixed {
    if ($deger === null) return $f();
    $eski = getenv('UYG_VERITABANI');
    putenv("UYG_VERITABANI=$deger");
    try { return $f(); } finally { putenv("UYG_VERITABANI=$eski"); }
}

$db = new PDO('sqlite::memory:');
$db->exec('CREATE TABLE uyeler (ad TEXT, yas INTEGER)');
$db->exec("INSERT INTO uyeler VALUES ('ayse', 30), ('mehmet', 41)");
function hazirDeyim(PDO $db, string $ad): string {
    $deyim = $db->prepare('SELECT yas FROM uyeler WHERE ad = ?');
    $deyim->execute([$ad]);
    return $deyim->queryString;
}

const BELIRTEC_UZUNLUGU = 16;
$belirtec = bin2hex(random_bytes(BELIRTEC_UZUNLUGU));
function epostaIsle(array $istekler, ?string $belirtec): string {
    $eposta = 'ayse@ornek.test';
    foreach ($istekler as $istek) {
        if ($belirtec !== null && !hash_equals($belirtec, $istek['belirtec'] ?? '')) continue;
        $eposta = $istek['eposta'];
    }
    return $eposta;
}
$kullaniciIstegi = ['koken' => 'https://ornek.test', 'belirtec' => $belirtec, 'eposta' => 'ayse@yeni.test'];
$baskaSiteden = ['koken' => 'https://baska.test', 'eposta' => 'bilinmeyen@baska.test'];

$cerez = ['sepet' => '2'];
$oturum = ['sepet' => 2];
$servisYaniti = '{"fiyat": 120}';
$calismaVarsayilani = ['cost' => 12];   // PHP 8.4 ve sonrası bcrypt varsayılanı

$sonuclar = [
    ['sepet (çerezde)', fn(array $s) => (int) ($s['istemci'] ?? $cerez)['sepet'],
        ['istemci' => ['sepet' => '5']]],
    ['sepet (oturumda)', fn(array $s) => $oturum['sepet'],
        ['istemci' => ['sepet' => '5']]],
    ['sorgu metni (birleştirme)', fn(array $s) => "SELECT yas FROM uyeler WHERE ad = '" . ($s['istemci'] ?? 'ayse') . "'",
        ['istemci' => 'mehmet']],
    ['sorgu metni (hazır deyim)', fn(array $s) => hazirDeyim($db, $s['istemci'] ?? 'ayse'),
        ['istemci' => 'mehmet']],
    ['e-posta (belirteçsiz)', fn(array $s) => epostaIsle([$kullaniciIstegi, ...($s['üçüncü taraf'] ?? [])], null),
        ['üçüncü taraf' => [$baskaSiteden]]],
    ['e-posta (belirteçli)', fn(array $s) => epostaIsle([$kullaniciIstegi, ...($s['üçüncü taraf'] ?? [])], $belirtec),
        ['üçüncü taraf' => [$baskaSiteden]]],
    ['fiyat (dış servis)', fn(array $s) => json_decode($s['üçüncü taraf'] ?? $servisYaniti, true, 4, JSON_THROW_ON_ERROR)['fiyat'],
        ['üçüncü taraf' => '{"fiyat": 125}']],
    ['karma öneki (varsayılan)', fn(array $s) => substr(password_hash('parola', PASSWORD_BCRYPT, $s['ortam'] ?? $calismaVarsayilani), 0, 7),
        ['ortam' => ['cost' => 10]]],
    ['karma öneki (açık maliyet)', fn(array $s) => substr(password_hash('parola', PASSWORD_BCRYPT, ['cost' => 12]), 0, 7),
        ['ortam' => ['cost' => 10]]],
    ['veritabanı adı', fn(array $s) => ortamda($s['ortam'] ?? null, fn() => getenv('UYG_VERITABANI')),
        ['ortam' => 'deneme.db']],
];

$sayac = ['sunucu' => 0, 'ortam' => 0, 'istemci' => 0, 'üçüncü taraf' => 0];
foreach ($sonuclar as [$ad, $sonuc, $sapmalar]) {
    $sayac[tarafRaporu($ad, $sonuc, $sapmalar)]++;
}
foreach ($sayac as $t => $n) echo "$t $n\n";
```

```
sepet (çerezde) → istemci
    istemci değiştirdi
sepet (oturumda) → sunucu
    istemci değiştiremedi
sorgu metni (birleştirme) → istemci
    istemci değiştirdi
sorgu metni (hazır deyim) → sunucu
    istemci değiştiremedi
e-posta (belirteçsiz) → üçüncü taraf
    üçüncü taraf değiştirdi
e-posta (belirteçli) → sunucu
    üçüncü taraf değiştiremedi
fiyat (dış servis) → üçüncü taraf
    üçüncü taraf değiştirdi
karma öneki (varsayılan) → ortam
    ortam değiştirdi
karma öneki (açık maliyet) → sunucu
    ortam değiştiremedi
veritabanı adı → ortam
    ortam değiştirdi
sunucu 4
ortam 2
istemci 2
üçüncü taraf 2
```

**DO2.** On sonucun **4'ü sunucu, 2'si ortam, 2'si istemci, 2'si üçüncü taraf** çıktı. Bu dağılım kursun bağlayıcı sayısıdır; sonraki derslerin hiçbiri onu değiştirmez, her ders kendi kesitini ölçer. Çıktının okunuşu çiftlerden başlar. On sonucun sekizi dört çift oluşturur ve her çiftte bir düzenek sonucu bir dış taraftan sunucuya taşır:

- Sepet çerezde durduğunda istemcinin gönderdiği `5` sepeti değiştirdi; oturumda durduğunda aynı sapma değiştiremedi.
- Sorgu metni birleştirmeyle kurulduğunda nötr bir ad (`mehmet`) metni değiştirdi; hazır deyimde deyim metni aynı kaldı.
- Belirteçsiz e-posta güncellemesinde başka bir siteden gelen istek e-postayı değiştirdi; belirteçli güncellemede `hash_equals()` o isteği geçirmedi.
- Karma öneki ortamın sağladığı varsayılan maliyetle üretildiğinde ortam öneki değiştirdi; maliyet açıkça verildiğinde değiştiremedi.

İki sonucun sahibi hiçbir düzenekle değişmez: dış servisin fiyatı her zaman üçüncü tarafındır ve veritabanı adı her zaman ortamındır. Bu ikisinde savunma sahibi değiştirmez, sonucu yalnız sınırlar.

#### Set-Cookie Başlığı

Çerezin sahibi kimse onu bir başlıkla kurar. Komut satırında başlık listesi boştur, bu yüzden aşağıda başlık gönderilmez, bir dizge olarak kurulur ve basılır. İmzalı çerez için iki küçük fonksiyon ayrı bir dosyada durur.

```php
<?php
// cerez.php — imzalı çerez değerini üreten ve doğrulayan iki fonksiyon
declare(strict_types=1);

const IMZA_ANAHTARI = 'ders-anahtari';

function imzala(string $deger): string
{
    return $deger . '.' . hash_hmac('sha256', $deger, IMZA_ANAHTARI);
}

function cerezDegeri(string $cerez, string $varsayilan): string
{
    [$deger, $imza] = array_pad(explode('.', $cerez, 2), 2, '');
    $beklenen = hash_hmac('sha256', $deger, IMZA_ANAHTARI);
    return hash_equals($beklenen, $imza) ? $deger : $varsayilan;
}
```

`imzala()` değeri `hash_hmac('sha256', …)` ile anahtarlı bir karmaya (imza) bağlar ve `değer.imza` biçiminde birleştirir. `cerezDegeri()` istemciden gelen çerezi böler, imzayı yeniden hesaplar ve `hash_equals()` ile karşılaştırır; imza tutmazsa varsayılan değeri verir. `IMZA_ANAHTARI` bir ders sabitidir.

```php
<?php
// baslik.php — Set-Cookie başlığı ve imzalı sepet çerezinin ölçümü
declare(strict_types=1);

require 'taraf.php';
require 'cerez.php';

$gecerli = imzala('2');
$baslik = 'Set-Cookie: sepet=' . $gecerli . '; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax';
echo str_replace(substr($gecerli, 2), '<imza>', $baslik), "\n";

$degistirilmis = '5' . substr($gecerli, 1);
echo 'değiştirilmiş değer imzadan geçti mi: ',
    cerezDegeri($degistirilmis, '0') === '5' ? 'evet' : 'hayır', "\n";

tarafRaporu(
    'sepet (imzalı çerez, değiştirilmiş değer)',
    fn(array $s) => cerezDegeri($s['istemci'] ?? $gecerli, '0'),
    ['istemci' => $degistirilmis],
);
```

```
Set-Cookie: sepet=2.<imza>; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax
değiştirilmiş değer imzadan geçti mi: hayır
sepet (imzalı çerez, değiştirilmiş değer) → istemci
    istemci değiştirdi
```

**DO3.** Başlıktaki nitelikler tarayıcıya istektir. `Path=/` çerezin hangi yollarda geri döneceğini, `Max-Age=3600` ne kadar saklanacağını söyler. `HttpOnly` sayfadaki betiğin çerezi okumasını, `Secure` çerezin yalnız şifreli bağlantıda gönderilmesini, `SameSite=Lax` çerezin başka bir sitenin isteğiyle birlikte gönderilmesini sınırlamayı ister. Üçü de sunucunun yazdığı bir dizgedir; uygulanması istemcidedir ve sunucu bunu bu derste ölçemez.

Ölçüm iki şey gösterir. Değiştirilmiş değer imzadan geçmedi ve `cerezDegeri()` varsayılana, yani `0`'a düştü. İmza çalıştı: sunucu değiştirilen değeri kabul etmedi. Ama sonuç yine değişti: `2` iken `0` oldu ve değişikliği yapan istemciydi. Taraf **istemci**dir.

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

İmzanın sınırı şudur: imza bir değerin anahtarla üretildiğini gösterir, değerin güncel olduğunu göstermez. İstemci elinde daha önce aldığı geçerli bir çerezi, kendi önceki sepetini geri gönderirse imza geçer mi?

```php
<?php
// eski.php — istemci önceki geçerli çerezini geri gönderirse
declare(strict_types=1);

require 'taraf.php';
require 'cerez.php';

$guncel = imzala('2');
$eski = imzala('1');
echo 'eski çerez imzadan geçti mi: ',
    cerezDegeri($eski, '0') === '1' ? 'evet' : 'hayır', "\n";

tarafRaporu(
    'sepet (imzalı çerez, eski geçerli değer)',
    fn(array $s) => cerezDegeri($s['istemci'] ?? $guncel, '0'),
    ['istemci' => $eski],
);
```

```
eski çerez imzadan geçti mi: evet
sepet (imzalı çerez, eski geçerli değer) → istemci
    istemci değiştirdi
```

**DO4.** Geçti. İmza değerin anahtarla üretildiğini kanıtlar, değerin güncel olduğunu kanıtlamaz: eski çerez geçerli bir imza taşıdığı için sunucu onu kabul etti ve sepet `2` yerine `1` oldu. Sonuç yine **istemci**dir. İmza istemcinin üretebileceği değerleri sunucunun önceden imzaladıklarına daraltır; hangisini göndereceğini seçmek istemcide kalır.

Savunma her zaman sahibi değiştirmez. Bu derste imzalı çerez, istemcinin ürettiği bir değeri geçerli kümeye daraltır; sonucu istemcinin elinden almaz. Sahibi değiştirmek için durumun istemcide durmaması gerekir.

#### Özet

- Çerez, HTTP'nin durumsuzluğunu durumu istemcide tutarak aşar; sunucu `Set-Cookie` ile yazar, istemci her istekte geri gönderir.
- Kurs her düzeneği tek soruyla ölçer: bu sonucu kim değiştirebiliyor. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremezse sunucu.
- Çekirdek on sonucun 4'ünü sunucu, 2'sini ortam, 2'sini istemci, 2'sini üçüncü taraf olarak sınıfladı; bu, çekirdeğin dağılımıdır, derslerin dağılımı değil.
- `Set-Cookie` nitelikleri tarayıcıya istektir; imzalı çerez değiştirilen değeri fark eder ama sonucu sunucuya taşımaz.
- Bu dersin ölçü sınıfı **istemci**'dir: imzalı çerezde de sonucun sahibi, değeri gönderen istemcidir.

#### Sonraki Adım

Çerezde sepet istemcide durdu ve istemci onu değiştirebildi. Sonraki ders, Oturumlar, değeri sunucuya taşıyor: sepet `$_SESSION`'da duruyorsa aynı sapma sonucu değiştirebiliyor mu ve çerezde yalnızca bir kimlik kaldığında o kimliği kim belirliyor?

### Oturumlar

Source: https://academia.sh/tr/kurslar/php-web/oturumlar

Durumu sunucuda tutan oturum düzeneği: oturum çerezindeki kimlik, $_SESSION, session_regenerate_id(true) ve kimliği taşıyan tarafın sınırı.

Çerezler dersi sepeti istemcide bıraktı ve istemci onu `2`'den `5`'e çevirebildi. İmza bu çevirmeyi fark etti ama sonucun sahibini değiştirmedi. Bu ders aynı değeri sunucuya taşır: istemcinin elinde yalnız bir kimlik kalır, sepetin kendisi sunucuda durur.

Düzeneğin adı **oturum**dur (session). `session_start()` çağrısı iki iş yapar. İstekte bir **oturum kimliği** (session id) taşıyan çerez varsa sunucudaki o kimliğe ait kaydı açar ve içeriğini `$_SESSION` dizisine yükler. Yoksa yeni bir kimlik üretir, boş bir kayıt açar ve kimliği yanıtta bir çerezle, yani **oturum çerezi** (session cookie) olarak yollar. İstemci sonraki isteklerde bu çerezi geri gönderir. Çerezin taşıdığı şey sepet değil, kaydın adıdır.

Kimlik bir tutamaçtır. C'de Bellek ve İşaretçiler kursunda bir işaretçi başka bir yerde duran veriyi gösterir ve gösterdiği şeyin yaşamını kodu yazan yönetir. Oturum kimliği de sunucudaki bir durumu gösteren tutamaçtır; fark, tutamacı elinde taşıyanın kodu yazan değil, istemci olmasıdır. Bu farkın ölçüm için ne anlama geldiği dersin sonunda görülür.

#### Sepet Sunucuda

Ölçüm Çerezler dersindeki aynı çekirdekle yapılır. `taraf.php` bir önceki dersten birebir gelir ve değişmez; burada yalnız yeniden verilir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Yeni dosya `oturum.php`'dir. İlk bölüm oturumu kurar. Dört ayar açıkça verilir, çünkü ders çalışma zamanının varsayılanına güvenmez. `session.use_strict_mode` sunucunun tanımadığı bir kimliği kabul etmemesini ister; varsayılanı kapalıdır. `session.cookie_samesite` oturum çerezinin başka bir sitenin isteğiyle gönderilmesini sınırlar; varsayılanı boştur. `session.cookie_httponly` çerezi sayfadaki betikten saklar. `session.save_path` kayıtların yazıldığı yeri geçici dizine bağlar. `ob_start()` çıktıyı tamponlar, çünkü oturum başlıkları ilk çıktıdan önce gönderilmek zorundadır ve tampon bu sırayı koşum boyunca korur.

```php
<?php
// oturum.php — sepet oturumda durur; istemcinin çerezi sonucu değiştiremez
declare(strict_types=1);

require 'taraf.php';

ob_start();
ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1');
ini_set('session.save_path', sys_get_temp_dir());
session_start();
$_SESSION['sepet'] = 2;

$onceki = session_id();
session_regenerate_id(true);
echo 'kimlik yenilendi: ', session_id() !== $onceki ? 'farklı' : 'aynı', "\n";
echo 'sepet korundu: ', $_SESSION['sepet'], "\n";

tarafRaporu(
    'sepet (oturumda)',
    function (array $s): int {
        if (isset($s['istemci'])) {
            $_COOKIE['sepet'] = $s['istemci'];
        }
        return $_SESSION['sepet'];
    },
    ['istemci' => '5'],
);
ob_end_flush();
```

```
kimlik yenilendi: farklı
sepet korundu: 2
sepet (oturumda) → sunucu
    istemci değiştiremedi
```

Sonuç bölümü iki işi bir arada gösterir. İlk iki satır girişte yapılan kimlik yenilemesidir; sonraki iki satır ölçümdür. Ölçümde istemci `sepet=5` gönderir ve sapma bunu `$_COOKIE`'ye yazar. Sepeti okuyan kod ise `$_SESSION['sepet']`'e bakar. Çerez değişir, sonuç değişmez.

**DO5.** Karar sırası istemci, üçüncü taraf, ortam, sunucudur. Bu ölçümün karşıtı Çerezler dersindeki ilk satırdır: orada aynı sapma aynı sepeti `2`'den `5`'e çevirdi ve sonuç **istemci** çıktı. Burada aynı sapma sonucu değiştiremedi ve sonuç **sunucu**dur. Fark değerin taşındığı yerdir: sepet çerezdeyken isteği gönderen sepetin sahibiydi, sepet sunucudaki kayıtta durunca onun elinde yalnız kaydın adı kalır.

#### Kaydın Ömrü ve Bedeli

Sunucuda durmanın iki bedeli vardır. Birincisi ömürdür. Kayıt, kullanıcı çıkış yapınca ya da süresi dolunca silinmelidir; silinmezse sunucuda kimsenin kullanmadığı kayıtlar birikir. Kayıt silindiğinde istemcinin elindeki kimlik geçersiz kalır ve `session.use_strict_mode` açıkken sunucu onu kabul etmez, yeni bir kimlik üretir. Kimliğin istemcide kalması, kaydın sunucuda yaşamasını garanti etmez; kaydın yaşadığına sunucu karar verir.

İkincisi ölçeklemedir. Kayıtlar tek bir makinenin geçici dizininde duruyorsa, aynı kullanıcının iki isteği iki farklı makineye düştüğünde ikinci makine kaydı bulamaz. Bu yüzden birden çok makine çalışan bir uygulamada kayıtlar paylaşılan bir depoya yazılır. Bu bir yapılandırma kararıdır ve bir sonraki dersin konusuna yaklaşır: depoyu kodun içine yazmak yerine ortamın seçmesi gerekir.

Sunucuda durmak bir şeyi daha değiştirir: sepetin doğruluğu artık istemcinin dürüstlüğüne bağlı değildir. Çerezde `5` gelen bir sepeti sunucunun gerçekten `5` olduğunu doğrulaması gerekirdi; oturumda doğrulanacak bir şey yoktur, çünkü değeri sunucu yazdı. Doğrulama yükü kalkmaz, yer değiştirir: kimliğin geçerliliği, kaydın var olup olmadığına indirgenir ve bunu `session_start()` kendi başına yapar.

#### Kimliği Yenilemek

Kimlik girişten önce ve sonra aynı kalırsa kimliği bilen biri giriş yapmış oturumu da bilir. Buna **oturum sabitleme** (session fixation) denir. Düzenek basittir: kullanıcı doğrulandığı anda `session_regenerate_id(true)` çağrılır. Fonksiyon yeni bir kimlik üretir, `true` argümanı eski kimliğe ait kaydı siler ve `$_SESSION` içeriği yeni kayda taşınır.

Ölçüm bunu iki satırda gösterir: kimlik yenilendikten sonra öncekinden **farklı**dır ve `sepet` `2` olarak korunur. Kimliğin kendisi basılmaz, çünkü her koşumda değişir; ölçülen şey farktır. Sabitleme saldırısı bu derste kurulmaz. Düzeneğin işi o saldırının dayandığı varsayımı, giriş öncesi ve sonrası kimliğin aynı kalmasını ortadan kaldırmaktır.

**DO6.** `session_regenerate_id(true)` sunucunun kendi kaydını güvenceye alan bir önlemdir ve taraf cümlesini değiştirmez. Kimliği üreten de, yenileyen de, eski kaydı silen de sunucudur. Ama kimliği yanıtta çerezle istemciye geri yollamak yine istemcide durmak zorundadır; bu iki durumun birleşimi bir sonraki bölümün konusudur.

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

Oturumun sahibi sunucu olarak sınıflandı. Bu cümlenin bir sınırı var: oturum kimliğinin kendisi istemcidedir. İstemci çerezde başka bir kimlik sunarsa ne olur? Aşağıdaki dosya önce bir oturum açıp sepeti `2` yapar, sonra aynı sepeti iki kimlikle okur: sunucunun verdiği kimlik ve istemcinin sunduğu başka bir kimlik. Başka kimlik, sunucunun hiç üretmediği nötr bir dizgedir.

```php
<?php
// baska.php — istemci başka bir oturum kimliği sunarsa hangi oturuma düşer
declare(strict_types=1);

require 'taraf.php';

ob_start();
ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1');
ini_set('session.save_path', sys_get_temp_dir());

function sepetOku(string $kimlik): int
{
    session_id($kimlik);
    session_start();
    $sepet = $_SESSION['sepet'] ?? 0;
    session_write_close();
    return $sepet;
}

session_start();
$_SESSION['sepet'] = 2;
$kimlik = session_id();
session_write_close();

tarafRaporu(
    'sepet (oturumda, istemci başka kimlik sunar)',
    fn(array $s) => sepetOku($s['istemci'] ?? $kimlik),
    ['istemci' => 'baskakimlik0123456789'],
);
ob_end_flush();
```

```
sepet (oturumda, istemci başka kimlik sunar) → istemci
    istemci değiştirdi
```

**DO7.** Sonuç değişti ve değiştiren **istemci**dir. Sunucu tanımadığı kimliği kabul etmedi (`session.use_strict_mode`) ve yeni, boş bir kayıt açtı; sepet `2` yerine `0` okundu. İstemci kendi sepetine başka bir kimlikle değil, hiçbir sepeti olmayan bir oturuma düştü.

Ölçümün ikinci bir okuması da vardır: sapma sunucunun kaydını değiştirmedi. Sunucudaki oturum `2` olarak kaldı, değişen hangi oturumun açıldığıdır. Karar sırası bu yüzden tek bir ölçümde iki cümleye bölünür: sepetin içeriğini istemci belirleyemez, hangi sepetin açılacağını istemci belirler.

#### Özet

- Oturum, durumu sunucuda tutar. İstemcide kalan yalnız bir kimliktir ve oturum çerezi bunu her istekle geri taşır.
- `session_start()` kimlik varsa kaydı açıp `$_SESSION`'a yükler, yoksa yeni bir kimlik ve boş kayıt üretir.
- Aynı `sepet=5` sapması Çerezler dersinde sonucu değiştirdi; oturumda değiştiremedi. Bu dersin ölçü sınıfı **sunucu**dur: sepetin içeriğini istemci belirleyemez.
- `session_regenerate_id(true)` girişte kimliği yeniler, eski kaydı siler ve `$_SESSION` içeriğini korur; sabitlemeye karşı düzenek budur.
- Sınır: kimliği taşıyan istemcidir. İstemci başka bir kimlik sunarsa kendi sepetine değil başka bir oturuma düşer, yani hangi oturumun açılacağı istemcide kalır.

#### Sonraki Adım

Çerez de, oturum da istekle gelen veriyi ölçtü. Bir uygulamanın okuduğu her değer istekten gelmez: veritabanı adı, anahtar ve bağlantı ayarları dağıtım ortamından gelir. Sonraki ders, Yapılandırma ve Ortam Değişkenleri, bu değerleri koddan ayırıp ortama bırakır ve kimin değiştirebildiğini ölçer.

### Yapılandırma ve Ortam Değişkenleri

Source: https://academia.sh/tr/kurslar/php-web/yapilandirma-ve-ortam-degiskenleri

Bağlantı adı ve anahtar gibi dağıtıma bağlı değerleri koddan ayırıp ortam değişkenlerinden okumak; eksik değişkende açık hata ve kodda sabit yazılan değerin bedeli.

Çerezler dersinde sepet istemcide, Oturumlar dersinde sunucuda durdu. İkisinde de değeri belirleyen istekti. Bir uygulamanın okuduğu her değer istekten gelmez: hangi veritabanına bağlanacağı, imza anahtarı, bir dış servisin adresi uygulama dağıtılırken belirlenir. Aynı kod geliştiricinin makinesinde bir değerle, üretimde başka bir değerle koşar.

Bu değerleri taşıyan en yaygın yol **ortam değişkeni**dir (environment variable): işletim sisteminin bir sürece verdiği ad ve değer çiftleri. PHP'de `getenv()` okur, `putenv()` yazar. Dağıtımı yapan kişi ya da araç değişkeni süreç başlamadan önce kurar; uygulama kodu onu normalde yalnız okur. Üretim Go: Test, Araçlar ve Başarım kursunda sorulan soru bir bilginin hangi araçtan okunduğuydu. Burada soru, değeri kimin değiştirebildiğidir.

#### Yapılandırma Katmanı

Ortamdan okuma kodun her yerine dağıtılmaz; tek bir yerde toplanır. `yapilandirma.php` üç fonksiyon verir ve ölçümün de dayanağı budur.

```php
<?php
// yapilandirma.php — ortam değişkenlerinden yapılandırma okuyan yardımcılar
declare(strict_types=1);

function ortam(string $ad, ?string $varsayilan = null): string
{
    $deger = getenv($ad);
    if ($deger !== false) {
        return $deger;
    }
    if ($varsayilan !== null) {
        return $varsayilan;
    }
    throw new RuntimeException("eksik ortam değişkeni: $ad");
}

function yapilandirma(): array
{
    return [
        'veritabani' => ortam('UYG_VERITABANI', 'uygulama.db'),
        'anahtar' => ortam('UYG_ANAHTAR'),
    ];
}

/** Değişkenleri geçici olarak kurar, işi yapar, eski değerleri geri koyar. */
function ortamla(array $degiskenler, callable $is): mixed
{
    $eskiler = [];
    foreach ($degiskenler as $ad => $deger) {
        $eskiler[$ad] = getenv($ad);
        putenv("$ad=$deger");
    }
    try {
        return $is();
    } finally {
        foreach ($eskiler as $ad => $eski) {
            putenv($eski === false ? $ad : "$ad=$eski");
        }
    }
}
```

`ortam()` değişkeni `getenv()` ile okur. Değişken yoksa `getenv()` `false` döner; fonksiyon önce varsayılan değere bakar, o da verilmediyse `RuntimeException` fırlatır. Varsayılan ile hata arasındaki ayrım bilinçlidir: veritabanı adı için makul bir varsayılan vardır, anahtar için yoktur. Anahtar eksikse sessizce boş bir değerle ilerlemek, imzasız bir uygulamayı yayına almak olurdu; uygulama açılırken durmalıdır.

`yapilandirma()` değerleri bir dizide toplar. Kodun geri kalanı `getenv()` bilmez, bu diziyi ister. `ortamla()` ölçüm yardımcısıdır: değişkenleri geçici kurar, işi yapar ve eski değerleri geri koyar.

Taraf çekirdeği bu derste de aynıdır ve değişmez.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

#### Aynı Kod, Farklı Ortam

`ortam.php` önce iki değişkeni siler, böylece ders çalışma ortamının ne taşıdığına güvenmez. Sonra eksik anahtarla `yapilandirma()` çağırır ve hatayı yakalar. Hata sınıfının adı ve iletisi basılır; iletiyi bu kod yazdığı için ortamdan bağımsızdır. Sonra anahtarı kurar ve okundu bilgisini basar. Anahtarın kendisi basılmaz: gizli değer günlüğe, çıktıya ya da hata iletisine girmez.

```php
<?php
// ortam.php — aynı kod, farklı ortam, farklı bağlantı adı
declare(strict_types=1);

require 'taraf.php';
require 'yapilandirma.php';

putenv('UYG_VERITABANI');
putenv('UYG_ANAHTAR');

try {
    yapilandirma();
} catch (RuntimeException $hata) {
    echo get_class($hata), ': ', $hata->getMessage(), "\n";
}

putenv('UYG_ANAHTAR=ders-gizli-deger');
$ayar = yapilandirma();
echo 'anahtar okundu: ', $ayar['anahtar'] !== '' ? 'evet' : 'hayır', "\n";

tarafRaporu(
    'veritabanı adı (ortam değişkeninden)',
    fn(array $s) => ortamla($s['ortam'] ?? [], fn() => yapilandirma()['veritabani']),
    ['ortam' => ['UYG_VERITABANI' => 'deneme.db']],
);
```

```
RuntimeException: eksik ortam değişkeni: UYG_ANAHTAR
anahtar okundu: evet
veritabanı adı (ortam değişkeninden) → ortam
    ortam değiştirdi
```

Son ölçüm sorunun cevabıdır. Dağıtımı yapan taraf `UYG_VERITABANI` değişkenini `deneme.db` yaptığında, hiçbir satırı değişmeyen kod farklı bir ad döndürür.

**DO8.** Karar sırası istemci, üçüncü taraf, ortam, sunucudur. Burada istemcinin ve üçüncü tarafın sapması tanımlı değildir, çünkü değeri ne isteğin göndericisi ne bir dış servis verir; tek sapma ortamındır ve sonucu değiştirdi. Taraf **ortam**dır. Ortamın yüzeyi en dardır: değişkenin değerini dağıtımı yapan verir; çalışan kod `putenv()` ile yalnız kendi sürecinin ortamını değiştirebilir, ölçümdeki sapma da böyle uygulanır. Bu darlık güvenliği değil, sorumluluğu belirler. Yanlış bir değerin sahibi artık kodu yazan değil dağıtımı yapandır.

**DO9.** Gizli değerler bu yüzden koda yazılmaz, ortamdan okunur. Koddaki bir anahtar depoya girer, depoyu okuyan herkesin eline geçer ve değişmesi için kodun yeniden dağıtılması gerekir. Ortamdaki anahtar depoda yoktur ve dağıtımı yapanın elinde döndürülebilir. Bu bir sızıntıyı imkânsız yapmaz; yalnız sızıntı yüzeyini kod deposundan dağıtım katmanına çeker.

#### Varsayılanın Sınırı

Varsayılan değer bir kolaylıktır ve bir riski de taşır. `UYG_VERITABANI` unutulursa uygulama `uygulama.db` ile açılır ve hiçbir şey olmamış gibi çalışır. Geliştirme makinesinde bu istenen davranıştır. Üretimde ise yanlış veritabanına yazmak, açılmayı reddetmekten daha pahalıdır. Bu yüzden varsayılan yalnız yanlış değerin zararsız olduğu değişkenlere verilir; zararlı olabilecek her değişken `ortam()`'ın ikinci argümanını boş bırakır ve eksikliği bir hataya çevirir.

Hatanın iletisi de bir tasarım kararıdır. İleti değişkenin adını söyler, değerini söylemez. Adı söylemek dağıtımı yapana neyin eksik olduğunu gösterir; değeri söylemek, değer gizliyse, günlüğe sızdırmak olur. Ölçümün ilk satırı bu iletiyi basar ve yalnız ad görünür.

Bir değişkenin kimin elinde olduğu da yapılandırmanın parçasıdır. Ortam değişkenini süreci başlatan kurar; çalışan süreç `putenv()` ile yalnız kendi ortamını değiştirebilir, başka bir sürecin ortamını değiştiremez. Bu, istemciden gelen bir değerle ortamdan gelen değer arasındaki asıl farktır: istemcinin değeri her istekle yeniden gelir ve her istekte farklı olabilir, ortamın değeri süreç başlarken gelir ve kod onu açıkça değiştirmedikçe süreç boyunca aynı kalır.

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

Yapılandırmanın alternatifi değeri koda yazmaktır. Aynı veritabanı adı bir sabit olarak kodda durursa ortam onu değiştirebilir mi?

```php
<?php
// sabit.php — kodda sabit yazılan değeri ortam değiştiremez
declare(strict_types=1);

require 'taraf.php';
require 'yapilandirma.php';

const SABIT_VERITABANI = 'uygulama.db';

function sabitVeritabani(): string
{
    return SABIT_VERITABANI;
}

tarafRaporu(
    'veritabanı adı (kodda sabit)',
    fn(array $s) => ortamla($s['ortam'] ?? [], fn() => sabitVeritabani()),
    ['ortam' => ['UYG_VERITABANI' => 'deneme.db']],
);
```

```
veritabanı adı (kodda sabit) → sunucu
    ortam değiştiremedi
```

**DO10.** Değiştiremedi ve sonuç **sunucu**dur. Sabit, ortamın sapmasını görmezden geldi: `UYG_VERITABANI` `deneme.db` olsa da kod `uygulama.db` döndürdü. Aynı ad, aynı kodda, her ortamda aynı kalır.

Bu ölçüm sabitin savunulabilir olduğunu söylemez. Sabit ortamın sapmasına karşı sağlamdır ama değeri değiştirmek kodu değiştirmek ve yeniden dağıtmak demektir. Geliştirici makinesinde ve üretimde aynı ad kullanılamıyorsa, ortamlar arasında taşınan şey kodun kendisidir ve her ortam için ayrı bir sürüm gerekir. Sabit sunucuya döner, ama bedeli taşınabilirliktir.

#### Özet

- Dağıtıma bağlı değerler (bağlantı adı, anahtar) koda yazılmaz; ortam değişkeninden okunur ve tek bir yapılandırma katmanında toplanır.
- Varsayılan değer makul bir değer varsa verilir; anahtar gibi vazgeçilmez bir değer eksikse `RuntimeException` ile uygulama açılırken durur.
- Gizli değer çıktıya, günlüğe ya da hata iletisine basılmaz.
- Aynı kod, `UYG_VERITABANI` değişince farklı ad döndürür. Bu dersin ölçü sınıfı **ortam**dır: değeri kodu yazan değil dağıtımı yapan belirler.
- Sınır: kodda sabit yazılan değer sunucuda kalır ve ortam onu değiştiremez, ama ortamlar arasında taşınamaz.

#### Sonraki Adım

Üç ders, durumun nerede durduğunu ve onu kimin değiştirebildiğini ölçtü: çerezde istemci, oturumda sunucu, yapılandırmada ortam. Sonraki konu veri erişimine geçer ve aynı soruyu veritabanına sorar: okunan bir değerin tipini kod mu belirliyor, yoksa kodun çalıştığı ortam mı?

## Veri Erişimi

Veritabanı erişim katmanı, işlemler, göç yönetimi, nesne–tablo bağlamasının sorguları, veri biçimleri ve HTTP istemcisi — uygulamanın veriyi okuduğu ve yazdığı her noktada sonucu kodun mu, ortamın mı, dış bir tarafın mı belirlediği.

### Veritabanı Erişim Katmanı

Source: https://academia.sh/tr/kurslar/php-web/veritabani-erisim-katmani

PDO ile bağlantı, hata kipi, hazır deyim, adlı ve konumsal yer tutucular ve fetch kipleri kurulur; bağlantı ayarı ortamdan gelirse dönen değerin tipini kimin değiştirebildiği ölçülür.

Önceki derste ayarın koddan değil ortamdan geldiği durumu ölçtük: aynı kod, farklı bir dağıtımda farklı davranabiliyordu. Veritabanı bağlantısı bu durumun en sık görüldüğü yerdir. Bağlantı ayarları kodun dışında tutulur ve dağıtımı yapan taraf onları değiştirebilir. Soru şudur: bağlantı ayarı ortamdan gelirse veritabanı ne tip döndürüyor?

Bu dersin ölçü ekseni, bir sonucu kimin değiştirebildiğidir. Kodun dışında üç taraf vardır ve karar sırası sabittir: önce istemci, sonra üçüncü taraf, sonra ortam. Hiçbiri sonucu değiştiremiyorsa sonuç sunucuya aittir. Sıra saldırı yüzeyinin genişliğini izler: istemci her isteği gönderen herkestir, üçüncü taraf yalnız bağlanılan taraflardır, ortam ise yalnız dağıtımı yapanın elindedir. Bu derste ağırlıklı taraf ortamdır. Çekirdek aşağıdaki dosyadır; her derste aynen verilir ve değiştirilmez.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

**VE1.** Veritabanı bellekteki bir veritabanıdır ve bağlantı her ölçümde yeniden kurulur; bağlantı kapanınca veri yok olur, bu yüzden her ölçüm aynı iki üyeyle başlar. **VE2.** Ortamın sapması bir bağlantı ayarı olarak modellenir: ayar dizisini dağıtımı yapan taraf verir, kod kendisi yazmaz. **VE3.** İstemci yalnız üye adını gönderir; sapması `ayse` yerine `mehmet` göndermesidir. **VE4.** Sonuç değer değil, `puan` sütununun PHP'deki tipidir.

#### Bağlantı ve Hazır Deyim

Veri erişim katmanı, uygulamanın veritabanıyla konuştuğu tek yerdir; PHP'de bu işi PDO (PHP Data Objects) üstlenir. Katmanın ne işe yaradığı veritabanı derslerinde anlatıldı; burada yalnızca PDO'nun onu nasıl somutlaştırdığına bakılır. Aşağıdaki dosya bağlantıyı kurar ve iki üyelik bir tablo hazırlar. Çıktısı yoktur, yalnız diske yazılır; sonraki bloklar onu `require` ile çeker.

```php
<?php
// baglanti.php — bellekteki veritabanına bağlanır, üye tablosunu kurar
declare(strict_types=1);

function baglan(array $ayarlar = []): PDO {
    $pdo = new PDO('sqlite::memory:', options: $ayarlar);
    $pdo->exec('CREATE TABLE uyeler (id INTEGER PRIMARY KEY, ad TEXT, puan INTEGER)');
    $pdo->exec("INSERT INTO uyeler (ad, puan) VALUES ('ayse', 7), ('mehmet', 3)");
    return $pdo;
}

/** Üyenin puanının PHP'de hangi tipte döndüğünü söyler. */
function puanTipi(string $ad, array $ayarlar, bool $acikDonusum): string {
    $pdo = baglan($ayarlar);
    $hazir = $pdo->prepare('SELECT puan FROM uyeler WHERE ad = :ad');
    $hazir->execute(['ad' => $ad]);
    $puan = $hazir->fetchColumn();
    return get_debug_type($acikDonusum ? (int) $puan : $puan);
}
```

Hata kipi (error mode), PDO'nun bir hatayı nasıl bildireceğini belirler. Üç seçenek vardır: sessiz kip hatayı yalnızca bir dönüş değeri ve hata kodu olarak bırakır, uyarı kipi bir uyarı üretir, istisna kipi bir `PDOException` fırlatır. PHP 8.0'dan beri varsayılan istisna kipidir; bu, bir sorgunun başarısız olmasının akışı kesmesi ve gözden kaçmaması demektir.

Hazır deyim (prepared statement), sorgu metnini ve değerleri iki ayrı adımda gönderir. `prepare()` sorgu metnini hazırlar; `execute()` değerleri yer tutucuların yerine bağlar. Yer tutucu iki biçimde yazılır: adlı yer tutucu (`:alt`) değeri adıyla, konumsal yer tutucu (`?`) sırasıyla eşler. Değer hiçbir zaman sorgu metninin içine yazılmaz. Bu ayrımın girdi güvenliğine ne kattığı Güvenlik konusunun işidir; burada yalnızca mekanizma kurulur.

Fetch kipi (fetch mode), satırın PHP'ye hangi biçimde döneceğini seçer. `FETCH_ASSOC` sütun adlarıyla bir dizi, `FETCH_NUM` sıra numaralarıyla bir dizi, `FETCH_COLUMN` yalnız ilk sütunun değerlerini verir.

```php
<?php
// veritabani.php — hazır deyim, yer tutucular ve fetch kipleri
declare(strict_types=1);
require 'baglanti.php';

$pdo = baglan();
echo 'hata kipi istisna: ', var_export($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION, true), "\n";

$adli = $pdo->prepare('SELECT ad, puan FROM uyeler WHERE puan > :alt');
$adli->execute(['alt' => 2]);
echo 'adlı, FETCH_ASSOC: ', json_encode($adli->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE), "\n";

$konumsal = $pdo->prepare('SELECT ad FROM uyeler WHERE puan > ?');
$konumsal->execute([5]);
echo 'konumsal, FETCH_COLUMN: ', json_encode($konumsal->fetchAll(PDO::FETCH_COLUMN)), "\n";

$sirali = $pdo->prepare('SELECT ad, puan FROM uyeler ORDER BY id');
$sirali->execute();
echo 'FETCH_NUM: ', json_encode($sirali->fetch(PDO::FETCH_NUM)), "\n";

try {
    $pdo->prepare('SELECT yok FROM uyeler')->execute();
} catch (PDOException $e) {
    echo 'geçersiz sorgu: ', get_class($e), "\n";
}
```

```
hata kipi istisna: true
adlı, FETCH_ASSOC: [{"ad":"ayse","puan":7},{"ad":"mehmet","puan":3}]
konumsal, FETCH_COLUMN: ["ayse"]
FETCH_NUM: ["ayse",7]
geçersiz sorgu: PDOException
```

Üç şey görülür. Varsayılan hata kipi istisnadır. Aynı iki satır adlı yer tutucuyla sütun adlı diziler olarak, konumsal yer tutucuyla yalnız ad olarak döner. Var olmayan bir sütunu soran sorgu sessizce boş dönmez, `PDOException` fırlatır.

#### Yazma Sorguları ve Etkilenen Satır

Okuma sorgusu satır döndürür; yazma sorgusu satır döndürmez, onun yerine bir etki bırakır. İki bilgi bu etkiyi anlatır. `lastInsertId()` en son eklenen satırın anahtarını verir. `rowCount()` yazma sorgusunun kaç satırı etkilediğini söyler. İkisi de sorgu başarıyla bittikten sonra okunur; hata kipi istisna olduğu için başarısız bir yazma zaten bu satıra hiç ulaşmaz.

`bindValue()` hazır deyime değeri tipiyle birlikte bağlamanın başka bir yoludur. `execute()` dizisi her değeri metin sayar; `bindValue()` ise üçüncü bağımsız değişkenle `PDO::PARAM_INT` gibi bir tip bildirir. Aşağıdaki dosya bir üye ekler, puanını günceller ve etkilenen satır sayısını okur.

```php
<?php
// yazma.php — ekleme, güncelleme ve etkilenen satır
declare(strict_types=1);
require 'baglanti.php';

$pdo = baglan();

$ekle = $pdo->prepare('INSERT INTO uyeler (ad, puan) VALUES (:ad, :puan)');
$ekle->bindValue(':ad', 'zeynep');
$ekle->bindValue(':puan', 9, PDO::PARAM_INT);
$ekle->execute();
echo 'son anahtar: ', $pdo->lastInsertId(), "\n";

$guncelle = $pdo->prepare('UPDATE uyeler SET puan = puan + 1 WHERE puan > :alt');
$guncelle->execute(['alt' => 5]);
echo 'etkilenen satır: ', $guncelle->rowCount(), "\n";

$yok = $pdo->prepare('UPDATE uyeler SET puan = 0 WHERE ad = :ad');
$yok->execute(['ad' => 'kimse']);
echo 'olmayan üye, etkilenen satır: ', $yok->rowCount(), "\n";
```

```
son anahtar: 3
etkilenen satır: 2
olmayan üye, etkilenen satır: 0
```

Üçüncü satır bir hata değildir. Hiçbir satırla eşleşmeyen bir `UPDATE` başarıyla biter ve sıfır satır etkiler. Yazmanın bir etki bırakıp bırakmadığını anlamak için `rowCount()` değerine bakılır; istisna bu durumda fırlamaz.

#### Dönen Değerin Tipi

Sütun tipi veritabanında `INTEGER` olsa da PHP'ye hangi tipte geleceği bir bağlantı ayarına bağlıdır. PHP 8.1'den beri SQLite sürücüsünün varsayılan davranışında tam sayı sütunu `int` olarak döner. `PDO::ATTR_STRINGIFY_FETCHES` ayarı açıksa her değer `string` olarak döner. Kod aynı kalır, yalnız ayar değişir. Ayarı dağıtımı yapan taraf verdiği için sapma ortamdan gelir.

Sonuç, `puan` sütununun tipidir. İstemcinin sapması üye adını değiştirir; ama `ayse` de `mehmet` de aynı tipte bir puana sahiptir. Aşağıdaki dosya önce iki tipi gösterir, ardından taraf raporunu alır.

```php
<?php
// tip.php — puan sütununun tipini kim değiştirebiliyor?
declare(strict_types=1);
require 'taraf.php';
require 'baglanti.php';

echo 'varsayılan: ', puanTipi('ayse', [], false), "\n";
echo 'metne çevirme açık: ', puanTipi('ayse', [PDO::ATTR_STRINGIFY_FETCHES => true], false), "\n";

tarafRaporu(
    'puan tipi',
    fn(array $s) => puanTipi($s['istemci'] ?? 'ayse', $s['ortam'] ?? [], false),
    ['istemci' => 'mehmet', 'ortam' => [PDO::ATTR_STRINGIFY_FETCHES => true]]
);
```

```
varsayılan: int
metne çevirme açık: string
puan tipi → ortam
    istemci değiştiremedi · ortam değiştirdi
```

İstemci tipi değiştiremez: hangi üyeyi sorarsa sorsun puan aynı tipte döner. Ortam değiştirebilir: ayar açıldığında aynı sorgu `int` yerine `string` getirir. Karar sırasına göre ilk değiştirebilen taraf ortamdır, bu yüzden ders ortama aittir. Dönen tip yalnızca bir yerleşim ayrıntısı gibi görünür, ama değer `int` bekleyen tipli bir parametreye verildiğinde `strict_types` altında `string` gelmesi bir `TypeError` fırlatır.

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

Aynı soruyu bir kez daha soralım, ama bu kez kod dönen değeri kendisi `int`'e çevirsin. Sorgu, yer tutucu ve ayar aynıdır; yalnızca `(int)` dönüşümü eklenmiştir. Ortamın sapması bağlantı ayarını yine değiştirir ve veritabanı yine `string` döndürür, ancak kod bunu sonuca ulaşmadan önce düzeltir.

```php
<?php
// sinir.php — açık dönüşümle aynı ölçüm
declare(strict_types=1);
require 'taraf.php';
require 'baglanti.php';

tarafRaporu(
    'puan tipi, açık dönüşümle',
    fn(array $s) => puanTipi($s['istemci'] ?? 'ayse', $s['ortam'] ?? [], true),
    ['istemci' => 'mehmet', 'ortam' => [PDO::ATTR_STRINGIFY_FETCHES => true]]
);
```

```
puan tipi, açık dönüşümle → sunucu
    istemci değiştiremedi · ortam değiştiremedi
```

Sonuç ortamdan sunucuya taşındı. Ortam ayarı hâlâ değiştirebilir, ama değişikliğin sonuca etkisi kalmaz; çünkü sonucun tipini artık kodun kendi dönüşümü belirler. Her savunma düzeneği sahibi bu biçimde taşımaz, bazı sonuçlarda sahip değişmeden kalır. Bu örnekte ise dönüşüm sonucu kodun içine çeker.

#### Özet

- PDO bağlantısı, hata kipi, hazır deyim ve fetch kipleri veri erişim katmanını oluşturur; varsayılan hata kipi istisnadır.
- Hazır deyim sorgu metnini ve değerleri ayırır; yer tutucu adlı (`:alt`) ya da konumsal (`?`) yazılır.
- Fetch kipi satırın biçimini seçer: sütun adlı dizi, sıralı dizi ya da tek sütun.
- Dönen değerin tipi bir bağlantı ayarına bağlıdır ve ayar ortamdan geldiği için bu derste taraf ortamdır.
- Kod değeri açıkça `int`'e çevirdiğinde ortamın sapması sonucu değiştiremez ve sonuç sunucuya geçer.

#### Sonraki Adım

Bağlantıyı ve tek bir sorguyu kurduk; ama bir iş çoğu zaman birden fazla yazmadan oluşur. İkinci yazma başarısız olduğunda ilki ne olur? Sonraki ders, yarıda kalan yazmayı işlemlerle ele alır.

### İşlemler

Source: https://academia.sh/tr/kurslar/php-web/islemler

İki hesap arasındaki aktarım işlemle ve işlemsiz çalıştırılır; ikinci yazma başarısız olduğunda bakiye toplamını kimin değiştirebildiği ölçülür ve geri alma çağrılmayan işlemin neyi korumadığı gösterilir.

Veri erişim katmanı bir sorguyu güvenle çalıştırıyordu. Ama bir iş çoğu zaman tek sorgudan oluşmaz: para bir hesaptan düşer, başka bir hesaba eklenir. İki yazmanın arasında bir şey ters giderse ilk yazma kalır, ikincisi kalmaz. Soru şudur: yarıda kalan yazma sonucu kimin elinde bırakır?

Veritabanları kursunda işlem ve ACID özellikleri anlatıldı; atomikliğin hangi bileşenden geldiği orada aranmıştı. Burada o soru yeniden sorulmaz. Sapma uygulanır ve sonuca bakılır: ikinci yazma başarısız olduğunda bakiye toplamı değişiyor mu, değişiyorsa kimin sapmasıyla? Karar sırası öncekiyle aynıdır: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin taraf hücresi sunucudur: işlem açıldığında sapmanın sonucu değiştirmesi kapanır. Çekirdek dosya her derste olduğu gibi aynen verilir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```
**VE5.** Bakiyeler tam sayıdır ve `bakiye >= 0` kısıtı tabloda durur; iki hesap `a` ve `b` yüz ve elli bakiyeyle başlar, yani toplam her zaman yüz ellidir. **VE6.** Ortamın sapması, dağıtımı yapanın tabloya koyduğu bir kısıttır: `b` hesabına yazılan her güncelleme reddedilir. Bu, ikinci yazmayı her seferinde aynı biçimde başarısız kılar. **VE7.** İstemci yalnız aktarılacak tutarı gönderir; sapması kırk yerine yirmi beş göndermesidir. **VE8.** Sonuç, aktarım denemesinden sonra iki bakiyenin toplamıdır.

#### İşlemsiz Aktarım

İşlem (transaction), birden fazla yazmayı tek bir birim olarak çalıştıran bir sarmalayıcıdır. PDO'da bu üç çağrıdır: `beginTransaction()` işlemi açar, `commit()` yazılanları kesinleştirir, `rollBack()` işlemin açıldığı andan beri yazılanları geri alır. Önce işlem olmadan bakalım. Aşağıdaki dosya hesapları kurar ve aktarımı iki ayrı güncelleme olarak yazar; `islemli` bayrağı açıkken aynı iki güncelleme işlemin içine alınır.

```php
<?php
// hesaplar.php — iki hesap ve aktarım
declare(strict_types=1);

function hesapKur(?string $kisit = null): PDO {
    $pdo = new PDO('sqlite::memory:');
    $pdo->exec('CREATE TABLE hesaplar (ad TEXT PRIMARY KEY, bakiye INTEGER CHECK (bakiye >= 0))');
    $pdo->exec("INSERT INTO hesaplar VALUES ('a', 100), ('b', 50)");
    if ($kisit !== null) $pdo->exec($kisit);
    return $pdo;
}

function toplam(PDO $pdo): int {
    return (int) $pdo->query('SELECT SUM(bakiye) FROM hesaplar')->fetchColumn();
}

function guncelle(PDO $pdo, string $ad, int $fark): void {
    $pdo->prepare('UPDATE hesaplar SET bakiye = bakiye + :fark WHERE ad = :ad')
        ->execute(['fark' => $fark, 'ad' => $ad]);
}

function aktar(PDO $pdo, int $tutar, bool $islemli): void {
    if ($islemli) $pdo->beginTransaction();
    try {
        guncelle($pdo, 'a', -$tutar);
        guncelle($pdo, 'b', $tutar);
        if ($islemli) $pdo->commit();
    } catch (PDOException $e) {
        if ($islemli) $pdo->rollBack();
    }
}
```

Sapma ortamdan gelen kısıttır: `b` hesabına yazan her güncelleme `RAISE(ABORT)` ile reddedilir. İlk yazma `a` hesabından tutarı düşer, ikincisi reddedilir ve işlem yoksa ilk yazma kalır.

```php
<?php
// islemsiz.php — işlem olmadan aktarım
declare(strict_types=1);
require 'taraf.php';
require 'hesaplar.php';

const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b'
               BEGIN SELECT RAISE(ABORT, 'reddedildi'); END";

function aktarimSonucu(array $s, bool $islemli): int {
    $pdo = hesapKur($s['ortam'] ?? null);
    aktar($pdo, $s['istemci'] ?? 40, $islemli);
    return toplam($pdo);
}

tarafRaporu('toplam, işlemsiz', fn(array $s) => aktarimSonucu($s, false), ['istemci' => 25, 'ortam' => KISIT]);
```

```
toplam, işlemsiz → ortam
    istemci değiştiremedi · ortam değiştirdi
```

Taban toplam yüz ellidir. İstemci tutarı değiştirdiğinde toplam yine yüz elli kalır; çünkü tutar iki hesap arasında yer değiştirir, toplamı etkilemez. Ortam kısıtı koyduğunda ise ilk yazma kalır, ikincisi gitmez ve toplam düşer. Sonuç ortamındır.

#### İşlemle Aktarım

Aynı sapma, aynı kısıt, aynı iki güncelleme; fark yalnız `islemli` bayrağıdır. İkinci yazma reddedildiğinde `catch` bloğu `rollBack()` çağırır ve işlemin başından beri yazılanlar geri alınır.

```php
<?php
// islemli.php — aynı aktarım işlemin içinde
declare(strict_types=1);
require 'taraf.php';
require 'hesaplar.php';

const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b'
               BEGIN SELECT RAISE(ABORT, 'reddedildi'); END";

function aktarimSonucu(array $s, bool $islemli): int {
    $pdo = hesapKur($s['ortam'] ?? null);
    aktar($pdo, $s['istemci'] ?? 40, $islemli);
    return toplam($pdo);
}

tarafRaporu('toplam, işlemli', fn(array $s) => aktarimSonucu($s, true), ['istemci' => 25, 'ortam' => KISIT]);
```

```
toplam, işlemli → sunucu
    istemci değiştiremedi · ortam değiştiremedi
```

Ortam yine ikinci yazmayı başarısız kılar, ama artık sonuç değişmez: ilk yazma da geri alınmıştır ve toplam yüz ellidir. Hiçbir dış taraf sonucu değiştiremediği için sonuç sunucuya geçti. Savunma düzeneği, yani işlem, sonucu ortamdan sunucuya taşıdı. İşlemin gücü burada yazılan kodun kendisinden değil, kodun `commit()` ve `rollBack()` çağrılarını doğru yerlere koymasından gelir.

#### İşlemin Durumu

İşlemin açık olup olmadığı `inTransaction()` ile sorulur. PDO işlemi iç içe açmaz: açık bir işlemin içinde ikinci kez `beginTransaction()` çağırmak da, açık işlem yokken `commit()` ya da `rollBack()` çağırmak da bir `PDOException` fırlatır. İç içe bir bölümü geri alınabilir yapmak gerekirse bunu kayıt noktası (savepoint) üstlenir; kayıt noktası sorgu olarak yazılır ve `ROLLBACK TO` yalnız o noktadan sonrasını geri alır.

```php
<?php
// durum.php — işlemin durumu, çift açma ve kayıt noktası
declare(strict_types=1);
require 'hesaplar.php';

$pdo = hesapKur();
echo 'başlangıç açık: ', var_export($pdo->inTransaction(), true), "\n";

try { $pdo->commit(); } catch (PDOException $e) { echo 'işlemsiz commit: ', get_class($e), "\n"; }

$pdo->beginTransaction();
try { $pdo->beginTransaction(); } catch (PDOException $e) { echo 'çift açma: ', get_class($e), "\n"; }

guncelle($pdo, 'a', -10);
$pdo->exec('SAVEPOINT yarim');
guncelle($pdo, 'a', -30);
echo 'nokta öncesi a: ', $pdo->query("SELECT bakiye FROM hesaplar WHERE ad = 'a'")->fetchColumn(), "\n";
$pdo->exec('ROLLBACK TO yarim');
echo 'nokta sonrası a: ', $pdo->query("SELECT bakiye FROM hesaplar WHERE ad = 'a'")->fetchColumn(), "\n";
$pdo->commit();
echo 'kesinleşti, toplam: ', toplam($pdo), ', açık: ', var_export($pdo->inTransaction(), true), "\n";
```

```
başlangıç açık: false
işlemsiz commit: PDOException
çift açma: PDOException
nokta öncesi a: 60
nokta sonrası a: 90
kesinleşti, toplam: 140, açık: false
```

Kayıt noktasından sonraki otuzluk düşüş geri alındı, ondan önceki onluk düşüş kaldı ve `commit()` yalnız onu kesinleştirdi. Toplam yüz kırktır; çünkü `a` hesabından düşen on, `b` hesabına eklenmedi. Bu blokta iki yazmalı bir aktarım yoktur; kayıt noktasının davranışını göstermek için tek yönlü bir düşüş yazılmıştır.

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

İşlemi açmak, korumanın tamamı değildir. Aynı aktarımı bu kez `rollBack()` çağrısı olmadan yazalım: `catch` bloğu istisnayı yutar ve işlem açık kalır. Bağlantı kapandığında işlemin ne olacağı bellekteki bir veritabanında gözlenemez; veritabanı bağlantıyla birlikte yok olur ve dosya veritabanı açılmaz. Gözlenebilen şey aynı bağlantıdan, işlem hâlâ açıkken okunan toplamdır.

```php
<?php
// yarim.php — işlem açık, rollBack çağrılmıyor
declare(strict_types=1);
require 'taraf.php';
require 'hesaplar.php';

const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b'
               BEGIN SELECT RAISE(ABORT, 'reddedildi'); END";

function yarimSonuc(array $s): int {
    $pdo = hesapKur($s['ortam'] ?? null);
    $pdo->beginTransaction();
    try {
        guncelle($pdo, 'a', -($s['istemci'] ?? 40));
        guncelle($pdo, 'b', $s['istemci'] ?? 40);
        $pdo->commit();
    } catch (PDOException $e) {
        // istisna yakalandı ama geri alma unutuldu
    }
    return toplam($pdo);
}

tarafRaporu('toplam, geri alma yok', 'yarimSonuc', ['istemci' => 25, 'ortam' => KISIT]);

$pdo = hesapKur(KISIT);
$pdo->beginTransaction();
try { guncelle($pdo, 'a', -40); guncelle($pdo, 'b', 40); } catch (PDOException $e) {}
echo 'işlem hâlâ açık: ', var_export($pdo->inTransaction(), true), "\n";
```

```
toplam, geri alma yok → ortam
    istemci değiştiremedi · ortam değiştirdi
işlem hâlâ açık: true
```

İşlem açıkken bile ortamın sapması toplamı değiştirdi. `rollBack()` çağrılmadığında işlem, açık kaldığı sürece hiçbir şeyi geri almaz; yalnız kesinleştirmeyi erteler. PDO açık kalan işlemi betik bittiğinde ya da bağlantı kapanırken kendiliğinden geri alır, ama o ana kadar aynı bağlantıdan okunan toplam düşmüş durumdadır. Sonuç sunucuya taşınmadı, ortamda kaldı. İşlem açmak ile işlemi doğru kapatmak aynı şey değildir.

#### Özet

- İşlem, birden fazla yazmayı `beginTransaction()`, `commit()` ve `rollBack()` ile tek birim olarak çalıştırır.
- İşlemsiz aktarımda ikinci yazma başarısız olduğunda ilk yazma kalır ve bakiye toplamı değişir; bu sapma ortamdan gelir.
- İşlemle aynı aktarımda `rollBack()` ilk yazmayı da geri alır ve toplam sapmadan etkilenmez; bu derste taraf sunucudur.
- `rollBack()` çağrılmayan işlem açık kalır ve sonuç ortamda kalır.
- Bir yazmanın etkisini yarım bırakmak, sonucu dış taraflardan birine teslim etmektir.

#### Sonraki Adım

İşlem verinin tutarlılığını korudu, ama tabloların kendisi nereden geliyor? Şema da bir yazma sonucudur ve her ortamda aynı olmayabilir. Sonraki ders, şemayı kimin belirlediğini göç yönetimiyle ele alır.

### Göç Yönetimi

Source: https://academia.sh/tr/kurslar/php-web/goc-yonetimi

Göç dosyaları, göç kaydı tablosu, sıralı uygulama ve geri alma küçük bir düzenekte kurulur; bu çalıştırmada kaç göçün uygulandığını veritabanının durumunun, şema sürümünü ise kodun belirlediği ölçülür.

İşlem, yazmanın bütün olarak kalmasını sağladı. Ama yazmaların üzerinde durduğu tablolar da bir yerden gelmek zorundadır ve uygulama büyüdükçe bu tablolar değişir. Şemayı elle değiştirmek, hangi ortamda hangi değişikliğin yapıldığını kimsenin bilmediği bir duruma götürür. Soru şudur: şemayı hangi veritabanı belirliyor?

Çerçeve kursundaki Göçler ve Tohumlama dersi, bir göçün çalıştığını ölçmüştü. Bu derste göçün kendisi yeniden anlatılmaz; sorulan şey, bu çalıştırmada kaç göçün uygulandığını kimin değiştirebildiğidir. Veritabanları kursundaki şema göçleri dersi de kavramı zaten tanıttı; burada yalnız bir PHP düzeneği kurulur. Karar sırası değişmez: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin ağırlıklı tarafı ortamdır. Çekirdek dosya aynen gelir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```
**VE9.** Göç dosyaları PHP dizisidir ve her göç bir ileri sorgusuyla bir geri sorgusu taşır; dosya sistemi okunmaz. **VE10.** Göç kaydı `gocler` tablosudur ve uygulanan her göçün adını tutar. **VE11.** Ortamın sapması veritabanının durumudur: boş bir veritabanı ya da göçleri önceden uygulanmış bir veritabanı. **VE12.** İstemci yalnız göçlerden sonra eklenecek üyenin adını gönderir; sapması `ayse` yerine `mehmet` göndermesidir.

#### Göç, Kayıt ve Sıra

Göç (migration), şemada tek bir değişikliği anlatan, adı sıralı bir dosyadır. Adın başındaki numara sırayı belirler: `001` önce, `003` sonra uygulanır. Göç kaydı, hangi göçlerin bu veritabanına uygulandığını tutan tablodur. Uygulayıcı kayıtta olmayan göçleri sırayla çalıştırır, her birini bir işlemin içinde uygular ve işlem bitince adını kayda ekler. Kayıt ve değişiklik aynı işlemde yazıldığı için biri olmadan öteki kalmaz.

Geri alma da aynı yapıyı tersine kullanır: kayıttaki son göçün geri sorgusu çalıştırılır ve adı kayıttan silinir. Aşağıdaki dosya üç göçü, kaydı ve iki uygulayıcıyı tanımlar. Çıktısı yoktur, yalnız diske yazılır.

```php
<?php
// gocler.php — göç dosyaları, göç kaydı ve sıralı uygulama
declare(strict_types=1);

const GOCLER = [
    '001-uyeler' => ['CREATE TABLE uyeler (id INTEGER PRIMARY KEY, ad TEXT)', 'DROP TABLE uyeler'],
    '002-puan' => ['ALTER TABLE uyeler ADD COLUMN puan INTEGER DEFAULT 0', 'ALTER TABLE uyeler DROP COLUMN puan'],
    '003-indeks' => ['CREATE INDEX uyeler_ad ON uyeler (ad)', 'DROP INDEX uyeler_ad'],
];

function gocKaydiKur(PDO $pdo): void {
    $pdo->exec('CREATE TABLE IF NOT EXISTS gocler (ad TEXT PRIMARY KEY)');
}

function uygulananlar(PDO $pdo): array {
    return $pdo->query('SELECT ad FROM gocler ORDER BY ad')->fetchAll(PDO::FETCH_COLUMN);
}

/** Kayıtta olmayan göçleri sırayla uygular; bu çalıştırmada uygulananların adını döndürür. */
function gocleriUygula(PDO $pdo): array {
    gocKaydiKur($pdo);
    $yapilan = uygulananlar($pdo);
    $buCalistirma = [];
    foreach (GOCLER as $ad => [$ileri, $geri]) {
        if (in_array($ad, $yapilan, true)) continue;
        $pdo->beginTransaction();
        $pdo->exec($ileri);
        $pdo->prepare('INSERT INTO gocler (ad) VALUES (:ad)')->execute(['ad' => $ad]);
        $pdo->commit();
        $buCalistirma[] = $ad;
    }
    return $buCalistirma;
}

/** Kayıttaki son göçü geri alır; geri aldığının adını döndürür. */
function sonGocuGeriAl(PDO $pdo): ?string {
    $yapilan = uygulananlar($pdo);
    $ad = end($yapilan);
    if ($ad === false) return null;
    $pdo->beginTransaction();
    $pdo->exec(GOCLER[$ad][1]);
    $pdo->prepare('DELETE FROM gocler WHERE ad = :ad')->execute(['ad' => $ad]);
    $pdo->commit();
    return $ad;
}

function sonSurum(PDO $pdo): ?string {
    $ad = $pdo->query('SELECT MAX(ad) FROM gocler')->fetchColumn();
    return $ad === false || $ad === null ? null : $ad;
}
```

Şimdi düzeneği boş bir veritabanında çalıştıralım. İkinci çalıştırmada kayıtta her göç bulunduğu için hiçbir göç uygulanmaz.

```php
<?php
// uygulama.php — uygulama, ikinci çalıştırma ve geri alma
declare(strict_types=1);
require 'gocler.php';

$pdo = new PDO('sqlite::memory:');
echo 'ilk çalıştırma: ', implode(', ', gocleriUygula($pdo)), "\n";
echo 'ikinci çalıştırma: ', count(gocleriUygula($pdo)), " göç\n";
echo 'geri alınan: ', sonGocuGeriAl($pdo), "\n";
echo 'kayıt: ', implode(', ', uygulananlar($pdo)), "\n";
echo 'yeniden uygulanan: ', implode(', ', gocleriUygula($pdo)), "\n";

echo 'sütunlar: ', implode(', ', $pdo->query("SELECT name FROM pragma_table_info('uyeler')")->fetchAll(PDO::FETCH_COLUMN)), "\n";
```

```
ilk çalıştırma: 001-uyeler, 002-puan, 003-indeks
ikinci çalıştırma: 0 göç
geri alınan: 003-indeks
kayıt: 001-uyeler, 002-puan
yeniden uygulanan: 003-indeks
sütunlar: id, ad, puan
```

İkinci çalıştırma sıfır göç uyguladı; çünkü uygulanıp uygulanmayacağına göç dosyası değil kayıt karar verir. Geri alınan göç yalnız sonuncusudur ve kayıttan silindiği için bir sonraki çalıştırmada yeniden uygulanır. Dosyalar aynı kalır, değişen kayıttır.

#### Veri Taşıyan Göç

Şema göçü yalnız tablo yapısını değiştirir; ama bazı göçler sütun eklemekle kalmaz, ona değer de yazar. Geri sorgusu böyle bir göçün şemasını eski hâline döndürür, yazılan değerleri döndürmez. Aşağıdaki dosya üç göçü uygular, bir üye ekler, `seviye` sütununu ekleyip puandan doldurur ve ardından geri sorgusuyla sütunu kaldırır.

```php
<?php
// veritasi.php — sütun ekleyip dolduran göçün geri alınması
declare(strict_types=1);
require 'gocler.php';

$pdo = new PDO('sqlite::memory:');
gocleriUygula($pdo);
$pdo->exec("INSERT INTO uyeler (ad, puan) VALUES ('ayse', 7), ('mehmet', 3)");

$pdo->exec('ALTER TABLE uyeler ADD COLUMN seviye TEXT');
$pdo->exec("UPDATE uyeler SET seviye = CASE WHEN puan > 5 THEN 'üst' ELSE 'alt' END");
echo 'göçten sonra: ', json_encode($pdo->query('SELECT ad, seviye FROM uyeler ORDER BY id')->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE), "\n";

$pdo->exec('ALTER TABLE uyeler DROP COLUMN seviye');
echo 'geri aldıktan sonra: ', json_encode($pdo->query('SELECT * FROM uyeler ORDER BY id')->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE), "\n";
```

```
göçten sonra: [{"ad":"ayse","seviye":"üst"},{"ad":"mehmet","seviye":"alt"}]
geri aldıktan sonra: [{"id":1,"ad":"ayse","puan":7},{"id":2,"ad":"mehmet","puan":3}]
```

Şema geri geldi, `seviye` değerleri gelmedi. Geri alma bir yapıyı eski hâline getirir; yazılmış veriyi geri getirmez. Bu yüzden veri taşıyan bir göçün geri sorgusu, yazmadan önce yedek almayı ya da geri alınamaz olarak işaretlenmeyi gerektirebilir. Bu karar göçü yazanındır ve kodda kayıtlı durur; hangi ortamda çalıştığına bakmaz.

#### Sonuç Kime Ait

Ölçülen sonuç, bu çalıştırmada uygulanan göç sayısıdır. Aynı kod, boş bir veritabanında üç göç uygular; göçleri önceden uygulanmış bir veritabanında hiçbirini uygulamaz. Hangi durumun geçerli olduğu kodun içinde değildir, uygulamanın bağlandığı veritabanındadır ve veritabanını hazırlayan taraf dağıtımı yapandır. İstemci ise göçler bittikten sonra bir üye ekler; bu, kayıtta hiçbir şeyi değiştirmez.

```php
<?php
// gocsonuc.php — bu çalıştırmada kaç göç uygulandı?
declare(strict_types=1);
require 'taraf.php';
require 'gocler.php';

function uygulananSayi(array $s): int {
    $pdo = new PDO('sqlite::memory:');
    if (($s['ortam'] ?? 'bos') === 'guncel') gocleriUygula($pdo);
    $buCalistirma = gocleriUygula($pdo);
    $pdo->prepare('INSERT INTO uyeler (ad) VALUES (:ad)')->execute(['ad' => $s['istemci'] ?? 'ayse']);
    return count($buCalistirma);
}

echo 'boş veritabanı: ', uygulananSayi([]), "\n";
echo 'güncel veritabanı: ', uygulananSayi(['ortam' => 'guncel']), "\n";
tarafRaporu('uygulanan göç sayısı', 'uygulananSayi', ['istemci' => 'mehmet', 'ortam' => 'guncel']);
```

```
boş veritabanı: 3
güncel veritabanı: 0
uygulanan göç sayısı → ortam
    istemci değiştiremedi · ortam değiştirdi
```

İstemci sayıyı değiştirmedi: üyenin adı göçlerin uygulanıp uygulanmadığına karışmaz. Ortam değiştirdi: veritabanı güncelken sayı üçten sıfıra indi. Sonuç ortama aittir. Göç düzeneğinin kendisi doğru çalışıyor; ama tek bir çalıştırmanın sonucu, düzeneğin değil, onu çalıştıran veritabanının durumunu yansıtır.

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

Aynı düzenekten bir başka sonuç alalım: göçler çalıştıktan sonraki şema sürümü, yani kayıttaki son göçün adı. Veritabanı boş da olsa güncel de olsa, göçler çalıştığında kayıtta aynı son ad durur. Bu sonuç veritabanının başlangıç durumuna bağlı değildir.

```php
<?php
// gocsinir.php — çalıştırmadan sonraki şema sürümü
declare(strict_types=1);
require 'taraf.php';
require 'gocler.php';

function sonrakiSurum(array $s): string {
    $pdo = new PDO('sqlite::memory:');
    if (($s['ortam'] ?? 'bos') === 'guncel') gocleriUygula($pdo);
    gocleriUygula($pdo);
    $pdo->prepare('INSERT INTO uyeler (ad) VALUES (:ad)')->execute(['ad' => $s['istemci'] ?? 'ayse']);
    return sonSurum($pdo) ?? 'yok';
}

tarafRaporu('çalıştırmadan sonraki sürüm', 'sonrakiSurum', ['istemci' => 'mehmet', 'ortam' => 'guncel']);
```

```
çalıştırmadan sonraki sürüm → sunucu
    istemci değiştiremedi · ortam değiştiremedi
```

Veritabanının durumu değişse de sürüm değişmedi; sürümü dağıtımın başlangıç durumu değil, göç dosyalarının son adı belirliyor. Aynı düzenekten iki sonuç iki farklı sahiplik verdi: uygulanan sayı ortamın, çalıştırma sonrası sürüm sunucunun. Bir göç düzeneği hangi soruyu yanıtladığına göre okunmalıdır.

#### Özet

- Göç, adı sıralı bir dosyadır; göç kaydı tablosu hangilerinin uygulandığını tutar ve uygulayıcı yalnız kayıtta olmayanları sırayla çalıştırır.
- Her göç ve kaydı aynı işlemde yazılır; geri alma son göçün geri sorgusunu çalıştırıp adı kayıttan siler.
- Bu çalıştırmada uygulanan göç sayısı veritabanının durumuna bağlıdır; bu derste taraf ortamdır.
- Çalıştırmadan sonraki şema sürümü her ortamda aynıdır ve sonuç sunucuya aittir.
- Aynı düzenekten alınan iki sonuç farklı tarafa ait olabilir.

#### Sonraki Adım

Şema artık kayıt altında; peki bu şemanın üstüne kurulan nesne–tablo bağlaması hangi sorguları arka planda gönderiyor? Sonraki ders, bir listeyi basarken gidenleri sayar.

### Nesne–Tablo Bağlamasının Sorguları

Source: https://academia.sh/tr/kurslar/php-web/nesne-tablo-baglamasinin-sorgulari

Bir liste basılırken gönderilen sorgular bir sayaçla sayılır; tembel yüklemede sorgu sayısının satırla büyüdüğü, istekli yüklemede ikide kaldığı ve sorgu metninin ise gönderilen veriye göre değişebildiği ölçülür.

Şema artık kayıt altında. Üstüne bir nesne–tablo bağlaması (object-relational mapping) kurulduğunda uygulama kodu satır değil nesne görür ve sorguları yazmaz. Sorgular yine gider; yalnızca nerede ve kaç tane gittiği koddan okunmaz hale gelir. Soru şudur: bir liste basılırken veritabanına kaç sorgu gidiyor?

Bu derste model kurulmaz. Çerçeve kursundaki Nesne ve Tablo Bağlaması ile İlişkiler dersleri modeli ve ilişkinin yüklenme zamanını işledi. Daha önceki bir kursta sorgu sayısı da ölçülmüştü; o ölçümde sayı yalnızca bir koşum sayısıydı. Burada sayı bir sonuçtur ve sorulan şey sahibidir: sayıyı istemci mi, ortam mı, yoksa sunucu mu belirliyor? Karar sırası aynıdır: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin taraf hücresi sunucudur ve çekirdek aşağıdadır.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```
**VE13.** Bağlamanın yaptığı iş, PDO çağrılarını saran küçük bir sınıfla gösterilir; her `sec()` çağrısı bir sorgu sayılır ve sorgu metni de kaydedilir. **VE14.** Üç yazar ve altı yazı vardır; yazıların yazarları `1` ve `2`'dir, `3` numaralı yazarın henüz yazısı yoktur. Sorgu önbelleği yoktur. **VE15.** İstemci sapması bir yazı daha eklemesidir; sapmanın değeri, eklenen yazının yazar numarasıdır. **VE16.** Sonuç, bir liste basılırken gönderilen sorgu sayısıdır.

#### Sayaç ve İki Yükleyici

Aşağıdaki dosya sayacı ve veriyi kurar, ardından listeyi iki biçimde yükler. Tembel yükleme (lazy loading) listeyi bir sorguyla alır, sonra her yazı için yazarını ayrı bir sorguyla ister. İstekli yükleme (eager loading) listeyi bir sorguyla alır, ardından gereken bütün yazarları `IN (...)` listesiyle tek ek sorguda getirir. Tembel biçimdeki bu davranış N+1 sorunu diye adlandırılır: bir liste sorgusu, artı satır başına bir sorgu.

```php
<?php
// sayac.php — PDO çağrılarını sayan sarmalayıcı ve iki liste yükleyicisi
declare(strict_types=1);

final class SayacliVeritabani {
    public int $sorgu = 0;
    public array $metinler = [];

    public function __construct(private PDO $pdo) {}

    public function sec(string $sql, array $degerler = []): array {
        $this->sorgu++;
        $this->metinler[] = $sql;
        $hazir = $this->pdo->prepare($sql);
        $hazir->execute($degerler);
        return $hazir->fetchAll(PDO::FETCH_ASSOC);
    }
}

function veriKur(?int $ekYazar = null): SayacliVeritabani {
    $pdo = new PDO('sqlite::memory:');
    $pdo->exec('CREATE TABLE yazarlar (id INTEGER PRIMARY KEY, ad TEXT)');
    $pdo->exec('CREATE TABLE yazilar (id INTEGER PRIMARY KEY, yazar_id INTEGER, baslik TEXT)');
    $pdo->exec("INSERT INTO yazarlar VALUES (1, 'ayse'), (2, 'mehmet'), (3, 'zeynep')");
    $ekle = $pdo->prepare('INSERT INTO yazilar (yazar_id, baslik) VALUES (?, ?)');
    for ($i = 1; $i <= 6; $i++) $ekle->execute([($i % 2) + 1, "yazı $i"]);
    if ($ekYazar !== null) $ekle->execute([$ekYazar, 'yazı 7']);
    return new SayacliVeritabani($pdo);
}

function listeTembel(SayacliVeritabani $db): array {
    $satirlar = [];
    foreach ($db->sec('SELECT yazar_id, baslik FROM yazilar ORDER BY id') as $yazi) {
        $yazar = $db->sec('SELECT ad FROM yazarlar WHERE id = ?', [$yazi['yazar_id']]);
        $satirlar[] = $yazi['baslik'] . ' — ' . $yazar[0]['ad'];
    }
    return $satirlar;
}

function listeIstekli(SayacliVeritabani $db): array {
    $yazilar = $db->sec('SELECT yazar_id, baslik FROM yazilar ORDER BY id');
    $kimlikler = array_values(array_unique(array_column($yazilar, 'yazar_id')));
    sort($kimlikler);
    $yerTutucular = implode(', ', array_fill(0, count($kimlikler), '?'));
    $adlar = array_column($db->sec("SELECT id, ad FROM yazarlar WHERE id IN ($yerTutucular)", $kimlikler), 'ad', 'id');
    return array_map(fn(array $y) => $y['baslik'] . ' — ' . $adlar[$y['yazar_id']], $yazilar);
}
```

İki yükleyiciyi aynı veriyle çalıştıralım. Liste aynı olmalı; fark yalnız sorgu sayısında çıkar.

```php
<?php
// liste.php — iki yükleyici, aynı liste, farklı sorgu sayısı
declare(strict_types=1);
require 'sayac.php';

$tembel = veriKur();
$tembelListe = listeTembel($tembel);
$istekli = veriKur();
$istekliListe = listeIstekli($istekli);

echo 'ilk satır: ', $tembelListe[0], "\n";
echo 'aynı liste: ', var_export($tembelListe === $istekliListe, true), "\n";
echo 'tembel: ', $tembel->sorgu, " sorgu\n";
echo 'istekli: ', $istekli->sorgu, " sorgu\n";
```

```
ilk satır: yazı 1 — mehmet
aynı liste: true
tembel: 7 sorgu
istekli: 2 sorgu
```

İki yükleyici de aynı altı satırı üretti. Tembel yükleme yedi sorgu gönderdi: biri liste için, altısı yazarlar için. İstekli yükleme ikisini gönderdi. Bağlama aracı kod tarafında ikisini de aynı görünüme sokar; fark yalnız veritabanına giden trafikte kalır.

#### Sayı Kime Ait

Şimdi sayıyı bir sonuç olarak ele alalım. İstemci sapması, listeye bir yazı daha eklenmesidir ve yazının yazarı `3` numaralıdır. Tembel yüklemede her yazı bir yazar sorgusu getirdiği için sayı yediden sekize çıkar. İstekli yüklemede yazar sorgusu tek kalır; yeni yazı sorgu sayısına katılmaz, yalnızca `IN` listesine bir değer ekler.

```php
<?php
// sorgusayisi.php — sorgu sayısını kim değiştirebiliyor?
declare(strict_types=1);
require 'taraf.php';
require 'sayac.php';

function sorguSayisi(string $yukleme, ?int $ekYazar): int {
    $db = veriKur($ekYazar);
    $yukleme === 'tembel' ? listeTembel($db) : listeIstekli($db);
    return $db->sorgu;
}

tarafRaporu('tembel yükleme', fn(array $s) => sorguSayisi('tembel', $s['istemci'] ?? null), ['istemci' => 3]);
tarafRaporu('istekli yükleme', fn(array $s) => sorguSayisi('istekli', $s['istemci'] ?? null), ['istemci' => 3]);
```

```
tembel yükleme → istemci
    istemci değiştirdi
istekli yükleme → sunucu
    istemci değiştiremedi
```

Tembel yüklemenin sorgu sayısı satır sayısıyla büyüdüğü için istemci değiştirebildi: her yeni yazı sayıyı bir artırdı. İstekli yüklemede sayı ikide kaldı; sapma etkisiz olduğu için sonuç sunucuya geçti. Savunma düzeneği, yani istekli yükleme, sonucu istemciden sunucuya taşıdı. Bu, yükleme biçiminin kodda seçilmesi demektir; bu seçimi yazılan kod verir, gelen istek vermez.

#### Yazar Hiç Okunmazsa

İstekli yükleme her zaman daha az sorgu göndermez. Liste yalnızca başlıkları basıyorsa yazara hiç erişilmez; tembel yükleme bu durumda ilişki sorgusunu hiç göndermez, istekli yükleme ise sonucu kullanılmayacak bir sorguyu yine de gönderir. İki yükleme biçimi arasındaki fark, ilişkinin erişilip erişilmediğine bağlıdır. Aşağıdaki dosya aynı listeyi yalnız başlıkları basacak biçimde iki yükleyiciyle yeniden kurar.

```php
<?php
// baslik.php — ilişkiye hiç erişilmeyen liste
declare(strict_types=1);
require 'sayac.php';

$tembel = veriKur();
$basliklar = array_column($tembel->sec('SELECT baslik FROM yazilar ORDER BY id'), 'baslik');
echo 'tembel, yalnız başlık: ', $tembel->sorgu, " sorgu\n";

$istekli = veriKur();
listeIstekli($istekli);
echo 'istekli, yazarlar boşa getirildi: ', $istekli->sorgu, " sorgu\n";
$basliklar2 = array_column($istekli->sec('SELECT baslik FROM yazilar ORDER BY id'), 'baslik');
echo 'başlıklar aynı: ', var_export($basliklar === $basliklar2, true), "\n";
```

```
tembel, yalnız başlık: 1 sorgu
istekli, yazarlar boşa getirildi: 2 sorgu
başlıklar aynı: true
```

Tembel yükleme yalnız başlıkları istediğinde tek sorgu yeterli oldu. İstekli yükleme yazarları gereksiz yere getirdi. Bu yüzden istekli yüklemenin sunucuya taşıdığı koruma, ilişkinin gerçekten kullanıldığı listeler için geçerlidir; kullanılmayan ilişkide aynı seçim fazladan bir sorgu gönderir.

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

İstekli yüklemenin sorgu sayısı sabit; peki sorgunun kendisi? `IN` listesindeki yer tutucu sayısı, listedeki farklı yazar sayısından kurulur. Sonuç olarak sorgu sayısı yerine ikinci sorgunun metni alınırsa soru değişir. İstemci sapmasının iki biçimini deneyelim: yeni yazı yeni bir yazara aitse (`3`) ve zaten listede olan bir yazara aitse (`1`).

```php
<?php
// sorgumetni.php — sorgu sayısı değil, sorgu metni
declare(strict_types=1);
require 'taraf.php';
require 'sayac.php';

function ikinciSorgu(?int $ekYazar): string {
    $db = veriKur($ekYazar);
    listeIstekli($db);
    return $db->metinler[1];
}

tarafRaporu('IN listesi, yeni yazar', fn(array $s) => ikinciSorgu($s['istemci'] ?? null), ['istemci' => 3]);
tarafRaporu('IN listesi, var olan yazar', fn(array $s) => ikinciSorgu($s['istemci'] ?? null), ['istemci' => 1]);
```

```
IN listesi, yeni yazar → istemci
    istemci değiştirdi
IN listesi, var olan yazar → sunucu
    istemci değiştiremedi
```

Sonuç, gönderilen veriye bağlı çıktı. Yeni yazar listeye girdiğinde yer tutucu sayısı arttı ve sorgu metni istemcinin eliyle değişti. Var olan yazarın yazısı eklendiğinde metin aynı kaldı. Sorgu sayısı sunucuya taşınmıştı; sorgu metni taşınmadı. Bir savunmanın hangi sonucu koruduğu, o sonucun neyi ölçtüğüne bağlıdır.

#### Özet

- Nesne–tablo bağlaması sorguları saklar; PDO çağrılarını saran bir sayaç kaç sorgu gittiğini görünür kılar.
- Tembel yüklemede sorgu sayısı satır sayısıyla büyür (N+1 sorunu); sayıyı istemci değiştirebilir.
- İstekli yüklemede sorgu sayısı `IN` listesiyle ikide kalır; bu derste taraf sunucudur.
- Aynı düzenekte sorgu metni, gönderilen veriye göre istemci tarafından değiştirilebilir.
- Bir savunmanın neyi koruduğu ölçülen sonuca bağlıdır: sayı korundu, metin korunmadı.

#### Sonraki Adım

Sorguların sayısı ve metni kontrol altındayken, sorguların döndürdüğü veri de bir sınırdan geçer. Uygulamanın dışındaki bir ortaktan gelen veri JSON, XML ya da CSV olarak gelir ve biçimi değişebilir. Sonraki ders, ortağın verisi biçim değiştirirse ne olduğunu ele alır.

### Veri Biçimleri

Source: https://academia.sh/tr/kurslar/php-web/veri-bicimleri

Ortaktan gelen JSON, XML ve CSV metni ayrıştırılır; ortak aynı alanı farklı yazdığında sonucu kimin değiştirebildiği ölçülür ve kendi yanıtımızdaki sayı gösteriminin kimin seçimi olduğu gösterilir.

Sorguların sayısı da metni de elimizdeydi. Ama bir uygulama yalnız kendi veritabanıyla konuşmaz; dışarıdaki bir ortaktan da veri alır ve bu veri JSON, XML ya da CSV olarak gelir. Biçimi ortak belirler. Soru şudur: ortağın verisi biçim değiştirirse ne olur?

Bu dersin ölçü ekseni yine sonucun sahibidir. Karar sırası değişmez: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Ağırlıklı taraf üçüncü taraftır: uygulamanın bağlandığı ortak, kodun dışında kalan ve bağlanılan tek taraftır. Ortaktan gelen metin ağdan alınmaz; bu derste sabit bir dizge olarak koda yazılır ve ağa çıkılmaz. Çekirdek dosya aynen gelir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```
**VE17.** Ortağın gönderdiği metin, bir fonksiyonun ürettiği sabit bir dizgedir; üç biçimde aynı iki ürünü anlatır: kodu `k1` ve `k2`, fiyatları `12.5` ve `8`. **VE18.** Ortağın sapması aynı alanı farklı yazmasıdır: fiyatı sayı yerine dizge olarak gönderir ya da alan adını `fiyat` yerine `ucret` yazar. **VE19.** Kod, ortağın verisini doğrulamadan olduğu gibi okur; doğrulama Güvenlik konusunun işidir. **VE20.** Sonuç, ayrıştırılan fiyat listesinin JSON gösterimidir.

#### Üç Biçim

JSON (JavaScript Object Notation), iç içe diziler ve nesneler taşıyan bir metin biçimidir. PHP'de `json_decode()` onu diziye çevirir. Geçersiz metinde varsayılan davranış sessizce `null` döndürmektir; `JSON_THROW_ON_ERROR` bayrağı bunu bir `JsonException` haline getirir ve hata gözden kaçmaz. XML, etiketlerle kurulu bir ağaçtır; `simplexml_load_string()` onu bir nesneye çevirir. CSV, satırlara ve virgüllerle ayrılmış alanlara dayanır; `str_getcsv()` bir satırı diziye böler. Üçünde de ortak sınır aynıdır: kod, ayrıştırılan şeyin biçimini kendisi bilmek zorundadır.

Aşağıdaki dosya ortağın üç biçimdeki metnini üretir ve her biri için fiyatları okuyan bir ayrıştırıcı tanımlar. Çıktısı yoktur, yalnız diske yazılır.

```php
<?php
// ortak.php — ortağın üç biçimi ve fiyatları okuyan ayrıştırıcılar
declare(strict_types=1);

function ortakJson(?string $sapma = null): string {
    $birinci = $sapma === 'dizge' ? '"12.5"' : '12.5';
    $ikinci = $sapma === 'dizge' ? '"8"' : '8';
    $alan = $sapma === 'alan' ? 'ucret' : 'fiyat';
    return '{"urunler":[{"kod":"k1","' . $alan . '":' . $birinci . '},{"kod":"k2","' . $alan . '":' . $ikinci . '}]}';
}

function ortakXml(): string {
    return '<urunler><urun kod="k1"><fiyat>12.5</fiyat></urun><urun kod="k2"><fiyat>8</fiyat></urun></urunler>';
}

function ortakCsv(): string {
    return "kod,fiyat\nk1,12.5\nk2,8";
}

function jsonFiyatlari(string $metin): array {
    $veri = json_decode($metin, true, flags: JSON_THROW_ON_ERROR);
    return array_map(fn(array $u) => $u['fiyat'] ?? null, $veri['urunler']);
}

function xmlFiyatlari(string $metin): array {
    $kok = simplexml_load_string($metin);
    $fiyatlar = [];
    foreach ($kok->urun as $urun) $fiyatlar[] = (string) $urun->fiyat;
    return $fiyatlar;
}

function csvFiyatlari(string $metin): array {
    $satirlar = explode("\n", $metin);
    array_shift($satirlar);
    return array_map(fn(string $satir) => str_getcsv($satir, escape: '')[1], $satirlar);
}
```

`str_getcsv()` çağrısındaki `escape: ''` bağımsız değişkeni, ters eğik çizginin kaçış karakteri sayılmasını kapatır; CSV'nin kendi kuralı yalnız tırnak işaretidir. Üç ayrıştırıcıyı çalıştıralım ve geçersiz JSON ile geçersiz XML'e bakalım.

```php
<?php
// bicimler.php — üç ayrıştırıcı ve geçersiz girdi
declare(strict_types=1);
require 'ortak.php';

echo 'json: ', json_encode(jsonFiyatlari(ortakJson())), "\n";
echo 'xml: ', json_encode(xmlFiyatlari(ortakXml())), "\n";
echo 'csv: ', json_encode(csvFiyatlari(ortakCsv())), "\n";

try {
    jsonFiyatlari('{"urunler":[');
} catch (JsonException $e) {
    echo 'geçersiz json: ', get_class($e), ', ileti: ', $e->getMessage(), "\n";
}

libxml_use_internal_errors(true);
echo 'geçersiz xml: ', var_export(simplexml_load_string('<urunler><urun>'), true), "\n";
```

```
json: [12.5,8]
xml: ["12.5","8"]
csv: ["12.5","8"]
geçersiz json: JsonException, ileti: Syntax error
geçersiz xml: false
```

JSON sayıları sayı olarak getirdi; XML ve CSV ise her değeri metin olarak. Bu fark ayrıştırıcıdan gelir ve ortağın kararı değildir: bir XML etiketinin içindeki `12.5`, biçim gereği her zaman metindir. Geçersiz JSON `JsonException` fırlattı; geçersiz XML ise hata yerine `false` döndürdü. İkisini de kodun kendisi denetlemek zorundadır.

#### Ortak Aynı Alanı Farklı Yazarsa

Ortağın sapması, kodun beklediği biçimden küçük bir ayrılıktır. Fiyatı `12.5` yerine `"12.5"` yazar; ya da alanı `fiyat` yerine `ucret` diye adlandırır. İlk durumda kod hata vermez, yalnız dönen değerin tipi değişir. İkinci durumda alan bulunamaz ve kod `null` okur. İkisi de ortağın elindedir.

```php
<?php
// ortaksonuc.php — fiyatları kim değiştirebiliyor?
declare(strict_types=1);
require 'taraf.php';
require 'ortak.php';

function fiyatlar(array $s): string {
    return json_encode(jsonFiyatlari(ortakJson($s['üçüncü taraf'] ?? null)));
}

tarafRaporu('fiyatlar, dizge', 'fiyatlar', ['üçüncü taraf' => 'dizge']);
tarafRaporu('fiyatlar, alan adı', 'fiyatlar', ['üçüncü taraf' => 'alan']);
```

```
fiyatlar, dizge → üçüncü taraf
    üçüncü taraf değiştirdi
fiyatlar, alan adı → üçüncü taraf
    üçüncü taraf değiştirdi
```

Her iki sapmada da sonuç üçüncü tarafın elindedir. Dizge durumunda fiyatlar sayı olmaktan çıktı; çıktıda tırnak içinde görünmeleri bunu gösterir ve `strict_types` altında sayı bekleyen bir fonksiyona verilmeleri artık tip uyuşmazlığı olur. Alan adı durumunda fiyatların hiçbiri okunamadı. Kod aynı kaldı; ortağın metni değişti. Bu yüzden ayrıştırmanın sonucu, ortağın biçimine uyulduğu sürece doğrudur; uyum ortağın sözüne bağlıdır.

#### Ayrıştırıcı Ne Garanti Eder, Ne Etmez

CSV'de bir alan virgül içeriyorsa çift tırnağa alınır. Ortak bu kuralı uygularsa ayrıştırıcı alanı bütün tutar; uygulamazsa satır beklenenden fazla sütuna bölünür. Aynı satırı iki biçimde okuyalım.

```php
<?php
// tirnak.php — virgül içeren CSV alanı
declare(strict_types=1);

$tirnakli = 'k3,"Kalem, mavi",4.5';
$tirnaksiz = 'k3,Kalem, mavi,4.5';
echo 'tırnaklı: ', json_encode(str_getcsv($tirnakli, escape: ''), JSON_UNESCAPED_UNICODE), "\n";
echo 'tırnaksız: ', json_encode(str_getcsv($tirnaksiz, escape: ''), JSON_UNESCAPED_UNICODE), "\n";
```

```
tırnaklı: ["k3","Kalem, mavi","4.5"]
tırnaksız: ["k3","Kalem"," mavi","4.5"]
```

Tırnaklı satır üç alan verdi, tırnaksız satır dört. Ayrıştırıcı iki durumda da doğru çalıştı; fark ortağın yazdığı metinden geldi. Aynı ayrım JSON ve XML için de geçerlidir: ayrıştırıcı biçimin kurallarını uygular, ortağın o kurallara uyup uymadığına karar vermez. Bu yüzden üç ayrıştırıcının hiçbiri fiyatın sayı olacağını önceden bilemez; bunu bilen yalnız ortağın sözleşmesidir ve sözleşme kodun dışındadır.

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

Şimdi yön tersine dönsün: ortaktan aldığımız bir değeri kendi yanıtımızda JSON olarak geri verelim. Ortağın gönderdiği indirim `1.0` ya da `1` olabilir; `json_decode()` birini ondalıklı, ötekini tam sayı olarak getirir. Yanıtı `json_encode()` ile üretirken PHP varsayılan olarak `1.0` değerini `1` diye yazar; `JSON_PRESERVE_ZERO_FRACTION` bayrağı ondalık kısmı korur ve `1.0` yazar. Bu gösterimi seçen biziz.

```php
<?php
// yanit.php — kendi yanıtımızdaki sayı gösterimi
declare(strict_types=1);
require 'taraf.php';

echo 'bayraksız: ', json_encode(1.0), "\n";
echo 'bayrakla: ', json_encode(1.0, JSON_PRESERVE_ZERO_FRACTION), "\n";

function yanit(array $s, int $bayrak, bool $zorla): string {
    $metin = $s['üçüncü taraf'] ?? '{"indirim":1.0}';
    $indirim = json_decode($metin, true, flags: JSON_THROW_ON_ERROR)['indirim'];
    return json_encode(['indirim' => $zorla ? (float) $indirim : $indirim], $bayrak);
}

$sapma = ['üçüncü taraf' => '{"indirim":1}'];
tarafRaporu('varsayılan bayrak', fn(array $s) => yanit($s, 0, false), $sapma);
tarafRaporu('bayrak, ortağın gösterimi', fn(array $s) => yanit($s, JSON_PRESERVE_ZERO_FRACTION, false), $sapma);
tarafRaporu('bayrak ve float dönüşümü', fn(array $s) => yanit($s, JSON_PRESERVE_ZERO_FRACTION, true), $sapma);
```

```
bayraksız: 1
bayrakla: 1.0
varsayılan bayrak → sunucu
    üçüncü taraf değiştiremedi
bayrak, ortağın gösterimi → üçüncü taraf
    üçüncü taraf değiştirdi
bayrak ve float dönüşümü → sunucu
    üçüncü taraf değiştiremedi
```

Varsayılan bayrakla yanıt, ortağın `1` ya da `1.0` yazmasından bağımsız aynı çıktı: sonuç sunucuda. Yalnız bayrak eklenince yanıt ortağın yazdığı gösterimi taşımaya başlar ve sonuç üçüncü tarafa döner. Bayrağa ek olarak değer `float`'a çevrilince yanıtın gösterimi yine tamamen bizim elimizde. Yanıtta sayının nasıl görüneceği sunucunun seçimidir; ortak yalnız bizim ona bıraktığımız kadarını belirler.

#### Özet

- JSON, XML ve CSV ayrı ayrıştırıcılarla okunur; `JSON_THROW_ON_ERROR` geçersiz JSON'u `JsonException`'a çevirir, `str_getcsv()` çağrısında `escape: ''` verilir.
- XML ve CSV değerleri metin olarak gelir; JSON sayıları sayı olarak gelir. Tipi belirleyen ayrıştırıcıdır, ortak değildir.
- Ortak aynı alanı dizge ya da başka adla yazarsa fiyat listesi değişir; bu derste taraf üçüncü taraftır.
- Kendi yanıtımızdaki sayı gösterimi `json_encode()` bayrağı ve tip dönüşümüyle belirlenir ve sunucunun seçimidir.
- Aynı ortak verisi bir sonuçta üçüncü tarafa, bir başka sonuçta sunucuya ait olabilir.

#### Sonraki Adım

Ortaktan veriyi bir kez ayrıştırdık; ama ortağa ulaşmak için önce bir istek göndermek gerekir. İstek ve yanıt arasında zaman aşımı da vardır. Sonraki ders, servis yanıt vermezse istemcinin ne yaptığını ele alır.

### HTTP İstemcisi

Source: https://academia.sh/tr/kurslar/php-web/http-istemcisi

Bir HTTP istemcisinin istek ve yanıt nesneleri, taşıma katmanı kapanışı, zaman aşımı istisnası, yeniden deneme ve varsayılan değerle kurulur; servisin bildirdiği fiyatın ve bu savunmaların sonucu kimin değiştirdiğine etkisi ölçülür.

Bir önceki derste ortağın gönderdiği metni ayrıştırdık. O metnin bize nasıl ulaştığını ise göstermedik. Bu derste ulaştırma işini ele alıyoruz ve bir şeyi baştan söylüyoruz: bu derste ağa çıkılmaz. Ders kodu ağa bağlanmayan bir ortamda çalışır; bu yüzden gerçek bir servis yerine, isteği alıp yanıt döndüren bir kapanış kullanılır. Soru şudur: servis yanıt vermezse istemci ne yapar ve sonucu kim belirler?

Ölçü ekseni yine sonucun sahibidir. Karar sırası değişmez: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu derste ağırlıklı taraf üçüncü taraftır: bağlandığımız servis. Çekirdek dosya aynen gelir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```
**VE21.** Servis, bir ürünün fiyatını JSON olarak bildirir; varsayılan fiyat `12.5`'tir. **VE22.** Üçüncü tarafın sapması iki biçimdedir: servis fiyatı `15.5` olarak bildirir ya da ilk çağrıda yanıt vermez ve zaman aşımı olur. **VE23.** Taşıma katmanı tek bir kapanıştır: bir istek nesnesi alır, bir yanıt nesnesi döndürür ya da zaman aşımı istisnası fırlatır. **VE24.** Zaman aşımı yalnız ilk çağrıda olur; ikinci çağrıda servis yanıt verir.

#### İstek, Yanıt ve Taşıma Katmanı

Bir HTTP isteği dört parçadan oluşur: yöntem, URL, başlıklar ve gövde. Gerçek bir yanıtın da başlıkları vardır; bu derste yanıt yalnız durum kodu ve gövdeyle modellenir. Bunları iki küçük sınıfla modelleyebiliriz. Taşıma katmanı, yani isteği gerçekten bir soket üzerinden yollayan parça, istemciden ayrı tutulur ve bir kapanış olarak verilir. Bu ayrımın iki yararı vardır: istemci mantığı ağsız denenebilir ve taşıma katmanı değiştiğinde istemci değişmez.

```php
<?php
// istemci.php — istek, yanıt, zaman aşımı ve fiyat istemcisi
declare(strict_types=1);

final class Istek {
    public function __construct(
        public readonly string $yontem,
        public readonly string $url,
        public readonly array $basliklar = [],
        public readonly string $govde = '',
    ) {}
}

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

final class ZamanAsimi extends RuntimeException {}

function servisKur(?string $sapma): callable {
    $cagri = 0;
    return function (Istek $istek) use ($sapma, &$cagri): Yanit {
        $cagri++;
        if ($sapma === 'zaman' && $cagri === 1) {
            throw new ZamanAsimi('servis yanıt vermedi');
        }
        $fiyat = $sapma === 'yeni' ? 15.5 : 12.5;
        return new Yanit(200, json_encode(['fiyat' => $fiyat]));
    };
}

function fiyatAl(callable $tasima, int $deneme = 1, ?float $varsayilan = null): ?float {
    $istek = new Istek('GET', 'https://servis.test/urun/k1', ['Accept' => 'application/json']);
    for ($i = 0; $i < $deneme; $i++) {
        try {
            $yanit = $tasima($istek);
            return (float) json_decode($yanit->govde, true, flags: JSON_THROW_ON_ERROR)['fiyat'];
        } catch (ZamanAsimi) {
            continue;
        }
    }
    return $varsayilan;
}
```

`servis.test` adı yalnız bir örnek alan adıdır; kapanış onu hiç çözümlemez. `ZamanAsimi` sınıfı, taşıma katmanının "yanıt gelmedi" durumunu bir istisna olarak taşır. `fiyatAl()` fonksiyonu isteği oluşturur, taşıma kapanışına verir, yanıt gövdesini ayrıştırır ve zaman aşımında yeniden dener; deneme hakkı bitince varsayılan değeri döndürür. Önce üç temel durumu görelim.

```php
<?php
// temel.php — istek nesnesi ve üç çağrı
declare(strict_types=1);
require 'istemci.php';

$istek = new Istek('GET', 'https://servis.test/urun/k1', ['Accept' => 'application/json']);
echo $istek->yontem, ' ', $istek->url, "\n";
echo 'başlık: ', json_encode($istek->basliklar), "\n";

$yanit = servisKur(null)($istek);
echo 'durum: ', $yanit->durum, ', gövde: ', $yanit->govde, "\n";

var_dump(fiyatAl(servisKur(null)));
var_dump(fiyatAl(servisKur('zaman')));
var_dump(fiyatAl(servisKur('zaman'), 2));
var_dump(fiyatAl(servisKur('zaman'), 1, 10.0));
```

```
GET https://servis.test/urun/k1
başlık: {"Accept":"application\/json"}
durum: 200, gövde: {"fiyat":12.5}
float(12.5)
NULL
float(12.5)
float(10)
```

Servis sağlıklıyken fiyat `12.5` geldi. İlk çağrıda zaman aşımı olan bir serviste tek denemeyle ve varsayılan değer olmadan sonuç `NULL`; fiyat bilinmiyor. İki denemeyle ikinci çağrı yanıt verdi ve fiyat geldi. Tek deneme ve varsayılan değerle fiyat, servisin söylemediği `10.0` oldu. Üç savunmanın üç ayrı sonucu var; hangisinin doğru olduğu uygulamanın kararıdır.

#### Servisin Bildirdiği Fiyat

Servis fiyatı `15.5` olarak bildirirse istemci bunu olduğu gibi aktarır. İstemci fiyatı hesaplamaz, yalnız taşır. Bu durumda sonuç, kodun hiçbir satırı değişmeden, değişir.

```php
<?php
// fiyatsonuc.php — servisin bildirdiği fiyat kimin?
declare(strict_types=1);
require 'taraf.php';
require 'istemci.php';

tarafRaporu('fiyat', fn(array $s) => (string) fiyatAl(servisKur($s['üçüncü taraf'] ?? null)), ['üçüncü taraf' => 'yeni']);
```

```
fiyat → üçüncü taraf
    üçüncü taraf değiştirdi
```

Fiyatı değiştiren servistir; istemci, ortam ya da sunucu değiştirmedi. Dışarıdan alınan bir değerin sahibi, değeri yazan taraftır.

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

Zaman aşımı olursa iki savunma vardır: yeniden denemek ve varsayılan değere dönmek. İkisi de sonucu sınırlar, yani sonucun en kötü hâlini belirler. Soru şudur: bu savunmalar sonucun sahibini değiştirir mi? Bunu aynı sapmayla, yani servisin ilk çağrıda yanıt vermemesiyle, üç yapılandırmada ölçelim.

```php
<?php
// savunma.php — zaman aşımında sonucun sahibi
declare(strict_types=1);
require 'taraf.php';
require 'istemci.php';

function sonuc(array $s, int $deneme, ?float $varsayilan): string {
    return var_export(fiyatAl(servisKur($s['üçüncü taraf'] ?? null), $deneme, $varsayilan), true);
}

$sapma = ['üçüncü taraf' => 'zaman'];
tarafRaporu('savunmasız', fn(array $s) => sonuc($s, 1, null), $sapma);
tarafRaporu('varsayılan değer', fn(array $s) => sonuc($s, 1, 10.0), $sapma);
tarafRaporu('yeniden deneme', fn(array $s) => sonuc($s, 2, null), $sapma);
```

```
savunmasız → üçüncü taraf
    üçüncü taraf değiştirdi
varsayılan değer → üçüncü taraf
    üçüncü taraf değiştirdi
yeniden deneme → sunucu
    üçüncü taraf değiştiremedi
```

Savunmasız istemcide zaman aşımı sonucu `NULL` yaptı ve sahibi üçüncü taraftır. Varsayılan değer `NULL` yerine `10.0` koydu; sonuç bir sınıra oturdu ama servisin yanıt vermemesi hâlâ sonucu değiştirdi, dolayısıyla sahip yine üçüncü taraftır. Sınır yazıldı, sahip değişmedi. Yeniden denemede ise ikinci çağrı sağlıklı yanıt verdi ve sonuç sapmasız ölçümle aynı çıktı; bu savunma sahibi sunucuya taşıdı. İkisi aynı sapmaya karşı kurulmuş savunmalardır, ama etkileri farklıdır. Savunmanın sonucu sınırlaması ile sahibi değiştirmesi ayrı iki şeydir: varsayılan değer yalnız birincisini yaptı.

Bir uyarı gerekir: yeniden denemenin sahibi sunucuya taşıması, yalnız servisin ikinci çağrıda yanıt vermesi koşuluna bağlıdır. VE24 bunu varsayar. Servis sürekli yanıt vermiyorsa yeniden deneme de sonucu kurtarmaz.

#### Özet

- İstek yöntem, URL, başlık ve gövdeden; yanıt durum kodu ve gövdeden oluşur. Taşıma katmanı bir kapanış olarak ayrıldığında istemci ağsız denenebilir.
- Zaman aşımı bir istisnadır; istemci onu yakalar ve deneme hakkına ya da varsayılan değere bakar.
- Servisin bildirdiği fiyatın sahibi üçüncü taraftır; istemci değeri yalnız taşır.
- Varsayılan değer sonucu sınırlar ama sahibi değiştirmez; bu derste ölçülen sahip yine üçüncü taraftır.
- Yeniden deneme, servis ikinci çağrıda yanıt verdiği sürece sahibi sunucuya taşır.

#### Sonraki Adım

İstemci artık yanıtı alıyor, ayrıştırıyor ve zaman aşımına karşı savunuyor. Ama yanıttan çıkan değer uygulamanın içine girdiğinde ve istemcinin kendi girdileri hesaba katıldığında, bu değerlerin geçerli olup olmadığına kim karar verir? Sonraki konu Güvenlik: Girdi Doğrulama ve Arındırma dersi, bu soruyla başlar.

## Güvenlik

Girdi doğrulama ve arındırma, SQL enjeksiyonu, siteler arası betik çalıştırma, istek sahteciliği, parola saklama ve kimlik doğrulama akışları — her biri bir sonucu istemcinin ya da üçüncü bir tarafın elinden alıp sunucuya taşıyan bir savunma olarak.

### Girdi Doğrulama ve Arındırma

Source: https://academia.sh/tr/kurslar/php-web/girdi-dogrulama-ve-arindirma

filter_var() ile bir istemci girdisini geçerli kümeye daraltmak, doğrulama ile arındırmanın ayrı adımlar olduğunu ve doğrulamanın değeri değiştirme gücünü istemciden almadığını ölçmek.

HTTP İstemcisi dersinde dış bir servisin yanıtı gecikince sonucu bir zaman aşımı istisnası sınırladı, ama fiyat yine üçüncü tarafın elinde kaldı. Güvenlik konusu aynı soruyu uygulamaya doğrudan gelen veriye çevirir: sorgu parametresi, form alanı ya da çerez olarak gelen bir değer, istemcinin gönderdiği şeydir. **Bu girdi geçerli mi ve geçerliyse sonucu kim değiştirebiliyor?**

**GÜ1.** Bu konunun bütün derslerinde istemci girdisi bir dizge sabitidir; gerçek bir istek kurulmaz. Geçersiz girdi nötr biçim hatalarıyla gösterilir (`'4x'`, `''`, `'ayse@'`), özel karakter dizgeleriyle değil. Koşan kod savunma tarafındadır. Rust müfredatının ileri kursunda bir güvencenin hangi denetimde sona erdiği ölçülmüştü; burada denetimin kendisi değil, sonucun kimin elinde kaldığı sorulur.

#### Doğrulama: Geçerli Küme

**Girdi doğrulama (input validation)**, gelen değerin beklenen biçimde ve beklenen aralıkta olup olmadığına karar verir ve değilse değeri reddeder. PHP'de bu işin yerleşik aracı `filter_var()`'dır: ikinci bağımsız değişkeni bir süzgeç sabiti, üçüncüsü seçenekler dizisidir. `FILTER_VALIDATE_INT` tamsayı biçimini, `min_range` ve `max_range` seçenekleri aralığı denetler; uyan girdide tamsayıyı, uymayanda `false` döndürür. `FILTER_VALIDATE_EMAIL` aynı sözleşmeyle e-posta biçimini sınar.

Ders boyunca iki küçük doğrulayıcı kullanılır. Alt çizgili sabit adlar PHP geleneğindedir; dosya yalnız diske yazılır ve ölçüm dosyaları onu çeker.

```php
<?php
// girdi.php — adet ve e-posta doğrulayıcıları
declare(strict_types=1);

const ADET_SECENEKLERI = ['options' => ['min_range' => 1, 'max_range' => 10]];

function adetDogrula(string $girdi): int|false {
    return filter_var($girdi, FILTER_VALIDATE_INT, ADET_SECENEKLERI);
}

function epostaDogrula(string $girdi): string|false {
    return filter_var($girdi, FILTER_VALIDATE_EMAIL);
}
```

Sipariş adedi 1 ile 10 arasında olmalıdır. Aşağıdaki ölçüm dört adet girdisini ve iki e-posta girdisini doğrulayıcılardan geçirir, ardından 0'dan 12'ye kadar her tamsayıyı dener ve kaçının kabul edildiğini sayar.

```php
<?php
// dogrula.php — geçerli ve geçersiz biçimler
declare(strict_types=1);
require 'girdi.php';

foreach (['4', '4x', '', '11'] as $girdi) {
    echo 'adet ', var_export($girdi, true), ' → ', var_export(adetDogrula($girdi), true), "\n";
}
foreach (['ayse@ornek.test', 'ayse@'] as $girdi) {
    echo 'e-posta ', var_export($girdi, true), ' → ', var_export(epostaDogrula($girdi), true), "\n";
}
$kabul = array_filter(range(0, 12), fn(int $n) => adetDogrula((string) $n) !== false);
echo count($kabul), ' değer kabul edildi: ', min($kabul), ' ile ', max($kabul), " arası\n";
```

```
adet '4' → 4
adet '4x' → false
adet '' → false
adet '11' → false
e-posta 'ayse@ornek.test' → 'ayse@ornek.test'
e-posta 'ayse@' → false
10 değer kabul edildi: 1 ile 10 arası
```

Geçerli biçim değeri tamsayıya çevirerek geçirir: `'4'` metni `4` olur. `'4x'` ve boş dizge biçim hatasıdır, `'11'` biçimce geçerli ama aralığın dışındadır; üçü de `false` döndürür. Dönüş tipi bu yüzden `int|false`'tır ve çağıran `false`'u açıkça denetlemek zorundadır; `false` bir sayı olarak kullanılırsa sıfır gibi davranır. Sayım, doğrulayıcının tanımladığı kümeyi gösterir: 13 değerden yalnız 1 ile 10 arasındaki on tanesi geçer.

#### Dönüş Değerini Denetlemek

`int|false` dönüşünün bir tuzağı vardır: geçerli sonuç `0` olabilir ve `0`, PHP'de `false` gibi yanlış sayılır. Aşağıdaki ölçüm, alt sınırı sıfır olan bir doğrulayıcıyı iki biçimde denetler: gevşek karşılaştırmayla (`$sonuc == false`) ve katı karşılaştırmayla (`$sonuc === false`).

```php
<?php
// sifir.php — sıfır geçerli bir değer olduğunda
declare(strict_types=1);

function stokDogrula(string $girdi): int|false {
    return filter_var($girdi, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 10]]);
}

foreach (['0', '5', '4x'] as $girdi) {
    $sonuc = stokDogrula($girdi);
    echo var_export($girdi, true), ' → ', var_export($sonuc, true),
        ' · gevşek red ', var_export($sonuc == false, true),
        ' · katı red ', var_export($sonuc === false, true), "\n";
}
```

```
'0' → 0 · gevşek red true · katı red false
'5' → 5 · gevşek red false · katı red false
'4x' → false · gevşek red true · katı red true
```

Gevşek karşılaştırma geçerli `'0'` girdisini de reddeder: `0 == false` doğrudur. Katı karşılaştırma yalnız gerçek `false`'u, yani `'4x'` biçim hatasını yakalar. Doğrulayıcının sözleşmesi bu yüzden `=== false` ile okunur; geçerli kümede sıfır varsa gevşek denetim sessizce bir geçerli değeri kaybettirir. Hata bu kez güvenlik tarafında değil fonksiyon tarafındadır, ama aynı kökten gelir: doğrulamanın sonucu yanlış okunursa, doğrulamanın kendisi bir şey kanıtlamaz.

#### Taraf: Doğrulama Sahibi Değiştirmez

Şimdi kursun sorusunu bu sonuca soralım. Çekirdek dosya, her derste aynı biçimde verilen `taraf.php`'dir; `DIS_TARAFLAR` kodun dışındaki üç tarafı karar sırasıyla tutar, `sina()` sonucu önce sapmasız sonra her sapmayla ayrı ayrı hesaplar, `taraf()` ilk değiştireni, hiçbiri değiştiremediyse sunucuyu seçer. Sıra saldırı yüzeyinin genişliğidir: istemci her isteği gönderen herkestir, üçüncü taraf yalnız uygulamanın bağlandığı taraflardır, ortam yalnız dağıtımı yapanın elindedir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Ölçülen sonuç, doğrulanmış adettir. Sapmasız istekte istemci `'3'` gönderir; istemcinin sapması başka bir geçerli sayıdır, `'7'`.

```php
<?php
// adet.php — doğrulanmış adedin tarafı
declare(strict_types=1);
require 'taraf.php';
require 'girdi.php';

tarafRaporu('adet (doğrulanmış)', fn(array $s) => adetDogrula($s['istemci'] ?? '3'), ['istemci' => '7']);
```

```
adet (doğrulanmış) → istemci
    istemci değiştirdi
```

Sonuç **istemcidir**. Doğrulama `'7'`'yi reddetmedi, çünkü `'7'` geçerli kümenin içindedir; sonuç `3`'ten `7`'ye geçti. Doğrulama istemcinin değeri değiştirmesini önlemez. Yaptığı, istemcinin değiştirebileceği değerleri sayımdaki on tamsayıya daraltmaktır: `'4x'`, `''` ve `'11'` sonucu `false`'a çevirir ve uygulamanın geri kalanına hiç ulaşmaz. Daralma bir kazançtır, ama sahip değişmemiştir; adedi hâlâ isteği gönderen belirler.

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

Doğrulamanın yanında sık karıştırılan ikinci bir adım vardır. **Arındırma (sanitization)** girdiyi reddetmez, değiştirir: `FILTER_SANITIZE_NUMBER_INT` rakam, artı ve eksi dışındaki her karakteri siler ve kalanı kabul eder. İki adım ayrıdır: doğrulama "bu değer geçerli mi" diye sorar ve hayır diyebilir; arındırma hayır diyemez. Aynı üç girdiyi her ikisinden de geçirelim, ayrıca arındırılmış değeri doğrulayıcıya verelim.

```php
<?php
// arindir.php — arındırma ile doğrulama yan yana
declare(strict_types=1);
require 'girdi.php';

foreach (['4', '4x', '1x0'] as $girdi) {
    $arindirilmis = filter_var($girdi, FILTER_SANITIZE_NUMBER_INT);
    echo var_export($girdi, true), ' → arındırma ', var_export($arindirilmis, true),
        ' · doğrulama ', var_export(adetDogrula($girdi), true),
        ' · arındırıp doğrulama ', var_export(adetDogrula($arindirilmis), true), "\n";
}
```

```
'4' → arındırma '4' · doğrulama 4 · arındırıp doğrulama 4
'4x' → arındırma '4' · doğrulama false · arındırıp doğrulama 4
'1x0' → arındırma '10' · doğrulama false · arındırıp doğrulama 10
```

Sonuç beklenen yöndedir ve rahatsız edicidir. Doğrulama `'4x'` ve `'1x0'` girdilerini reddeder; arındırma ikisini de kabul eder, üstelik değiştirerek: `'4x'` sessizce `4`, `'1x0'` sessizce `10` olur. Arındırılmış değer doğrulayıcıdan geçer, çünkü arındırma bozukluğu silmiştir. Girdinin bozuk olduğu bilgisi bu noktada kaybolur: uygulama dört adetlik bir sipariş alır, kullanıcı ise `4x` yazmıştır. Sınırlayıcı sonuç şudur: arındırma doğrulamanın yerini tutmaz. Bozuk girdi önce doğrulamayla reddedilir; arındırma, değerin reddedilemeyeceği, yalnız güvenli biçime getirilmesi gereken yerlerde kullanılır.

#### Özet

- Bu dersin ölçü sınıfı **istemci**: doğrulanmış adedi istemci `'3'`'ten `'7'`'ye değiştirdi; doğrulama bunu önlemedi.
- `filter_var()` ile `FILTER_VALIDATE_INT` ve aralık seçenekleri girdiyi geçerli kümeye daraltır: 13 değerden yalnız on tanesi geçti; geçersiz biçim `false` döndürür.
- Doğrulama reddeder, arındırma değiştirip kabul eder; ikisi ayrı adımdır ve yerine geçmez.
- Arındırma, bozuk bir girdiyi sessizce başka bir değere çevirir; reddedilmesi gereken girdi sonuca değer olarak girer.
- Savunma her zaman sahibi değiştirmez: doğrulama sonucu sunucuya taşımaz, yalnız istemcinin değiştirebileceklerini daraltır.

#### Sonraki Adım

Doğrulanmış bir değer, geçerli kümede kaldığı sürece hâlâ istemcinin seçtiği bir değerdir. Peki bu değer bir sorgu metninin içine girerse, metnin kendisini kim belirler? SQL Enjeksiyonu Önleme dersi, hazır deyimin sorgu metnini istemciden alıp sunucuya taşıdığını ölçer.

### SQL Enjeksiyonu Önleme

Source: https://academia.sh/tr/kurslar/php-web/sql-enjeksiyonu-onleme

Sorgu metnini istemcinin değerinden ayıran parametreli sorguyu ölçmek: aynı sorgu birleştirmede değer değişince değişir, hazır deyimde değişmez, ve bağlama her yerde çalışmaz.

Girdi Doğrulama ve Arındırma dersinde doğrulanmış adedi yine istemci belirliyordu; doğrulama yalnız geçerli kümeyi daralttı. Aynı değer bir sorgu metninin içine girerse soru değişir: sorgunun **metnini** kim belirliyor? Metin istemcinin değerinden oluşuyorsa, sorgunun ne yapacağı da ondan oluşur.

**GÜ2.** Bu derste sorguya giren değer nötr bir addır: `ayse` ve `mehmet`. İki ad da geçerli kullanıcı adıdır; ölçülen şey, adın değişmesiyle sorgu metninin değişip değişmediğidir. Sorgu metnini bozacak girdiler yazılmaz ve denenmez. Bütün sorgular bellekteki veritabanında koşar.

#### Ortak Veri

Ölçümler aynı küçük tabloyu kullanır: üç kullanıcı, kasıtlı olarak alfabe dışı sırayla eklenmiş. Dosya yalnız diske yazılır.

```php
<?php
// veri.php — bellekteki veritabanı ve üç kullanıcı
declare(strict_types=1);

function baglan(): PDO {
    $pdo = new PDO('sqlite::memory:');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->exec('CREATE TABLE kullanicilar (id INTEGER PRIMARY KEY, ad TEXT)');
    foreach (['mehmet', 'zeynep', 'ayse'] as $ad) {
        $pdo->prepare('INSERT INTO kullanicilar (ad) VALUES (?)')->execute([$ad]);
    }
    return $pdo;
}
```

#### Birleştirme ve Parametreli Sorgu

**Birleştirilmiş sorguda** değer, metnin parçası olur. **Parametreli sorguda (prepared statement)** metin sabittir ve değer ayrı bir adımda **bağlanır (binding)**: `prepare()` önce yalnızca yer tutuculu metni hazırlar, `execute()` değeri o metne yerleştirmeden, ayrı iletir. Hazır deyimin elindeki metne `queryString` özelliğinden bakılabilir; aşağıdaki ölçüm iki yöntemin sorgu metnini iki adla karşılaştırır.

```php
<?php
// sorgu.php — iki yöntemin sorgu metni
declare(strict_types=1);
require 'veri.php';

function birlestirmeMetni(string $ad): string {
    return "SELECT id, ad FROM kullanicilar WHERE ad = '" . $ad . "'";
}

function parametreliMetin(PDO $pdo, string $ad): string {
    $deyim = $pdo->prepare('SELECT id, ad FROM kullanicilar WHERE ad = ?');
    $deyim->execute([$ad]);
    return $deyim->queryString;
}
```

Dosya yalnız fonksiyonları tanımlar. Ölçüm, iki adla iki yöntemi yan yana çağırır.

```php
<?php
// karsilastir.php — iki yöntemde sorgu metni
declare(strict_types=1);
require 'sorgu.php';

$pdo = baglan();
foreach (['ayse', 'mehmet'] as $ad) {
    echo 'birleştirme  ', birlestirmeMetni($ad), "\n";
    echo 'parametreli  ', parametreliMetin($pdo, $ad), "\n";
}
```

```
birleştirme  SELECT id, ad FROM kullanicilar WHERE ad = 'ayse'
parametreli  SELECT id, ad FROM kullanicilar WHERE ad = ?
birleştirme  SELECT id, ad FROM kullanicilar WHERE ad = 'mehmet'
parametreli  SELECT id, ad FROM kullanicilar WHERE ad = ?
```

Birleştirmede ad değişince sorgu metninin kendisi değişti: `'ayse'` yerine `'mehmet'` metne yazıldı. Parametreli sorguda metin iki adda da aynı kaldı; ad metnin dışında, bağlanan değer olarak taşındı. Bu fark güvenlik özelliğinin tamamıdır: metnin değeri istemciden alınmış olması, metnin yapısını istemcinin değiştirebileceği anlamına gelir. Metin sabit olduğunda değer ne olursa olsun yalnız değer olarak kalır.

#### Taraf: Metni Kim Belirliyor

Aynı ölçümü kursun sorusuna bağlayalım. Çekirdek dosya, her derste olduğu gibi `taraf.php`'dir; `sina()` sonucu sapmasız hesaplar, sonra her dış tarafın sapmasıyla yeniden hesaplar ve `taraf()` değiştiren ilk tarafı seçer. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Sonuç, `prepare()`'e giden sorgu metnidir. İstemcinin sapması `ayse` yerine `mehmet` göndermektir.

```php
<?php
// enjeksiyon.php — sorgu metninin tarafı
declare(strict_types=1);
require 'taraf.php';
require 'sorgu.php';

$pdo = baglan();
tarafRaporu('birleştirme', fn(array $s) => birlestirmeMetni($s['istemci'] ?? 'ayse'), ['istemci' => 'mehmet']);
tarafRaporu('parametreli', fn(array $s) => parametreliMetin($pdo, $s['istemci'] ?? 'ayse'), ['istemci' => 'mehmet']);
```

```
birleştirme → istemci
    istemci değiştirdi
parametreli → sunucu
    istemci değiştiremedi
```

Birleştirmede sonucu **istemci** değiştirdi. Parametreli sorguda metni değiştirebilecek üç taraf da değiştiremedi; kalan sahip **sunucudur**, çünkü metin kodda sabit yazılmıştır. Burada kaçışlama ya da doğrulama gibi bir daralma yoktur: sorgu metninin sahibi tümüyle el değiştirmiştir. Doğrulama değerin kümesini daraltır ve istemci kalır; hazır deyim ise metni istemcinin erişemeyeceği bir yere koyar.

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

Bağlama her yerde çalışmaz: yer tutucu yalnız **değerin** yerine konur; sütun adı, tablo adı ya da sıralama yönü gibi sorgu yapısının parçaları bağlanamaz. `ORDER BY ?` yazıp sütun adını bağlayan kişi, sıralamayı bağlamış olmaz; sayı ya da dizge sabiti bağlamış olur. Aşağıda aynı `kullanicilar` tablosu `ad` sütununa göre sıralanmak istenir; bağlama ile sıralama istenen sütunun adı verilir.

```php
<?php
// siralama.php — bağlama ve izin listesi
declare(strict_types=1);
require 'veri.php';

const SIRALAMA_IZINLERI = ['ad' => 'ad', 'id' => 'id'];

function baglamaIleSirala(PDO $pdo, string $sutun): array {
    $deyim = $pdo->prepare('SELECT ad FROM kullanicilar ORDER BY ?');
    $deyim->execute([$sutun]);
    return $deyim->fetchAll(PDO::FETCH_COLUMN);
}

function izinListesiyleSirala(PDO $pdo, string $istek): array {
    $sutun = SIRALAMA_IZINLERI[$istek] ?? 'id';
    return $pdo->query('SELECT ad FROM kullanicilar ORDER BY ' . $sutun)->fetchAll(PDO::FETCH_COLUMN);
}

$pdo = baglan();
echo 'bağlama      ', implode(', ', baglamaIleSirala($pdo, 'ad')), "\n";
echo 'izin listesi ', implode(', ', izinListesiyleSirala($pdo, 'ad')), "\n";
echo 'listede yok  ', implode(', ', izinListesiyleSirala($pdo, 'sifre')), "\n";
```

```
bağlama      mehmet, zeynep, ayse
izin listesi ayse, mehmet, zeynep
listede yok  mehmet, zeynep, ayse
```

Bağlamayla sıralama beklenen sonucu vermedi: satırlar `ad` sırasıyla değil, eklenme sırasıyla geldi (`mehmet, zeynep, ayse`). Bağlanan `'ad'` bir sütun adı olarak değil, her satırda aynı olan bir dizge değeri olarak okundu ve sıralama bir şey değiştirmedi. Sorgu hata da vermedi; yanlışlık sessizdir. İzin listesi sıralamayı gerçekten uyguladı (`ayse, mehmet, zeynep`) ve listede olmayan `sifre` isteği hataya düşmeden varsayılan `id` sırasına döndü. Sütun adı sorguya girerse de girdiği yer kodda sabit yazılı bir listedir; istemcinin değeri yalnız listeden bir anahtar seçer.

#### Özet

- Bu dersin ölçü sınıfı birleştirmede **istemci**, parametreli sorguda **sunucu**: sorgu metnini ikincisinde yalnızca `prepare()`'e verilen sabit dizge belirler.
- Birleştirmede adı `ayse`'den `mehmet`'e değiştirmek sorgu metnini değiştirdi; parametreli sorguda metin iki adda da aynı kaldı.
- Hazır deyim ve bağlama değeri metinden ayırır; savunma sonucu sahibinden alır, daraltmakla yetinmez.
- Bağlama yalnız değer içindir: `ORDER BY ?` sıralamayı değiştirmedi ve sütun adı bağlanamadı.
- Sorgu yapısının parçası istemciden geliyorsa çözüm, kod içinde sabit bir izin listesinden seçmektir; listede olmayan istek varsayılana düşer.

#### Sonraki Adım

Sorgu metnini sunucuya almak bir yöndeki yolu kapattı. Aynı değer bu kez bir sayfaya basılırsa, çıktının metnini kim belirler? Siteler Arası Betik Çalıştırma Önleme dersi, çıktı anında kaçışlamayı aynı taraf sorusuyla ölçer.

### Siteler Arası Betik Çalıştırma Önleme

Source: https://academia.sh/tr/kurslar/php-web/siteler-arasi-betik-calistirma-onleme

Kullanıcıdan gelen metni sayfaya basarken kaçışlamanın sonucu nasıl daralttığını, bağlama göre kaçışlamayı ve çift kaçışlamayı ölçmek; kaçışlama sahibi değiştirmez.

SQL Enjeksiyonu Önleme dersinde değer sorgu metninden ayrıldı ve sorgunun sahibi sunucu oldu. Değerin gittiği başka bir yer sayfadır: kullanıcı bir ad girer, uygulama bu adı HTML'e basar. **Siteler arası betik çalıştırma (cross-site scripting, XSS)** önleme sorusu şudur: basılan metnin yapısını, yani hangi parçasının işaretleme sayılacağını kim belirliyor?

**GÜ3.** Bu derste kaçışlanan metinler dört zararsız örnektir: `Tom & Jerry`, `<b>kalın</b>`, `"tırnak"` ve `O'Brien`. Amaç, işaretleme karakterlerinin metin olarak çıkıp çıkmadığını ölçmektir; çalışan bir betik üretmek ya da başka bir siteden istek yapmak ölçülmez. Koşan kod savunma tarafındadır.

#### Kaçışlama: Metni Metin Olarak Basmak

**Kaçışlama (escaping)**, işaretleme dilinde özel anlam taşıyan karakterleri, aynı karakteri metin olarak gösteren karşılıklarla değiştirir. PHP'de bu işi `htmlspecialchars()` yapar: `&`, `<`, `>`, çift tırnak ve tek tırnak için `&amp;`, `&lt;`, `&gt;`, `&quot;` ve `&#039;` yazar. Aşağıdaki dosya iki fonksiyon tanımlar: ham çıktı üreten ve kaçışlayan. Ham çıktıyı üreten fonksiyon yalnız karşıtlık için vardır.

```php
<?php
// cikti.php — ham ve kaçışlanmış çıktı
declare(strict_types=1);

function kacisla(string $metin): string {
    return htmlspecialchars($metin, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function selamlamaHam(string $ad): string {
    return '<p>Merhaba ' . $ad . '</p>';
}

function selamlama(string $ad): string {
    return '<p>Merhaba ' . kacisla($ad) . '</p>';
}

function girdiAlani(string $ad): string {
    return '<input value="' . kacisla($ad) . '">';
}
```

Dört örnek metni önce `kacisla()`'dan, sonra iki selamlama fonksiyonundan geçirelim. Ham çıktıda `<b>kalın</b>` sayfanın yapısına karışır; kaçışlanan çıktıda `<` karakteri sayısı sabit kalır.

```php
<?php
// kacisla.php — kaçışlanan karakterler
declare(strict_types=1);
require 'cikti.php';

foreach (['Tom & Jerry', '<b>kalın</b>', '"tırnak"', "O'Brien"] as $metin) {
    echo $metin, ' → ', kacisla($metin), "\n";
}
echo "\n";
foreach (['ayse', '<b>kalın</b>'] as $ad) {
    echo mb_str_pad($ad, 14), ' ham ', substr_count(selamlamaHam($ad), '<'),
        ' · kaçışlı ', substr_count(selamlama($ad), '<'), " etiket başlangıcı\n";
}
```

```
Tom & Jerry → Tom &amp; Jerry
<b>kalın</b> → &lt;b&gt;kalın&lt;/b&gt;
"tırnak" → &quot;tırnak&quot;
O'Brien → O&#039;Brien

ayse           ham 2 · kaçışlı 2 etiket başlangıcı
<b>kalın</b>   ham 4 · kaçışlı 2 etiket başlangıcı
```

Dört metin de metin olarak çıktı: `&`, `<`, `>`, çift ve tek tırnak karşılıklarına döndü. Sayım yapıyı gösterir. `ayse` adında ham ve kaçışlı çıktı aynıdır, iki etiket başlangıcı vardır (`<p>` ve `</p>`). `<b>kalın</b>` adında ham çıktı dört etiket başlangıcına çıkar; kaçışlı çıktı ikide kalır. Ham çıktıda istemcinin değeri sayfanın yapısına iki etiket ekledi, kaçışlıda ekleyemedi.

#### Bağlama Göre Kaçışlama

Aynı fonksiyon her bağlamda yeterli değildir; yeterli olduğu bağlam, çıktının nereye gittiğiyle belirlenir. Metin düğümünde `<` ve `&` yeterlidir. Çift tırnaklı bir **öznitelik** değerinde tırnağın da kaçışlanması gerekir, çünkü değer tırnakla biter. `ENT_QUOTES` bayrağı ikisini de kapsar. `girdiAlani()` bu bağlamı kullanır.

```php
<?php
// baglam.php — öznitelik içi kaçışlama
declare(strict_types=1);
require 'cikti.php';

echo girdiAlani('"tırnak"'), "\n";
echo girdiAlani("O'Brien"), "\n";
echo selamlama("O'Brien"), "\n";
```

```
<input value="&quot;tırnak&quot;">
<input value="O&#039;Brien">
<p>Merhaba O&#039;Brien</p>
```

Öznitelik bağlamında çift tırnak `&quot;` olarak çıktı, yani değeri erken bitiremez. Tek tırnak her iki bağlamda `&#039;` olur. Sonuç şudur: bağlam adlandırılmadan "kaçışladım" denmez. Metin düğümü için yeterli olan, tırnaksız bir özniteliğe ya da başka bir bağlama (betik içeriği, adres) aynen taşınmaz; o bağlamlar için ayrı kaçışlama gerekir ve bu ders yalnızca metin düğümü ile çift tırnaklı özniteliği kapsar.

#### Taraf: Kaçışlama Sahibi Değiştirmez

Aynı sorunun taraf ölçümü, `taraf.php` çekirdeğiyle yapılır. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Sonuç, kaçışlanmış ve kaçışlanmamış selamlama HTML'idir. İstemcinin sapması farklı bir addır: `<b>kalın</b>`.

```php
<?php
// xss.php — basılan HTML'in tarafı
declare(strict_types=1);
require 'taraf.php';
require 'cikti.php';

tarafRaporu('ham çıktı', fn(array $s) => selamlamaHam($s['istemci'] ?? 'ayse'), ['istemci' => '<b>kalın</b>']);
tarafRaporu('kaçışlı çıktı', fn(array $s) => selamlama($s['istemci'] ?? 'ayse'), ['istemci' => '<b>kalın</b>']);
```

```
ham çıktı → istemci
    istemci değiştirdi
kaçışlı çıktı → istemci
    istemci değiştirdi
```

İki çıktının sahibi de **istemcidir**. Kaçışlama bu sahibi değiştirmedi, çünkü basılan metin hâlâ istemcinin gönderdiği ada bağlıdır: ad değişince HTML değişir. Değişen, bu değişimin türüdür. Ham çıktıda istemci sayfanın yapısını da değiştirebiliyordu, kaçışlıda yalnız bir metin düğümünün içeriğini değiştirebilir. Kaçışlama sonucu sunucuya taşımaz, istemcinin elindeki alanı daraltır; sahip aynı kalır.

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

Kaçışlama tek yerde yapılmalıdır. Bir değer depolanırken kaçışlanır, basılırken yine kaçışlanırsa karakterler iki kez dönüştürülür: `&` önce `&amp;` olur, sonra o `&` yeniden `&amp;` olur. Ölçüm, aynı metni bir kez ve iki kez kaçışlar.

```php
<?php
// cift.php — çift kaçışlama
declare(strict_types=1);
require 'cikti.php';

$metin = 'Tom & Jerry';
$saklanan = $metin;
echo 'bir kez   ', kacisla($saklanan), "\n";
echo 'iki kez   ', kacisla(kacisla($saklanan)), "\n";
```

```
bir kez   Tom &amp; Jerry
iki kez   Tom &amp;amp; Jerry
```

İkinci kaçışlama, ilkinin yazdığı `&amp;` içindeki `&` karakterini yeniden kaçışladı ve tarayıcıda `Tom &amp; Jerry` metni görünür oldu. Çift kaçışlama güvenlik açığı değildir, görünür bir hatadır; ama onu "düzeltmek" için kaçışlamayı kapatmak ya da bir yere atlamak ham çıktıyı geri getirir. Doğru düzeltme kaçışlamayı tek yere koymaktır: değer depoda ham durur ve basıldığı anda, basıldığı bağlama göre bir kez kaçışlanır.

#### Özet

- Bu dersin ölçü sınıfı **istemci**: kaçışlanmış selamlamada da basılan HTML'i, istemcinin gönderdiği ad değiştirdi.
- Kaçışlama sahibi değiştirmez, daraltır: `<b>kalın</b>` ham çıktıda etiket sayısını artırdı, kaçışlı çıktıda artırmadı; sayfanın yapısı istemcinin değeriyle genişlemedi.
- Aynı fonksiyon metin düğümünde ve çift tırnaklı özniteliğte yeterlidir, `ENT_QUOTES` ile tek ve çift tırnak da kaçışlanır; bağlam açıkça seçilir.
- Çift kaçışlama metni bozar: `Tom &amp;amp; Jerry`. Kaçışlama yalnız çıktı anında, tek yerde yapılır; depoda ham değer durur.

#### Sonraki Adım

Kaçışlama sayfaya basılan metni sınırladı. Sayfadan gelen bir istek ise başka bir siteden de gelebilir: isteği gerçekten kullanıcının mı yaptığını kim belirliyor? İstek Sahteciliği Koruması dersi bunu bir belirteçle ölçer.

### İstek Sahteciliği Koruması

Source: https://academia.sh/tr/kurslar/php-web/istek-sahteciligi-korumasi

Bir isteği kullanıcının kendi uygulamasının mı yoksa başka bir sitenin mi yaptığını sahtecilik belirteciyle ayırmak, belirtecin ölçülen etkisi ve SameSite çerez niteliğinin sınırı.

Kaçışlama sayfaya basılan metni sınırladı. İstek ise başka yönden gelir: kullanıcı uygulamada oturum açıkken başka bir sitedeki bir sayfa, kullanıcının tarayıcısından uygulamaya bir istek gönderebilir. **Siteler arası istek sahteciliği (cross-site request forgery, CSRF)** önleme sorusu şudur: bu isteğin kullanıcının kendi ekranından mı yoksa başka bir yerden mi geldiğini kim belirliyor? Bu ders isteğin kabul edilip edilmediğini ölçer; reddedilen isteğin kodu koştu mu diye sormaz.

**GÜ4.** Bu derste "başka site" yalnız bir dizi kaydıdır: `'koken' => 'https://baska.test'`. Gerçek bir ikinci site, çerez ya da tarayıcı kurulmaz. İstekler dizidir; ölçülen şey, e-posta güncellemesinin kabul edilip edilmediğidir. Belirteç hiçbir yerde basılmaz, yalnız uzunluğu basılır.

#### Belirteçsiz ve Belirteçli Güncelleme

**Sahtecilik belirteci (CSRF token)**, sunucunun kullanıcıya gösterdiği forma koyduğu ve istekle birlikte geri beklediği rastgele bir değerdir. Kullanıcının kendi ekranından gelen istek bu değeri taşır; başka bir sitedeki sayfa değeri bilmediği için taşıyamaz. Belirteç `random_bytes()` ile üretilir, `bin2hex()` ile okunabilir hâle getirilir ve karşılaştırma `hash_equals()` ile yapılır: bu fonksiyon iki dizgeyi, farkın ilk bulunduğu konumdan bağımsız bir sürede karşılaştırır.

```php
<?php
// istek.php — korumasız ve belirteçli e-posta güncellemesi
declare(strict_types=1);

function belirtecUret(): string {
    return bin2hex(random_bytes(16));
}

function guncelle(string $mevcut, array $istek, ?string $beklenen): string {
    if ($beklenen !== null && !hash_equals($beklenen, (string) ($istek['belirtec'] ?? ''))) {
        return $mevcut;
    }
    return $istek['eposta'];
}
```

`guncelle()` yeni e-postayı döndürür; istek reddedilirse mevcut değeri olduğu gibi geri verir. `$beklenen` boşsa koruma yoktur. Üç istek iki yöntemle denenir: kullanıcının kendi ekranından, belirteçsiz başka siteden, yanlış belirteçli başka siteden.

```php
<?php
// guncelle.php — üç istek, iki yöntem
declare(strict_types=1);
require 'istek.php';

$mevcut = 'ayse@ornek.test';
$belirtec = belirtecUret();
$istekler = [
    'uygulamadan    ' => ['koken' => 'https://uygulama.test', 'eposta' => 'yeni@ornek.test', 'belirtec' => $belirtec],
    'belirteçsiz    ' => ['koken' => 'https://baska.test', 'eposta' => 'baska@ornek.test'],
    'yanlış belirteç' => ['koken' => 'https://baska.test', 'eposta' => 'baska@ornek.test', 'belirtec' => str_repeat('0', 32)],
];
foreach ($istekler as $ad => $istek) {
    $korumasiz = guncelle($mevcut, $istek, null) !== $mevcut;
    $korumali = guncelle($mevcut, $istek, $belirtec) !== $mevcut;
    echo $ad, ' · korumasız ', $korumasiz ? 'kabul' : 'ret', ' · belirteçli ', $korumali ? 'kabul' : 'ret', "\n";
}
echo 'belirteç uzunluğu ', strlen($belirtec), "\n";
echo 'iki belirteç ', belirtecUret() === belirtecUret() ? 'aynı' : 'farklı', "\n";
```

```
uygulamadan     · korumasız kabul · belirteçli kabul
belirteçsiz     · korumasız kabul · belirteçli ret
yanlış belirteç · korumasız kabul · belirteçli ret
belirteç uzunluğu 32
iki belirteç farklı
```

Korumasız yöntem üç isteği de kabul etti; başka sitenin isteği ile kullanıcının kendi isteği arasında fark yoktu. Belirteçli yöntem yalnız belirteci taşıyan isteği kabul etti; belirteçsiz ve yanlış belirteçli istek reddedildi. Belirteç 32 karakterdir (16 bayt, onaltılık yazımla) ve iki üretim farklı değer verdi. Ölçülen şey kabul ya da retten ibarettir; belirtecin kendisi basılmaz, çünkü basılan bir belirteç bir sonraki çalıştırmada başka bir değer olurdu ve gizli bir değerin çıktıda yeri yoktur.

#### Taraf: Güncellemeyi Kim Yapıyor

Aynı ölçümü `taraf.php` çekirdeğine bağlayalım. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Sonuç, güncellemeden sonraki e-postadır. Sapma, üçüncü tarafın belirteçsiz gönderdiği istektir: başka sitenin kaydı ve kendi e-postası. Sapmasız istek, kullanıcının belirteçli kendi isteğidir.

```php
<?php
// sahtecilik.php — e-postayı kim değiştirdi
declare(strict_types=1);
require 'taraf.php';
require 'istek.php';

$belirtec = belirtecUret();
$kendi = ['koken' => 'https://uygulama.test', 'eposta' => 'ayse@ornek.test', 'belirtec' => $belirtec];
$baska = ['koken' => 'https://baska.test', 'eposta' => 'baska@ornek.test'];

tarafRaporu('belirteçsiz', fn(array $s) => guncelle('ayse@ornek.test', $s['üçüncü taraf'] ?? $kendi, null), ['üçüncü taraf' => $baska]);
tarafRaporu('belirteçli', fn(array $s) => guncelle('ayse@ornek.test', $s['üçüncü taraf'] ?? $kendi, $belirtec), ['üçüncü taraf' => $baska]);
```

```
belirteçsiz → üçüncü taraf
    üçüncü taraf değiştirdi
belirteçli → sunucu
    üçüncü taraf değiştiremedi
```

Belirteçsiz güncellemede e-postayı **üçüncü taraf** değiştirdi: başka sitenin isteği kullanıcının e-postasını kendi değeriyle değiştirdi. Belirteçli güncellemede üçüncü taraf değiştiremedi; kalan sahip **sunucudur**, çünkü belirteci üreten ve karşılaştıran kod orada. Belirteç değeri sunucunun elinde kalır ve forma yalnız kopyası konur; isteği gönderen taraf kopyayı görmediği sürece karşılığını üretemez.

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

Tarayıcıların çerezi başka bir sitenin isteğiyle göndermesini sınırlayan bir **çerez niteliği (cookie attribute)** vardır: `SameSite`. Sunucu bu niteliği çerez başlığına yazarak ister; uygulamak ise tarayıcıya aittir. Sunucu tarafında ölçülebilen tek şey, başlık dizgesinin içinde nitelik olup olmadığıdır.

```php
<?php
// samesite.php — SameSite sunucuda yalnız bir başlık dizgesidir
declare(strict_types=1);
require 'taraf.php';
require 'istek.php';

function cerezBasligi(string $ad, string $deger, string $samesite): string {
    return 'Set-Cookie: ' . $ad . '=' . $deger . '; SameSite=' . $samesite . '; HttpOnly';
}

echo cerezBasligi('oturum', 'deger', 'Lax'), "\n";

$kendi = ['koken' => 'https://uygulama.test', 'eposta' => 'ayse@ornek.test'];
$baska = ['koken' => 'https://baska.test', 'eposta' => 'baska@ornek.test'];
tarafRaporu('SameSite yalnız', fn(array $s) => guncelle('ayse@ornek.test', $s['üçüncü taraf'] ?? $kendi, null), ['üçüncü taraf' => $baska]);
```

```
Set-Cookie: oturum=deger; SameSite=Lax; HttpOnly
SameSite yalnız → üçüncü taraf
    üçüncü taraf değiştirdi
```

Başlık dizgesinde `SameSite=Lax` var, ama sunucu kodundaki sonuç değişmedi: e-postayı yine üçüncü taraf değiştirdi. Bu beklenen sonuçtur. Niteliğin etkisi sunucuda koşan hiçbir satırda ölçülemez, çünkü çerezi isteğe eklemeyi ya da eklememeyi tarayıcı seçer; sunucu yalnız ister. Dolayısıyla `SameSite` belirtecin yerine konmaz, yanına konur: nitelik tarayıcı tarafında bir katman ekler, belirteç sunucu tarafında sahibi değiştirir. İkincisi olmadan sunucunun sonucu üçüncü tarafın elinde kalır.

#### Özet

- Bu dersin ölçü sınıfı belirteçsiz güncellemede **üçüncü taraf**, belirteçli güncellemede **sunucu**: e-postayı belirteç olmadan başka sitenin isteği değiştirdi; belirteçle değiştiremedi.
- Sahtecilik belirteci `random_bytes()` ve `bin2hex()` ile üretilir (32 karakter), `hash_equals()` ile karşılaştırılır; belirtecin kendisi basılmaz, uzunluğu basılır.
- Savunma sonucu sahibinden alır: yanlış ve eksik belirteç aynı biçimde reddedildi, e-posta mevcut değerinde kaldı.
- `SameSite` çerez niteliği tarayıcının davranışıdır; sunucu yalnız ister ve başlık dizgesinde görünür. Sunucudaki sonucu değiştirmedi, belirtecin yerine değil yanına konur.

#### Sonraki Adım

İstek artık kullanıcının ekranından gelmişse, istekle gelen kimlik bilgisi saklanırken de aynı soru sorulur: parola nasıl saklanıyor ve saklanan karmayı kim değiştirebiliyor? Parola Saklama dersi bunu ölçer.

### Parola Saklama

Source: https://academia.sh/tr/kurslar/php-web/parola-saklama

Parolanın karma olarak saklanmasını, farklı tuzla aynı parolanın farklı karma vermesini, maliyet parametresini ve girişte yeniden karmalamayı ölçmek; varsayılan maliyet ortamın elindedir.

İstek Sahteciliği Koruması dersi isteğin kimden geldiğini sunucunun belirlemesini sağladı. Kimliğin kendisi bir parolaya dayanıyorsa, parolanın nasıl saklandığı sorusu kalır: saklanan değeri ve onu üreten ayarı kim belirliyor? Bu ders parolayı saklamanın yerleşik yolunu ve onun bir ayarının ortamın elinde olup olmadığını ölçer.

**GÜ5.** Bu derste parola ve karma yalnız saklama ve doğrulama için kullanılır. Parola tahmin etmek, karma kırmak ya da saklanmış bir karmayı geri çevirmek modellenmez. Karmanın tamamı hiçbir yerde basılmaz; yalnız önek, uzunluk ve doğrulama sonucu basılır.

#### Karma ve Doğrulama

Bir parola olduğu gibi saklanmaz. **Karma (hash)**, parolanın geri çevrilemeyen bir özetidir: aynı parola aynı özeti verir, ama özetten parola üretilemez. Parola için bu iş `password_hash()` ile yapılır. Fonksiyon her çağrıda rastgele bir **tuz (salt)** üretir ve karmanın içine yazar; böylece aynı parolanın iki karması birbirinden farklıdır. `password_verify()` parolayı ve karmayı alır, karmanın içindeki tuzu ve ayarı okuyarak doğrular. Karmanın başındaki `$2y$12$` öneki algoritmayı (`2y`, bcrypt) ve **maliyet parametresini (cost)** taşır.

Aşağıdaki dosya, derste kullanılan fonksiyonları tanımlar: karma üretme, önek alma ve giriş sırasında doğrulayıp gerekirse yeniden karmalama.

```php
<?php
// parola.php — karmalama, önek ve girişte yeniden karmalama
declare(strict_types=1);

function karmala(string $parola, ?int $maliyet = null): string {
    if ($maliyet === null) {
        return password_hash($parola, PASSWORD_BCRYPT);
    }
    return password_hash($parola, PASSWORD_BCRYPT, ['cost' => $maliyet]);
}

function onek(string $karma): string {
    return substr($karma, 0, 7);
}

function girisYap(string $parola, string $kayitliKarma, int $hedefMaliyet): array {
    if (!password_verify($parola, $kayitliKarma)) {
        return ['basarili' => false, 'karma' => $kayitliKarma];
    }
    if (password_needs_rehash($kayitliKarma, PASSWORD_BCRYPT, ['cost' => $hedefMaliyet])) {
        $kayitliKarma = karmala($parola, $hedefMaliyet);
    }
    return ['basarili' => true, 'karma' => $kayitliKarma];
}
```

İlk ölçüm bir parolayı karmalar; doğru ve yanlış parolayı doğrular; aynı parolayı iki kez karmalayıp karmaların farklı olup olmadığına ve ikisinin de doğrulanıp doğrulanmadığına bakar.

```php
<?php
// karma.php — karma, önek ve doğrulama
declare(strict_types=1);
require 'parola.php';

$karma = karmala('ornek-parola');
$ikinci = karmala('ornek-parola');
echo 'önek ', onek($karma), ' · uzunluk ', strlen($karma), "\n";
echo 'doğru parola ', var_export(password_verify('ornek-parola', $karma), true), "\n";
echo 'yanlış parola ', var_export(password_verify('baska-parola', $karma), true), "\n";
echo 'iki karma ', $karma === $ikinci ? 'aynı' : 'farklı', "\n";
echo 'ikinci karma doğrulanır ', var_export(password_verify('ornek-parola', $ikinci), true), "\n";
```

```
önek $2y$12$ · uzunluk 60
doğru parola true
yanlış parola false
iki karma farklı
ikinci karma doğrulanır true
```

Karma 60 karakterdir ve `$2y$12$` ile başlar: bcrypt, maliyet 12. Doğru parola doğrulandı, yanlış parola doğrulanmadı. Aynı parolanın iki karması farklıdır, çünkü her çağrıda yeni bir tuz yazıldı; yine de ikincisi de aynı parolayla doğrulandı. Doğrulama karmaları karşılaştırmaz, parolayı karmanın içindeki tuzla yeniden karmalayıp sonucu karşılaştırır. Karmanın tamamı basılmadı; öneki ve uzunluğu ölçü için yeterlidir.

#### Taraf: Maliyeti Kim Belirliyor

Maliyet parametresi, bir karmanın üretiminin ne kadar iş gerektirdiğini belirler. Açıkça verilmezse çalışma zamanının varsayılanı kullanılır; bu varsayılan PHP'nin sürümüne bağlıdır (8.4 öncesinde 10, sonrasında 12). Aynı koda başka bir ortamda başka bir önek çıkabilir. Ölçüm için ortamın sapması, varsayılan maliyetin 10 olmasıdır. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Sonuç, üretilen karmanın önekidir. İstemcinin sapması başka bir paroladır.

```php
<?php
// maliyet.php — karma önekinin tarafı
declare(strict_types=1);
require 'taraf.php';
require 'parola.php';

function varsayilanOnek(string $parola, int $maliyet): string {
    return onek(karmala($parola, $maliyet));
}

tarafRaporu(
    'varsayılan maliyet',
    fn(array $s) => varsayilanOnek($s['istemci'] ?? 'ornek-parola', $s['ortam'] ?? PASSWORD_BCRYPT_DEFAULT_COST),
    ['istemci' => 'baska-parola', 'ortam' => 10]
);
```

```
varsayılan maliyet → ortam
    istemci değiştiremedi · ortam değiştirdi
```

Önekin sahibi **ortamdır**. İstemcinin parolası önekin hiçbir karakterini değiştirmedi: parola karmanın içeriğini belirler, ayarını belirlemez. Varsayılan maliyet 12 yerine 10 olunca önek `$2y$12$` yerine `$2y$10$` oldu. Kodda maliyet yazmadığı için ayarı çalışma zamanının sürümü belirliyor; aynı kod başka bir dağıtımda sessizce başka bir maliyetle karma üretir.

#### Girişte Yeniden Karmalama

Maliyet zamanla artırılır, ama saklanan karmalar eski maliyetle kalır. `password_needs_rehash()` bir karmanın hedef ayara uyup uymadığını söyler. Parolanın düz metni yalnızca girişte, kullanıcı onu yazdığında elde olduğu için yeniden karmalama da orada yapılır. Aşağıdaki ölçüm maliyeti 10 olan bir karmayı hedef maliyet 12 ile karşılaştırır, sonra doğru ve yanlış parolayla girişi dener.

```php
<?php
// giris.php — girişte yeniden karmalama
declare(strict_types=1);
require 'parola.php';

$eski = karmala('ornek-parola', 10);
echo 'eski önek ', onek($eski), "\n";
echo 'yeniden karmalama gerekir ', var_export(password_needs_rehash($eski, PASSWORD_BCRYPT, ['cost' => 12]), true), "\n";

$dogru = girisYap('ornek-parola', $eski, 12);
echo 'doğru parola ', var_export($dogru['basarili'], true), ' · önek ', onek($dogru['karma']), "\n";
$yanlis = girisYap('baska-parola', $eski, 12);
echo 'yanlış parola ', var_export($yanlis['basarili'], true), ' · önek ', onek($yanlis['karma']), "\n";
```

```
eski önek $2y$10$
yeniden karmalama gerekir true
doğru parola true · önek $2y$12$
yanlış parola false · önek $2y$10$
```

Maliyeti 10 olan karma, hedef 12 için `password_needs_rehash()` ile `true` verdi. Doğru parolayla girişte karma yeniden üretildi ve önek `$2y$12$` oldu. Yanlış parolayla giriş başarısız oldu ve saklanan karmaya dokunulmadı: önek `$2y$10$` olarak kaldı. Yeniden karmalamanın girişte yapılması bir tercih değil zorunluluktur, çünkü düz metin parola başka hiçbir anda elde değildir.

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

Maliyet açıkça verilirse varsayılan okunmaz. Kodun `karmala($parola, 12)` yazması, ortamın varsayılanını sonuçtan çıkarır. Aynı taraf ölçümünü bu kez maliyeti sabitleyerek tekrarlayalım; ortamın sapması (10) aynıdır.

```php
<?php
// acik.php — maliyet açık verildiğinde taraf
declare(strict_types=1);
require 'taraf.php';
require 'parola.php';

function acikOnek(string $parola): string {
    return onek(karmala($parola, 12));
}

tarafRaporu(
    'açık maliyet',
    fn(array $s) => acikOnek($s['istemci'] ?? 'ornek-parola'),
    ['istemci' => 'baska-parola', 'ortam' => 10]
);
```

```
açık maliyet → sunucu
    istemci değiştiremedi · ortam değiştiremedi
```

Maliyet kodda `12` olarak yazılınca ortamın varsayılanı sonuca girmez: ortamın sapması (10) önekte bir şey değiştirmedi. Sahip **sunucudur**. Bu, sunucunun ayar sayısını artırması değil, ortamın artık bir şey belirleyememesidir; maliyeti artırmak için kod değiştirilir, `password_needs_rehash()` ise eski karmaları girişte yeni ayara taşır.

#### Özet

- Bu dersin ölçü sınıfı varsayılan maliyette **ortam**, açık maliyette **sunucu**: önekin maliyet kısmını ilkinde çalışma zamanının varsayılanı, ikincisinde kodun yazdığı sayı belirledi.
- `password_hash()` her çağrıda yeni bir tuz üretir; aynı parolanın iki karması farklıdır ve ikisi de `password_verify()` ile doğrulanır. Karma 60 karakterdir, öneki algoritmayı ve maliyeti taşır.
- Farklı bir parola önekin sonucunu değiştirmedi: parola karmanın içeriğini belirler, ayarını belirlemez.
- `password_needs_rehash()` eski maliyetli karmayı yakalar; yeniden karmalama düz metin parolanın elde olduğu tek yerde, doğru girişte yapılır. Yanlış parola karmayı değiştirmez.

#### Sonraki Adım

Parolanın saklanması bir kimlik doğrulama akışının bir parçasıdır. Parola, oturum, belirteç ve yetki birlikte çalıştığında, isteğin kimliğini kim belirliyor? Kimlik Doğrulama Akışları dersi bunu bütün güvenlik bölümünün bulgularıyla birlikte ele alır.

### Kimlik Doğrulama Akışları

Source: https://academia.sh/tr/kurslar/php-web/kimlik-dogrulama-akislari

İsteğin kimliğini oturum tabanlı ve taşıyıcı belirteçli akışta ölçmek, imzanın neyi koruyup neyi korumadığını görmek ve kursun on beş dersini tek tabloda toplamak.

Parola Saklama dersi saklanan değeri ve onu üreten ayarı ölçtü. Parola doğrulandıktan sonra da bir soru kalır: sonraki istekler hangi kullanıcı adına işleniyor ve bunu kim belirliyor? **Kimlik doğrulama (authentication)** kullanıcının kim olduğunu saptar; ne yapabileceğini saptayan **yetkilendirme** ayrı bir iştir ve Kimlik Doğrulama ve Yetkilendirme dersinde çerçeve içinde ele alındı. Orada kapının isteği koşturup koşturmadığı ölçüldü. Burada her parça koşar; sorulan, isteğin kimliğini kimin değiştirebildiğidir.

**GÜ6.** Bu derste parola yalnız doğrulanır. Parola tahmin etmek, oturum çalmak, belirteç taklit etmek ve yetki aşmak modellenmez. İstemcinin sapması her zaman nötr bir değerdir: başka bir kullanıcının kendi girişinden aldığı oturum kimliği ya da belirteç.

#### İki Akış

HTTP durumsuzdur; sunucu her isteği ayrı görür. Kimliği isteğe iki yoldan bağlarız. **Oturum tabanlı akışta** giriş sonrası sunucu rastgele bir oturum kimliği üretir, kimlikten kullanıcıya giden kaydı kendi belleğinde tutar ve kimliği istemciye verir; istemci onu her istekte geri gönderir. **Taşıyıcı belirteçli akışta** sunucu kayıt tutmaz: kullanıcı adını `hash_hmac()` ile imzalar ve `kullanici.imza` biçiminde bir **taşıyıcı belirteç (bearer token)** verir. İstemci belirteci her istekte, örneğin `Authorization` başlığında taşır; sunucu imzayı yeniden hesaplayıp `hash_equals()` ile karşılaştırır.

Ölçüm çekirdeği önceki derslerdekiyle aynıdır: `sina()` sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla hesaplar; `taraf()` sırayla istemciye, üçüncü tarafa, ortama bakar ve hiçbiri değiştiremediyse sunucuyu söyler. Dosya her derste olduğu gibi değişmeden verilir.

```php
<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}
```

Aşağıdaki dosya iki akışı kurar. Parolalar yalnız `password_verify()` ile sınanır, oturum kimliği `random_bytes()` ile üretilir ve belirteç imzası `hash_hmac('sha256', ...)` ile atılır. İmza anahtarı burada bir eğitim dizgesidir; gerçek bir uygulamada anahtar kodda durmaz, ortam değişkeninden okunur.

```php
<?php
// kimlik.php — oturum ve belirteç akışı
declare(strict_types=1);

const IMZA_ANAHTARI = 'ders-anahtari';

function karmalar(): array {
    $karmalar = [];
    foreach (['ayse' => 'parola-bir', 'mehmet' => 'parola-iki'] as $kullanici => $parola) {
        $karmalar[$kullanici] = password_hash($parola, PASSWORD_BCRYPT, ['cost' => 10]);
    }
    return $karmalar;
}

function girisDogrula(array $karmalar, string $kullanici, string $parola): bool {
    return isset($karmalar[$kullanici]) && password_verify($parola, $karmalar[$kullanici]);
}

final class OturumDeposu {
    private array $kayitlar = [];

    public function ac(string $kullanici): string {
        $kimlik = bin2hex(random_bytes(16));
        $this->kayitlar[$kimlik] = $kullanici;
        return $kimlik;
    }

    public function kullanici(string $kimlik): string {
        return $this->kayitlar[$kimlik] ?? 'anonim';
    }

    public function kapat(string $kimlik): void {
        unset($this->kayitlar[$kimlik]);
    }
}

function belirtecUret(string $kullanici, string $anahtar = IMZA_ANAHTARI): string {
    return $kullanici . '.' . hash_hmac('sha256', $kullanici, $anahtar);
}

function belirtecKullanici(string $belirtec, string $anahtar = IMZA_ANAHTARI): string {
    $parcalar = explode('.', $belirtec, 2);
    if (count($parcalar) !== 2) return 'anonim';
    $beklenen = hash_hmac('sha256', $parcalar[0], $anahtar);
    return hash_equals($beklenen, $parcalar[1]) ? $parcalar[0] : 'anonim';
}
```

#### İsteğin Kimliği

Sonuç, isteğin işlendiği kullanıcı adıdır. İstemcinin sapması, başka bir kullanıcının kendi girişinden aldığı kimlik bilgisini sunmaktır: oturum akışında `mehmet`'in oturum kimliği, belirteç akışında `mehmet`'in belirteci. Rastgele değerler basılmaz; yalnız uzunlukları ve sonuçları basılır.

```php
<?php
// akis.php — isteğin kimliği, iki akış
declare(strict_types=1);
require 'taraf.php';
require 'kimlik.php';

$karmalar = karmalar();
foreach (['ayse' => 'parola-bir', 'mehmet' => 'parola-iki'] as $kullanici => $parola) {
    echo "giriş $kullanici ", var_export(girisDogrula($karmalar, $kullanici, $parola), true), "\n";
}
echo 'yanlış parola ', var_export(girisDogrula($karmalar, 'ayse', 'parola-iki'), true), "\n";

$depo = new OturumDeposu();
$oturumAyse = $depo->ac('ayse');
$oturumMehmet = $depo->ac('mehmet');
$belirtecAyse = belirtecUret('ayse');
$belirtecMehmet = belirtecUret('mehmet');
echo 'oturum kimliği ', strlen($oturumAyse), ' karakter · imza ', strlen(explode('.', $belirtecAyse)[1]), " karakter\n";

tarafRaporu('kimlik (oturum)', fn(array $s) => $depo->kullanici($s['istemci'] ?? $oturumAyse), ['istemci' => $oturumMehmet]);
tarafRaporu('kimlik (belirteç)', fn(array $s) => belirtecKullanici($s['istemci'] ?? $belirtecAyse), ['istemci' => $belirtecMehmet]);

echo 'uyuşmayan imza ', belirtecKullanici('ayse.' . str_repeat('0', 64)), "\n";
echo 'tanınmayan oturum kimliği ', $depo->kullanici(str_repeat('0', 32)), "\n";
```

```
giriş ayse true
giriş mehmet true
yanlış parola false
oturum kimliği 32 karakter · imza 64 karakter
kimlik (oturum) → istemci
    istemci değiştirdi
kimlik (belirteç) → istemci
    istemci değiştirdi
uyuşmayan imza anonim
tanınmayan oturum kimliği anonim
```

İki akışta da sonuç **istemcidir**. İstemci sunduğu oturum kimliğini ya da belirteci değiştirince isteğin kimliği `ayse`'den `mehmet`'e geçti. Çünkü kimliği isteğe taşıyan, her istekte onu gönderen istemcidir; sunucu yalnız sunulan bilgiyi doğrular. Doğrulama, aynı akışın iki ayrı noktasında farklı çalışır: oturum akışında sunucu kimliği kendi kaydında arar, belirteç akışında imzayı hesaplayıp karşılaştırır.

**GÜ7.** Son iki satır savunmanın sınırını gösterir. İmzası uyuşmayan belirteç bir kullanıcı adı üretmedi, tanınmayan oturum kimliği de üretmedi: ikisi de `anonim` verdi. İstemci bir kullanıcı adı seçemez; elinde sunucunun kendisine verdiği kimlik bilgilerinden birini sunabilir ya da hiçbirini sunamaz. Yani savunma sonucu istemciden almaz, istemcinin seçebileceği kümeyi sunucunun verdiği değerlere daraltır. Bu, kursun başından beri tekrarlanan cümledir: savunma her zaman sahibi değiştirmez, kimi zaman yalnız neyin değiştirilebileceğini daraltır.

Çerezler dersi imzalı çerezde aynı şeyi göstermişti. Oturumlar dersinde sepeti sunucuya taşıyan düzenek burada kimliği taşımaz, çünkü kimliğin kendisi istemcinin elindeki bir değerdir; Oturumlar dersinin sınırı da aynıydı: hangi oturumun açılacağı istemcide kalır.

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

İki akışın sonucu aynı tarafa düştü, ama sunucunun elindeki çare aynı değil. Oturum akışında sunucu kaydı sildiğinde kimlik artık hiçbir kullanıcıya gitmez. Belirteç akışında sunucu kayıt tutmadığı için silecek bir şey yoktur; imza anahtarı değişmedikçe belirteç geçerli kalır. Anahtar ortamdan geliyorsa onu değiştiren dağıtımdır ve değişiklik bütün belirteçleri birden düşürür.

```php
<?php
// cikis.php — çıkış ve anahtar değişimi
declare(strict_types=1);
require 'taraf.php';
require 'kimlik.php';

$depo = new OturumDeposu();
$oturum = $depo->ac('ayse');
$belirtec = belirtecUret('ayse');

echo 'çıkıştan önce oturum ', $depo->kullanici($oturum), ' · belirteç ', belirtecKullanici($belirtec), "\n";
$depo->kapat($oturum);
echo 'çıkıştan sonra oturum ', $depo->kullanici($oturum), ' · belirteç ', belirtecKullanici($belirtec), "\n";

tarafRaporu(
    'kimlik (anahtar ortamdan)',
    fn(array $s) => belirtecKullanici($belirtec, $s['ortam'] ?? IMZA_ANAHTARI),
    ['ortam' => 'yeni-anahtar']
);
```

```
çıkıştan önce oturum ayse · belirteç ayse
çıkıştan sonra oturum anonim · belirteç ayse
kimlik (anahtar ortamdan) → ortam
    ortam değiştirdi
```

Çıkıştan sonra oturum `anonim` verdi, belirteç `ayse` vermeyi sürdürdü. Belirtecin çıkışta ortadan kalkmasını isteyen bir uygulama sunucuya ayrıca bir iptal kaydı eklemek zorundadır; bu da belirtecin kayıtsız olma avantajını geri alır. Anahtar değişince belirteç düştü ve sonucu değiştiren **ortamdır**: anahtarı dağıtım belirler. **GÜ8.** Bu derste süre ölçülmez; bir belirtecin bitiş zamanı taşıması ya da oturumun boşta kalınca kapanması saat gerektirir ve ölçüm kapsamında değildir. Burada yalnız sunucunun hangi çareye sahip olduğuna bakıldı.

#### Özet

- Bu dersin ölçü sınıfı **istemci**: oturum kimliğini ya da belirteci değiştiren istemci isteğin kimliğini değiştirdi, iki akışta da.
- Oturum akışı kimliği sunucu belleğindeki bir kayıttan, belirteç akışı `hash_hmac()` imzasını `hash_equals()` ile doğrulayarak çözer; ikisi de tanınmayan değeri `anonim`'e düşürür.
- İmza istemcinin seçebileceği kümeyi sunucunun verdiği belirteçlerle sınırlar, sahibi değiştirmez.
- Oturum sunucuda iptal edilir; imzalı belirteç anahtar değişmedikçe geçerli kalır ve anahtar ortamdan geliyorsa onu değiştiren ortamdır.

#### Kurs Kapanışı

Bu kursta on beş derste bir sonucu kimin değiştirebildiğini ölçtük: oturum ve durum, veri erişimi, güvenlik. Her ders kendi sonucunu ve bir karşıt ya da sınırlayıcı ölçümünü taşıdı. Aşağıdaki tablo hepsini tek yerde toplar; taraf hücreleri kaynak dersin özetinden alınmıştır.

| Ders | Ölçülen sonuç | Taraf | Sapma ya da karşıt ölçüm |
|---|---|---|---|
| Çerezler | Çerezdeki sepet, imzalı çerez | istemci | İmza değiştirilen değeri fark etti, sahibi değiştirmedi |
| Oturumlar | `$_SESSION`'daki sepet | sunucu | Aynı `sepet=5` çerezde sonucu değiştirdi |
| Yapılandırma ve Ortam Değişkenleri | `UYG_VERITABANI` ile bağlantı adı | ortam | Koda yazılan değer sunucuda kalır, taşınmaz |
| Veritabanı Erişim Katmanı | Dönen değerin tipi | ortam | Açık `int` dönüşümüyle sunucu |
| İşlemler | İki hesabın bakiye toplamı | sunucu | İşlemsiz aktarımda ortam toplamı değiştirdi |
| Göç Yönetimi | Uygulanan göç sayısı | ortam | Şema sürümü her ortamda aynı: sunucu |
| Nesne–Tablo Bağlamasının Sorguları | Gönderilen sorgu sayısı | sunucu | Tembel yüklemede satırla büyüdü: istemci |
| Veri Biçimleri | Ayrıştırılan fiyat | üçüncü taraf | Kendi JSON'umuzdaki sayı gösterimi: sunucu |
| HTTP İstemcisi | Servisin bildirdiği fiyat | üçüncü taraf | Varsayılan değer sonucu sınırlar, sahibi değiştirmez |
| Girdi Doğrulama ve Arındırma | Doğrulanan adet | istemci | Arındırma bozuk girdiyi başka değere çevirir |
| SQL Enjeksiyonu Önleme | `prepare()`'e giden metin | sunucu | Birleştirmede istemci |
| Siteler Arası Betik Çalıştırma Önleme | Basılan HTML | istemci | Çift kaçışlama metni bozar |
| İstek Sahteciliği Koruması | E-posta güncellemesi | sunucu | Belirteçsiz güncellemede üçüncü taraf |
| Parola Saklama | Karma öneki | ortam | Açık maliyette sunucu |
| Kimlik Doğrulama Akışları | İsteğin kimliği | istemci | Anahtar ortamdan gelince ortam |

Tabloda iki ayrı dağılım var ve ikisi ayrı şeyleri sayar. **Derslerin dağılımı** her dersin ölçü sınıfını sayar: 5 sunucu · 4 ortam · 4 istemci · 2 üçüncü taraf. **Çekirdeğin dağılımı** Çerezler dersinde koşturulan on sonucu sayar: 4 sunucu · 2 ortam · 2 istemci · 2 üçüncü taraf. Biri on beş dersin, öteki on ölçümün dağılımıdır; aynı sayılar değildir ve biri ötekini doğrulamaz.

Kursun söylediği tek cümle şudur: bir web uygulamasında sonucun sahibi kodu yazan değildir. İstemci isteği, ortam ayarı, üçüncü taraf veriyi getirir; savunma düzenekleri sahibi kimi zaman sunucuya taşır, kimi zaman yalnız daraltır. Sıradaki kurs bu uygulamayı üretim ortamına taşır: dağıtım, çalıştırma ve başarım konusu.

