PHP Hata ve İstisna Yönetimi: try, catch, throw ve Özel İstisnalar

PHP Hata ve İstisna Yönetimi: try, catch, throw ve Özel İstisnalar

6 dakika okuma

Hata yönetimi çoğu kod tabanında iki uçtan birine kayar: hiç yakalanmayan istisnalar ya da her satırı saran try blokları. İkisi de aynı sonucu doğurur; sorunun kaynağı kaybolur. Doğru yaklaşım, istisnanın nerede anlamlı biçimde ele alınabileceğini belirleyip yalnızca orada yakalamaktır.

Bu bölümde PHP'nin hata hiyerarşisi, try ve catch kullanımı, kendi istisna sınıflarının yazılması ve üretim ortamında hataların nasıl görüntüleneceği ele alınıyor.

Error ve Exception ayrımı

PHP 7 ile birlikte fırlatılabilir her şey Throwable arayüzünü uygular. Bu arayüzün altında iki ana dal bulunur. Exception uygulamanın öngördüğü, ele alınabilir durumları anlatır: dosya bulunamadı, doğrulama başarısız oldu. Error ise dilin kendi düzeyindeki sorunları temsil eder: tip uyuşmazlığı, tanımsız metot çağrısı, bellek sınırı.

PHP10 satır
try {
    $sonuc = riskliIslem();
} catch (JsonException $e) {
    // beklenen ve ele alınabilir durum
    $sonuc = null;
} catch (TypeError $e) {
    // kod hatası: yakalanıp gizlenmemeli, kayda alınıp yeniden fırlatılmalı
    $this->gunluk->critical($e->getMessage(), ['iz' => $e->getTraceAsString()]);
    throw $e;
}

Genel kural, Error türevlerinin yakalanıp yutulmamasıdır. Bunlar kodun düzeltilmesi gereken kusurlarıdır; sessizce geçilirse yalnızca belirtiler gizlenir. Throwable yakalamak yalnızca uygulamanın en dış katmanında, kayıt tutmak ve düzgün bir yanıt üretmek için anlamlıdır.

try, catch, finally

finally bloğu, istisna fırlatılsın ya da fırlatılmasın çalışır. Açılan kaynakların kapatılması için uygundur. Birden fazla istisna tipi tek blokta yakalanabilir; yakalanan nesneye ihtiyaç yoksa değişken adı yazılmayabilir.

PHP12 satır
$dosya = fopen($yol, 'r');

try {
    return islemeAl($dosya);
} catch (RuntimeException|LogicException $e) {
    $this->gunluk->error('İşleme başarısız', ['hata' => $e->getMessage()]);
    return null;
} catch (JsonException) {          // nesneye ihtiyaç yoksa değişken yazılmaz
    return null;
} finally {
    fclose($dosya);                // her durumda çalışır
}

catch blokları yukarıdan aşağıya değerlendirilir, bu yüzden en özel tip en üstte yazılır. Üst tip önce yazılırsa alttaki bloklara hiç girilmez.

Kendi istisna sınıflarını yazmak

Yerleşik istisnalar genel amaçlıdır. Uygulamaya özgü durumlar için ayrı sınıf tanımlamak, çağıran tarafın hangi durumu ele aldığını mesaj metnine bakmadan seçebilmesini sağlar. Alan adına göre bir üst sınıf ve altında somut durumlar yeterlidir.

PHP12 satır
abstract class OdemeHatasi extends RuntimeException {}

final class YetersizBakiye extends OdemeHatasi
{
    public function __construct(
        public readonly int $eksikKurus,
    ) {
        parent::__construct(sprintf('Bakiye %d kuruş yetersiz', $eksikKurus));
    }
}

final class SaglayiciUlasilamadi extends OdemeHatasi {}

İstisna nesnesine bağlam taşımak, mesaj metnini ayrıştırmaktan çok daha sağlıklıdır. Yukarıdaki örnekte eksik tutar bir özellik olarak taşındığı için çağıran taraf kullanıcıya doğru miktarı gösterebilir.

PHP7 satır
try {
    $this->odeme->tahsilEt($siparis);
} catch (YetersizBakiye $e) {
    return $this->uyari("Bakiyeniz {$e->eksikKurus} kuruş yetersiz.");
} catch (SaglayiciUlasilamadi $e) {
    return $this->kuyrugaAl($siparis);   // sonra tekrar denenecek
}

İstisna zincirlemek

Alt katmandan gelen bir istisna, üst katmanın diliyle yeniden fırlatılırken aslı kaybedilmemelidir. Üçüncü parametre olarak verilen önceki istisna, izin tamamını korur.

PHP8 satır
try {
    $satir = $this->pdo->query($sql)->fetch();
} catch (PDOException $e) {
    throw new KullaniciDeposuHatasi(
        'Kullanıcı okunamadı',
        previous: $e,          // asıl neden korunur
    );
}

Kayıt tutarken yalnızca en üstteki mesaj değil, zincirin tamamı yazılmalıdır. getPrevious ile ilerleyen kısa bir döngü, gerçek nedeni günlüğe taşır.

PHP13 satır
function zincir(Throwable $e): string
{
    $satirlar = [];

    do {
        $satirlar[] = sprintf(
            '%s: %s (%s:%d)',
            $e::class, $e->getMessage(), $e->getFile(), $e->getLine()
        );
    } while ($e = $e->getPrevious());

    return implode(PHP_EOL . 'neden -> ', $satirlar);
}

Hangi katmanda yakalanmalı

Bir istisnanın yakalanacağı yer, o istisna hakkında karar verebilecek bilgiye sahip olan katmandır. Veritabanı sınıfı bağlantı hatasını yakalayıp boş dizi döndürürse, üst katman verinin gerçekten bulunmadığını mı yoksa erişilemediğini mi anlamaz. Bu iki durum farklı yanıtlar gerektirir: biri boş liste, diğeri hata sayfası.

Pratik bir ayrım şöyle kurulabilir. Altyapı katmanı istisnayı zenginleştirip yukarı taşır. Uygulama katmanı, alternatif bir yol varsa yakalar; örneğin önbellek okunamadığında kaynağa gider. Sunum katmanı ise kullanıcıya ne gösterileceğine karar verir ve son yakalayıcıyı burada bulundurur.

Boş catch bloğu hemen her durumda bir kusurdur. İstisnanın bilinçli olarak yok sayıldığı ender durumlarda, gerekçenin bir yorum satırıyla yazılması gerekir; aksi halde blok bir süre sonra gizlenmiş bir hataya dönüşür.

İstisnanın uygun olmadığı yerler

İstisnalar akış kontrolü aracı değildir. Beklenen ve sık yaşanan durumlar için kullanıldıklarında hem okunabilirlik düşer hem de gereksiz maliyet doğar. Kullanıcının formu eksik doldurması beklenen bir durumdur ve doğrulama sonucuyla ifade edilir; veritabanı sunucusunun yanıt vermemesi ise beklenmedik bir durumdur ve istisnayla ifade edilir.

PHP14 satır
// beklenen durum: sonuç nesnesi döndürülür
final class DogrulamaSonucu
{
    public function __construct(
        public readonly bool $gecerli,
        /** @var list<string> */
        public readonly array $hatalar = [],
    ) {}
}

// beklenmedik durum: istisna fırlatılır
if (!$this->pdo->beginTransaction()) {
    throw new RuntimeException('İşlem başlatılamadı');
}

Yerleşik fonksiyonların bir bölümü istisna fırlatmaz, false döndürür ya da uyarı üretir. Bu davranış açıkça değiştirilebildiği yerlerde değiştirilmelidir; böylece hata yönetimi tek bir yoldan yürür.

PHP7 satır
// PDO hataları varsayılan olarak sessizdir, istisnaya çevrilir
$pdo = new PDO($dsn, $kullanici, $parola, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// JSON hataları da istisnaya çevrilebilir
$veri = json_decode($ham, true, flags: JSON_THROW_ON_ERROR);

Yakalanmayan istisnalar ve genel işleyiciler

Uygulamanın en dış katmanında bir yakalayıcı bulunmalıdır. Amaç hatayı gizlemek değil, kullanıcıya anlaşılır bir yanıt verirken ayrıntıyı günlüğe yazmaktır.

PHP18 satır
set_exception_handler(function (Throwable $e): void {
    error_log((string) $e);
    http_response_code(500);
    echo 'Beklenmeyen bir hata oluştu.';
});

// uyarı ve bildirimleri de istisnaya çevirerek tek bir yoldan yönetmek
set_error_handler(function (int $no, string $mesaj, string $dosya, int $satir): bool {
    throw new ErrorException($mesaj, 0, $no, $dosya, $satir);
});

// ölümcül hatalar yukarıdaki iki işleyiciye düşmez
register_shutdown_function(function (): void {
    $son = error_get_last();
    if ($son !== null && $son['type'] === E_ERROR) {
        error_log('Ölümcül hata: ' . $son['message']);
    }
});

Üretim ortamında hata görüntüleme

Hata metinlerinin ziyaretçiye gösterilmesi bilgi sızdırır. Dosya yolları, sorgu metinleri ve sınıf adları saldırı yüzeyini genişletir. Üretimde görüntüleme kapatılır, kayıt açık tutulur.

INI6 satır
; php.ini - üretim
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/hata.log
error_reporting = E_ALL

Geliştirme ortamında ise tersi geçerlidir: tüm hatalar görünür olmalıdır. Uyarıların bastırıldığı bir ortamda çalışan kod, üretimde ilk kez hata verdiğinde teşhis çok daha zordur. @ operatörü ile hata bastırma hemen her durumda yanlış çözümdür.

Kullanıcıya gösterilen mesaj ile günlüğe yazılan kayıt aynı olmak zorunda değildir. Ekranda kısa bir açıklama ve bir izleme numarası göstermek, destek kaydı geldiğinde ilgili günlük satırını saniyeler içinde bulmayı sağlar.

PHP6 satır
$izlemeNo = bin2hex(random_bytes(6));

error_log(sprintf('[%s] %s', $izlemeNo, (string) $e));

http_response_code(500);
echo "Beklenmeyen bir hata oluştu. Destek kodu: {$izlemeNo}";

Kontrol listesi

  • İstisnalar yalnızca anlamlı biçimde ele alınabilecekleri katmanda yakalanıyor.
  • Error türevleri yutulmuyor; kayda alınıp yeniden fırlatılıyor.
  • Uygulamaya özgü durumlar için ayrı istisna sınıfları tanımlı.
  • Yeniden fırlatmada previous parametresi doldurularak zincir korunuyor.
  • En dış katmanda genel bir işleyici ve kapanış kaydı bulunuyor.
  • Üretimde display_errors kapalı, log_errors açık.
  • Kaynak kapatma işlemleri finally bloğunda yapılıyor.

İlgili yazılar

PHP eğitim serisi

Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.

Temeller

  1. Kurulum ve temel sözdizimi
  2. Döngüler ve akış kontrolü
  3. Diziler
  4. Fonksiyonlar
  5. String fonksiyonları
  6. Süper globaller
  7. Form işleme
  8. Dosya işlemleri

Dilin ayrıntıları

  1. Tip sistemi ve strict_types
  2. Hata ve istisna yönetimi (bu yazı)
  3. Düzenli ifadeler
  4. Tarih ve saat işlemleri

Nesne yönelimli PHP

  1. Sınıf ve nesne
  2. Kalıtım, arayüz ve trait
  3. Statik üyeler, sabitler ve enum
  4. Namespace, Composer ve autoload

Veri ve güvenlik

  1. PDO ile veritabanı işlemleri
  2. Oturum yönetimi ve kimlik doğrulama
  3. Güvenli çıktı ve XSS
  4. JSON ile veri değişimi

Dış dünya ve üretim

  1. cURL ile dış API çağırma
  2. Kendi REST API'ni yazmak
  3. PHPUnit ile birim test
  4. Hata ayıklama ve performans ölçümü
  5. Üretime alma ve güvenlik

Yorumlar

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar