İçeriğe geç
academia.sh

Ders 04 / 10

Hata Ayıklayıcı

Hata ayıklarken görülen bilginin, yani istisna izindeki argüman değerinin, uyarı metninin ve çağrı yığıtının hangi profilde göründüğünü ve üretime çıkıp çıkmadığını ölçün.

Yayın Güncelleme

Bu derste

  • Adım adım hata ayıklayıcıyı, kesme noktasını ve adımlamayı kavram olarak tanımlar
  • İstisna izindeki argüman değerinin geliştirme ve üretim profilinde nasıl davrandığını ölçer
  • debug_backtrace() ile istisna izinin aynı ayara bağlı olmadığını gösterir
  • Değerin istisna iletisine yazıldığı durumda korumanın kapsamını ölçer
İçindekiler

Önceki ders, aynı iddianın geliştirmede yakalanıp üretimde sessizce geçebildiğini gösterdi. Bir hata ayıklarken gördüğümüz bilgi de aynı yönde ayrışır mı? Bir hatanın izinde hangi değerin göründüğü, ekrana hangi uyarının basıldığı ve çağrı yığıtının ne kadarının açıldığı, yine ayar dosyasındaki yönergelere bağlıdır. Bu derste hata ayıklamanın gördüğü bilginin üretime çıkıp çıkmadığı ölçülür.

Adım adım hata ayıklayıcı (step debugger), koşan bir programı satır satır durdurup içini gösteren araç sınıfıdır. Programın durmasını istediğimiz satıra bir kesme noktası (breakpoint) koyarız; program oraya gelince durur ve o andaki değişkenler, çağrı yığıtı ve değerler okunabilir. Adımlama (stepping) ise durmuş programı bir satır, bir fonksiyonun içine ya da fonksiyondan dışarı ilerletmektir. Bu araçlar geliştiricinin makinesinde, koşuma dışarıdan bağlanarak çalışır. Bu ders hiçbir eklenti adı anmaz, hiçbir şey kurmaz ve hiçbir bağlantı noktası açmaz; hata ayıklayıcının varlığı yalnız kavram olarak anlatılır, ölçülen şey PHP’nin kendi araçlarının gösterdiğidir.

Dokuzuncu müfredatın beşinci kursunda (M09/K05) bir bilginin hangi araçtan okunduğu soruldu. Burada soru değişir: aracın gösterdiği bilgi, üretimde de görünüyor mu?

Hata Ayıklamanın Gördüğü Üç Bilgi

PHP’nin kendi araçlarından üçü, hata ayıklayıcının gösterdiğine benzer bilgi üretir. İstisnanın izi (getTrace(), getTraceAsString()), istisnanın fırlatıldığı yere kadar çağrılan fonksiyonları ve her çağrıya verilen argümanları listeler. debug_backtrace() aynı yığıtı istenen anda, istisna olmadan döndürür. Uyarı ve bildirimler ise display_errors açıksa yanıta doğrudan basılır.

Ölçüm çekirdeği önceki derslerle aynıdır. yan.php hiç değişmez.

<?php
// yan.php — bu davranış üretime çıkıyor mu?
declare(strict_types=1);

// PHP'nin dağıttığı iki ayar dosyası (php.ini-development, php.ini-production) yedi
// yönergede ayrışır; yedisi buraya birebir alındı. Üretimdeki son iki satır dağıtım
// dosyasında yorum satırıdır: kod önbelleği komut satırında da açık, dosya denetimi kapalı.
const PROFILLER = [
    'geliştirme' => [
        'display_errors' => '1',
        'display_startup_errors' => '1',
        'error_reporting' => '32767',
        'mysqlnd.collect_memory_statistics' => '1',
        'zend.assertions' => '1',
        'zend.exception_ignore_args' => '0',
        'zend.exception_string_param_max_len' => '15',
    ],
    'üretim' => [
        'display_errors' => '0',
        'display_startup_errors' => '0',
        'error_reporting' => '24575',
        'mysqlnd.collect_memory_statistics' => '0',
        'zend.assertions' => '-1',
        'zend.exception_ignore_args' => '1',
        'zend.exception_string_param_max_len' => '0',
        'opcache.enable_cli' => '1',
        'opcache.validate_timestamps' => '0',
    ],
];

/** Betiği ayar dosyası olmadan, yalnız profilin yönergeleriyle ayrı bir süreçte koşturur. */
function calistir(string $betik, array $yonergeler): bool {
    $komut = ['php', '-n'];
    foreach ($yonergeler as $ad => $deger) {
        array_push($komut, '-d', "$ad=$deger");
    }
    $komut[] = $betik;
    $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'w']], $borular);
    $cikti = stream_get_contents($borular[1]);
    fclose($borular[1]);
    proc_close($surec);
    $satirlar = array_values(array_filter(array_map('trim', explode("\n", $cikti))));
    return end($satirlar) === 'evet';
}

/** Davranışın iki koşumda görülüp görülmediğinden yanını çıkarır. */
function yan(bool $gelistirmede, bool $uretimde): string {
    return match (true) {
        $gelistirmede && $uretimde => 'iki yanda da',
        $gelistirmede => 'yalnız geliştirmede',
        $uretimde => 'yalnız üretimde',
        default => 'koşuma girmiyor',
    };
}

function yanRaporu(string $ad, string $betik): string {
    $gelistirmede = calistir($betik, PROFILLER['geliştirme']);
    $uretimde = calistir($betik, PROFILLER['üretim']);
    $sonYan = yan($gelistirmede, $uretimde);
    echo "$ad → $sonYan\n    geliştirme ", $gelistirmede ? 'evet' : 'hayır',
        ' · üretim ', $uretimde ? 'evet' : 'hayır', "\n";
    return $sonYan;
}

Nötr bir örnek kullanılır: ode() fonksiyonu kart-0042 değerini alır ve bir RuntimeException fırlatır. Değer bir kart numarasını andırır, ama gerçek bir numara değildir; amaç, izde görünen bir argümanın başka bir yere sızabilecek bir değer olduğunu göstermektir. Üç yoklama aynı sürücüde koşar.

<?php
// hata.php — hata ayıklamanın gördüğü üç bilgi, iki profil
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'istisna izi argüman değerini taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            throw new RuntimeException('red');
        }
        try {
            ode('kart-0042');
        } catch (RuntimeException $e) {
            echo str_contains($e->getTraceAsString(), 'kart-0042') ? "evet\n" : "hayır\n";
        }
        PHP,
    'debug_backtrace argüman değerini taşır' => <<<'PHP'
        <?php
        function ode(string $kart): array {
            return debug_backtrace();
        }
        $iz = ode('kart-0042');
        echo in_array('kart-0042', $iz[0]['args'] ?? [], true) ? "evet\n" : "hayır\n";
        PHP,
    'uyarı yanıta basılır' => <<<'PHP'
        <?php
        ob_start();
        echo $tanimsiz;
        $yanit = ob_get_clean();
        echo str_contains($yanit, 'Warning') ? "evet\n" : "hayır\n";
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
istisna izi argüman değerini taşır → yalnız geliştirmede
    geliştirme evet · üretim hayır
debug_backtrace argüman değerini taşır → iki yanda da
    geliştirme evet · üretim evet
uyarı yanıta basılır → yalnız geliştirmede
    geliştirme evet · üretim hayır

Birinci yoklamada ayar farkı doğrudan görülür. Geliştirme profilinde zend.exception_ignore_args=0 olduğu için istisna izi her çağrının argümanlarını taşır ve kart-0042 izde okunur. Üretim profilinde zend.exception_ignore_args=1 olduğu için iz yalnız fonksiyon adlarını ve satırları taşır; argüman listesi boştur. Bu bir korumadır: hata günlüğüne ya da bir hata sayfasına düşen iz, kullanıcının verdiği değeri taşımaz. Üretimde sıkılaştırılan bir yönerge olduğu için bu yanın yalnız geliştirmede olması bir kusur değil, istenen sonuçtur.

Üçüncü yoklama da aynı yandadır. Geliştirme profilinde display_errors=1 ve error_reporting=32767 olduğu için tanımsız değişken uyarısı çıktıya basılır. Üretim profilinde display_errors=0 olduğu için uyarı yanıta girmez. Hata iletisinin yanıta basılması, dosya yolunu, satır numarasını ve çoğu zaman bir değeri ziyaretçiye gösterir. Buna bilgi sızıntısı (information disclosure) denir ve üretimde kapalı tutulmasının sebebi budur.

İkinci yoklama bu tablonun dışında kalır. debug_backtrace() çağrısı iki profilde de argümanı döndürür. Sebep, zend.exception_ignore_args yönergesinin yalnız istisna izini ilgilendirmesidir; kodun kendisinin istediği yığıt dökümünü etkilemez. Koruma bir yönergedir, bir engel değildir: argümanı bir istisna izinden saklar ama koda yazılmış bir debug_backtrace() çağrısından saklamaz. Bu çağrı üretime çıkan bir dosyada kalmışsa, değer üretimde de okunur.

İletiye Yazılan Değer

Koruma argüman listesini kapsıyorsa, değer başka bir yere yazıldığında ne olur? Bir geliştirici hatayı anlaşılır kılmak için değeri istisnanın iletisine yazabilir: new RuntimeException("red: $kart"). Bu, hata ayıklarken sık yapılan bir kolaylıktır, çünkü ileti hem günlükte hem de ekranda görünür. Soru şudur: üretim profili bu değeri de gizler mi?

Üretimde Neden Durdurulmaz

Hata ayıklayıcının kesme noktası, programı durdurur ve içini okunur kılar. Bunun iki anlamı vardır. Durmuş bir program istek yanıtlamaz: üretimde bir kesme noktası, o isteği bekleten bir kilit olur. Ayrıca durmuş programın gösterdiği her değişken, hata ayıklayıcıya bağlanan kişiye açıktır; bu kişinin yetkisi, istek sahibinin yetkisinden geniştir. İkisi de hata ayıklayıcıyı geliştirme profiline ait bir araç yapar.

Üretimde bunun yerini daha dar araçlar alır: günlük, istisna izi ve metrik. Bunlar programı durdurmaz, ama gösterdikleri bilgi ayara bağlıdır. Bu derste ölçülen şey tam olarak budur. Geliştirmede ayrıntılı bilgi istenir, çünkü okuyan kişi hatayı çözecek olandır. Üretimde aynı bilgi, günlüğe, hata sayfasına ya da bir izleme servisine gidebilir ve orada kimin okuyacağı bilinmez. zend.exception_ignore_args bu yüzden üretim profilinde 1 olur, display_errors bu yüzden 0 olur.

Burada bir ayrım yapmak gerekir. Bu yönergeler hatayı gizlemez; hatanın nerede gösterileceğini belirler. Üretimde hata yine oluşur, yine günlüğe yazılır, yine istisna olarak fırlatılır. Değişen, yanıta ve izde hangi alanın bulunduğudur. Bu yüzden “üretimde hata yok” diye bir sonuç çıkmaz; yalnız “üretimde hatanın ayrıntısı görünmez” sonucu çıkar.

İz Biçimi ve Bildirim Düzeyi

İzin kendisini de ölçelim. getTraceAsString(), her çağrıyı bir satırda yazar ve satırın başında #0, #1 gibi bir sıra numarası bulunur. debug_print_backtrace() aynı bilgiyi doğrudan çıktıya basar. İkisi de aynı korumaya uyar mı, ölçelim. İlk yoklama iz satırının çağrılan fonksiyonun adını taşıdığını, ikincisi debug_print_backtrace() çıktısının argüman değerini taşıyıp taşımadığını sorar. Üçüncüsü başka bir bilgiye bakar: kullanımdan kaldırma bildirimi. ini_set('display_errors', '1') ile ekrana basma açılsa bile, error_reporting değeri o bildirimi dışarıda bırakıyorsa bildirim hiç üretilmez. Üretim profilinde error_reporting=24575 olduğu için E_DEPRECATED bayrağı kapalıdır.

<?php
// iz.php — iz biçimi ve bildirim düzeyi
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'iz satırı fonksiyon adını taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            throw new RuntimeException('red');
        }
        try {
            ode('kart-0042');
        } catch (RuntimeException $e) {
            echo str_contains($e->getTraceAsString(), '#0') && str_contains($e->getTraceAsString(), 'ode(') ? "evet\n" : "hayır\n";
        }
        PHP,
    'debug_print_backtrace argüman değerini taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            debug_print_backtrace();
        }
        ob_start();
        ode('kart-0042');
        echo str_contains(ob_get_clean(), 'kart-0042') ? "evet\n" : "hayır\n";
        PHP,
    'ekrana basma açılsa da kullanımdan kaldırma bildirimi gelir' => <<<'PHP'
        <?php
        ini_set('display_errors', '1');
        ob_start();
        strlen(null);
        echo str_contains(ob_get_clean(), 'Deprecated') ? "evet\n" : "hayır\n";
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
iz satırı fonksiyon adını taşır → iki yanda da
    geliştirme evet · üretim evet
debug_print_backtrace argüman değerini taşır → yalnız geliştirmede
    geliştirme evet · üretim hayır
ekrana basma açılsa da kullanımdan kaldırma bildirimi gelir → yalnız geliştirmede
    geliştirme evet · üretim hayır

Okunuş şöyledir. İz biçimi ayardan bağımsızdır: iz satırı iki profilde de sıra numarasını ve fonksiyon adını taşır. İkinci satır beklenmedik bir ayrım gösterir: debug_print_backtrace() argüman değerini yalnız geliştirmede basar, üretimde basmaz. Bu farkı zend.exception_ignore_args yaratmaz; o yönerge yalnız istisna izini ilgilendirir. Fark zend.exception_string_param_max_len yönergesinden gelir: debug_print_backtrace() yığıtı metne çevirirken dize argümanları bu uzunlukta keser. Değer geliştirmede 15, üretimde 0 olduğu için kart-0042 geliştirmede tam basılır, üretimde basılmaz. Bu derste daha önce ölçülen debug_backtrace() ise yığıtı metne çevirmeden dizi olarak döndürür ve argümanı iki yanda da verir. Aynı yığıtı okuyan iki fonksiyon, koruma açısından farklı davranır. Hangisinin çağrıldığı bu yüzden önemlidir.

Üçüncü yoklama da yalnız geliştirmede gelir: kullanımdan kaldırma bildirimi, üretim profilinde display_errors açılsa bile üretilmez, çünkü error_reporting o düzeyi kapatır. Bu, bir uyarının görünmemesinin iki ayrı sebebi olabileceğini gösterir: bildirim üretilmiş ama ekrana basılmıyor olabilir ya da hiç üretilmiyor olabilir. Üretimde ikinci durum, bir kullanımdan kaldırmanın yanıtta da, bu ölçümde de iz bırakmaması demektir. Kullanımdan kaldırılan bir çağrı geliştirmede fark edilmezse, üretimde de fark edilmez.

Kesme Noktasının Taklidi

Kesme noktası bir satırda durup çağıranı, argümanları ve yerel değişkenleri gösterir. debug_backtrace() bunun ne kadarını taklit edebilir? Üç yoklama, durdurulan noktadan ne okunabildiğini sorar: çağıran fonksiyonun adı, çağrının argümanı ve çağıran fonksiyonun yerel değişkeni.

<?php
// taklit.php — debug_backtrace kesme noktasının neyini taklit eder
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'çağıranın adı okunur' => <<<'PHP'
        <?php
        function dur(): array {
            return debug_backtrace();
        }
        function ode(string $kart): array {
            $kalan = 'bakiye-77';
            return dur();
        }
        $iz = ode('kart-0042');
        echo ($iz[1]['function'] ?? '') === 'ode' ? "evet\n" : "hayır\n";
        PHP,
    'çağıranın argümanı okunur' => <<<'PHP'
        <?php
        function dur(): array {
            return debug_backtrace();
        }
        function ode(string $kart): array {
            $kalan = 'bakiye-77';
            return dur();
        }
        $iz = ode('kart-0042');
        echo in_array('kart-0042', $iz[1]['args'] ?? [], true) ? "evet\n" : "hayır\n";
        PHP,
    'çağıranın yerel değişkeni okunur' => <<<'PHP'
        <?php
        function dur(): array {
            return debug_backtrace();
        }
        function ode(string $kart): array {
            $kalan = 'bakiye-77';
            return dur();
        }
        $iz = ode('kart-0042');
        echo str_contains(json_encode($iz), 'bakiye-77') ? "evet\n" : "hayır\n";
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
çağıranın adı okunur → iki yanda da
    geliştirme evet · üretim evet
çağıranın argümanı okunur → iki yanda da
    geliştirme evet · üretim evet
çağıranın yerel değişkeni okunur → koşuma girmiyor
    geliştirme hayır · üretim hayır

Sonuç taklidin sınırını çizer. Çağıranın adı ve argümanı okunur; yerel değişken okunmaz. Gerçek bir kesme noktası durdurduğu satırda bütün yerel değişkenleri gösterir, debug_backtrace() ise yalnız çağrı zincirini ve argümanları verir. İki araç aynı ailede değildir: biri programı durdurup içine bakar, öbürü programın kendi kendine bıraktığı bir kaydı okur. Bu fark, hata ayıklayıcıyı üretimde neden kullanmadığımızın, ama debug_backtrace() çağrısının neden unutulduğunda tehlikeli olduğunun gerekçesidir: ikincisi üretime çıkar ve argümanları iki profilde de gösterir.

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

Sürücü iki istisnayı yan yana koşturur: biri değeri argüman olarak alır, öbürü değeri iletiye yazar. Her ikisinde de kart-0042 değerinin istisna içinde bir yerde bulunup bulunmadığı, ayrı ayrı sorulur. İz için getTraceAsString(), ileti için getMessage() okunur.

<?php
// sinir.php — değer argümanda mı, iletide mi
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'değer argümandaysa iz taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            throw new RuntimeException('red');
        }
        try {
            ode('kart-0042');
        } catch (RuntimeException $e) {
            echo str_contains($e->getTraceAsString(), 'kart-0042') ? "evet\n" : "hayır\n";
        }
        PHP,
    'değer iletideyse ileti taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            throw new RuntimeException("red: $kart");
        }
        try {
            ode('kart-0042');
        } catch (RuntimeException $e) {
            echo str_contains($e->getMessage(), 'kart-0042') ? "evet\n" : "hayır\n";
        }
        PHP,
    'değer iletideyse iz de taşır' => <<<'PHP'
        <?php
        function ode(string $kart): void {
            throw new RuntimeException("red: $kart");
        }
        try {
            ode('kart-0042');
        } catch (RuntimeException $e) {
            echo str_contains($e->getTraceAsString(), 'kart-0042') ? "evet\n" : "hayır\n";
        }
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
değer argümandaysa iz taşır → yalnız geliştirmede
    geliştirme evet · üretim hayır
değer iletideyse ileti taşır → iki yanda da
    geliştirme evet · üretim evet
değer iletideyse iz de taşır → yalnız geliştirmede
    geliştirme evet · üretim hayır

Sonuç korumanın sınırını gösterir. Değer argümandaysa üretim profili onu izden siler. Değer iletiye yazılmışsa zend.exception_ignore_args ona dokunmaz: getMessage() iki profilde de kart-0042 değerini taşır ve ileti hata günlüğüne ya da hata sayfasına olduğu gibi gider. Koruma yalnız argüman listesini kapsar; ileti bir dizgedir ve PHP onun içine karışmaz. Üçüncü satır ayrımı tamamlar: değer iletideyse bile iz, argüman listesi boş olduğu için üretimde değeri taşımaz. Değer, izden değil iletiden sızar. Hangi alanın okunduğu ölçüyü belirler.

Bunun iki sonucu vardır. Birincisi, üretimde izin değer taşımaması tek başına bir güvence değildir; değerin iletiye yazılmadığı ayrıca doğrulanmalıdır. İkincisi, hata ayıklarken işe yarayan iletiyi, yani değeri taşıyan iletiyi, üretime çıkmadan önce gözden geçirmek gerekir.

Özet

  • Adım adım hata ayıklayıcı, kesme noktasında durdurup değişkenleri ve çağrı yığıtını gösteren araç sınıfıdır; PHP’nin kendi karşılığı istisna izi, debug_backtrace() ve display_errors’tur.
  • İstisna izindeki argüman değeri yalnız geliştirmede görünür; üretimde zend.exception_ignore_args=1 onu gizler ve bu bir korumadır.
  • Uyarının yanıta basılması da yalnız geliştirmede görülür; üretimde display_errors=0 olduğu için bilgi sızıntısı kapanır.
  • debug_backtrace() iki profilde de argümanı döndürür: koruma istisna izine özgüdür, koda yazılmış çağrıyı kapsamaz.
  • Değer istisna iletisine yazılırsa üretim profili onu gizlemez; koruma yalnız argüman listesini kapsar.

Sonraki Adım

Bu dersle birlikte kalite araçlarının hepsi, bir davranışın iki profilde ayrışıp ayrışmadığı sorusuyla tarandı: statik çözümleme ve stil denetimi koşumun dışında kaldı, iddia ve ayrıntılı hata bilgisi yalnız geliştirmede göründü. Sıradaki derslerde soru dosyanın kendisine döner: bir dosya değiştiğinde PHP hangi sürümü koşturur, diskteki mi, önbellekteki mi? Yanıt yine ayar dosyasında yazılıdır ve üretim profilinde başka bir değer alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat