PHP'de veritabanına bağlanmanın standart yolu PDO (PHP Data Objects) sınıfıdır. Bağlantıyı new PDO($dsn, $kullanici, $sifre, $secenekler) ile açar, kullanıcıdan gelen her değeri SQL metnine eklemek yerine prepare() ile hazırladığınız sorguya yer tutucu (? ya da :ad) olarak verir, execute() ile çalıştırır ve sonuçları fetch() veya fetchAll() ile alırsınız. Birden çok değişikliğin birlikte başarılı ya da birlikte başarısız olması gerekiyorsa beginTransaction(), commit() ve rollBack() kullanılır.

PDO, MySQL, PostgreSQL, SQLite ve diğer sürücüler için aynı arayüzü sunar; yalnızca bağlantı metni (DSN) ve bazı sürücüye özel ayrıntılar değişir. Bu yazıdaki tüm örnekler kurulum gerektirmeyen SQLite bellek içi veritabanı (sqlite::memory:) ile PHP 8.5.12 üzerinde çalıştırıldı. SQLite hakkında genel bilgi için SQLite nedir, MySQL için MySQL nedir yazılarına, serinin tamamı için PHP öğrenme rehberi sayfasına bakabilirsiniz.

Bağlantı: DSN, kullanıcı, şifre ve seçenekler

PDO::__construct() dört parametre alır: DSN, kullanıcı adı, şifre ve seçenek dizisi. DSN genel olarak sürücü adı, iki nokta ve sürücüye özel bağlantı bilgisinden oluşur. SQLite için sqlite: önekinden sonra diskteki veritabanının mutlak yolu yazılır; sqlite::memory: bellekte bir veritabanı açar; yalnızca sqlite: yazılırsa bağlantı kapanınca silinen geçici bir veritabanı kullanılır. MySQL için DSN mysql:host=localhost;dbname=testdb biçimindedir ve port, charset gibi ek alanlar alabilir.

php
<?php
$pdo = new PDO('sqlite::memory:', options: [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
echo $pdo->getAttribute(PDO::ATTR_DRIVER_NAME), "\n"; // çıktı: sqlite
echo get_class($pdo), "\n";                           // çıktı: PDO

$pdo->exec('CREATE TABLE urunler (
    id INTEGER PRIMARY KEY,
    ad TEXT NOT NULL,
    fiyat REAL NOT NULL,
    stok INTEGER NOT NULL
)');
echo "tablo hazır\n"; // çıktı: tablo hazır

Kılavuza göre bağlantı kurulamazsa yapıcı, hata modu ne olursa olsun her zaman PDOException fırlatır. Şifre parametresi #[\SensitiveParameter] özniteliğiyle işaretlidir; bu öznitelik, yığın izinde (stack trace) parametrenin değerinin gizlenmesini sağlar. Yine de bağlantı hatasını yakalayıp kullanıcıya genel bir mesaj göstermek, ayrıntıyı ise yalnızca kendi günlüğünüze yazmak iyi bir alışkanlıktır. Öznitelikler hakkında ayrıntı PHP attributes yazısında bulunuyor.

PHP 8.4: PDO::connect ve sürücüye özel alt sınıflar

PHP 8.4 ile PDO'ya sürücüye özel alt sınıflar eklendi: Pdo\Sqlite, Pdo\Mysql, Pdo\Pgsql, Pdo\Odbc, Pdo\Firebird ve Pdo\DbLib. Yeni statik PDO::connect() metodu, bağlanılan veritabanına ait bir alt sınıf varsa onun örneğini, yoksa genel bir PDO nesnesi döndürür. Önceki örnekte görüldüğü gibi new PDO(...) ise düz PDO sınıfı verir. Aşağıdaki satır PHP 8.4 veya sonrasını gerektirir:

php
<?php
$db = PDO::connect('sqlite::memory:');
echo get_class($db), "\n"; // çıktı: Pdo\Sqlite

PHP 8.5'te ise PDO sınıfındaki sürücüye özel sabitler (örneğin PDO::MYSQL_ATTR_INIT_COMMAND) kullanımdan kaldırıldı; yerlerine alt sınıf sabitleri (Pdo\Mysql::ATTR_INIT_COMMAND) önerilir. DSN'in bir URI'dan okunmasını sağlayan uri: şeması da uzak kaynaklı DSN'lerin güvenlik riski nedeniyle 8.5'te kullanımdan kaldırıldı. Sürüm değişikliklerinin tamamı PHP 8.4 yenilikleri ve PHP 8.5 yenilikleri yazılarında.

Hata modu: neden ERRMODE_EXCEPTION?

PDO üç hata stratejisi sunar. PDO::ERRMODE_SILENT yalnızca hata kodunu saklar, siz her çağrıdan sonra errorCode() veya errorInfo() ile kontrol edersiniz; PHP 8.0 öncesinde varsayılan buydu. PDO::ERRMODE_WARNING buna ek olarak bir E_WARNING üretir. PDO::ERRMODE_EXCEPTION ise PDOException fırlatır ve PHP 8.0'dan beri varsayılan moddur. Kılavuz istisna modunun hata yönetimini daha az iç içe kodla ve daha açık biçimde kurmanızı sağladığını belirtir.

php
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->query('SELECT * FROM olmayan_tablo');
/*
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such table: olmayan_tablo in /yol/ornek.php:3
Stack trace:
#0 /yol/ornek.php(3): PDO->query('SELECT * FROM o...')
#1 {main}
  thrown in /yol/ornek.php on line 3
*/

Mesajdaki SQLSTATE[HY000] kısmı, PDO'nun sürücülerin kendi hata kodlarını eşlediği standart SQL-92 SQLSTATE kodudur. Varsayılan zaten istisna modu olsa da seçeneği bağlantıda açıkça yazmak, kodun eski bir PHP sürümünde veya farklı ayarla çalışırken sessizce davranış değiştirmesini önler. PDOException yakalama, yeniden fırlatma ve finally kullanımı try/catch ve exception yazısında, yakalanmamış istisnaların okunması Fatal error: Uncaught çözümü yazısında anlatılıyor.

Hazırlanmış sorgular: prepare ve execute

Hazırlanmış sorgular: prepare ve execute: Yer tutucunun sınırları, bindValue ve bindParam

Hazırlanmış sorgu (prepared statement), değerlerin yerine yer tutucu bulunan bir SQL şablonudur. Kılavuz iki temel yararını şöyle özetler: Sorgu bir kez ayrıştırılır ve farklı değerlerle defalarca çalıştırılabilir; parametrelerin tırnaklanmasını ise sürücü üstlenir. Uygulama değerleri yalnızca hazırlanmış sorgularla gönderiyorsa SQL enjeksiyonu oluşmaz; ancak kılavuz, sorgunun başka bölümleri kaçırılmamış girdiyle kuruluyorsa riskin sürdüğünü de vurgular. Konunun genel çerçevesi SQL injection önleme yazısında.

php
<?php
$ekle = $pdo->prepare('INSERT INTO urunler (ad, fiyat, stok) VALUES (:ad, :fiyat, :stok)');
$ekle->execute(['ad' => 'Çay', 'fiyat' => 45.5, 'stok' => 120]);
$ekle->execute(['ad' => 'Kahve', 'fiyat' => 210.0, 'stok' => 0]);
$ekle->execute(['ad' => 'Şeker', 'fiyat' => 38.75, 'stok' => 60]);
echo $pdo->lastInsertId(), "\n"; // çıktı: 3
var_dump($ekle->rowCount());      // çıktı: int(1)

$sorgu = $pdo->prepare('SELECT id, ad, fiyat, stok FROM urunler WHERE stok > ? ORDER BY fiyat');
$sorgu->execute([10]);
$satir = $sorgu->fetch();
var_dump($satir);
/*
array(4) {
  ["id"]=>
  int(3)
  ["ad"]=>
  string(6) "Şeker"
  ["fiyat"]=>
  float(38.75)
  ["stok"]=>
  int(60)
}
*/

Bu örnek, önceki bölümdeki urunler tablosunu ve FETCH_ASSOC varsayılan modunu kullanır. Aynı $ekle nesnesi üç kez farklı değerlerle çalıştırıldı. İsimli yer tutucularda dizi anahtarları iki noktasız yazılabilir. lastInsertId() son eklenen satırın kimliğini string olarak döndürür. rowCount() son INSERT, UPDATE veya DELETE ile etkilenen satır sayısını verir; kılavuz SELECT için bu değerin tanımsız olduğunu ve sürücüden sürücüye değiştiğini, taşınabilir kodda ona güvenilmemesi gerektiğini belirtir. Satır saymak için SELECT COUNT(*) kullanın.

Bu kurulumda SQLite sürücüsü INTEGER ve REAL sütunları PHP'nin int ve float tipleriyle döndürdü. Farklı sürücülerde ve ayarlarda sayısal değerlerin string olarak gelebileceğini hesaba katın; karşılaştırmalarda bu farkın neden önemli olduğu eşitlik ve tip dönüşümü yazısında anlatılıyor.

Yer tutucunun sınırları

Yer tutucu yalnızca bir değerin yerine geçer; tablo adı, sütun adı, ORDER BY yönü veya SQL anahtar kelimesi yerine kullanılamaz. Kılavuzdaki örnekte de gösterildiği gibi LIKE '%?%' yazmak yanlıştır; yer tutucu değerin tamamını temsil etmelidir. Joker karakterleri değere eklersiniz:

php
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE kullanicilar (ad TEXT)');
$pdo->exec("INSERT INTO kullanicilar VALUES ('ayse'), ('mehmet')");

$girdi = "x' OR '1'='1";

$tehlikeli = "SELECT COUNT(*) FROM kullanicilar WHERE ad = '$girdi'";
echo $pdo->query($tehlikeli)->fetchColumn(), "\n"; // çıktı: 2

$guvenli = $pdo->prepare('SELECT COUNT(*) FROM kullanicilar WHERE ad = ?');
$guvenli->execute([$girdi]);
echo $guvenli->fetchColumn(), "\n"; // çıktı: 0

$ara = $pdo->prepare('SELECT ad FROM kullanicilar WHERE ad LIKE ?');
$ara->execute(['%eh%']);
print_r($ara->fetchAll(PDO::FETCH_COLUMN));
/*
Array
(
    [0] => mehmet
)
*/

Değeri doğrudan SQL metnine yerleştiren ilk sorguda girdi, WHERE koşulunu her satır için doğru yapan bir ifadeye dönüştü ve iki kullanıcının tamamı sayıldı. Hazırlanmış sorguda aynı metin yalnızca bir veri olarak ele alındı ve eşleşme bulunamadı. Sütun veya sıralama yönünü kullanıcıya seçtirmeniz gerekiyorsa izin verilen değerleri bir dizide tutup girdiyi bu listeyle karşılaştırın; match ifadesi bunun için kısa bir yol sunar.

bindValue ve bindParam

Değerleri execute() dizisiyle vermek yerine tek tek bağlayabilirsiniz. bindValue() o anki değeri bağlar. bindParam() ise değişkeni referans olarak bağlar ve değer ancak execute() çağrıldığı anda okunur; döngüde aynı sorguyu çalıştırırken işe yarar. Üçüncü parametre PDO::PARAM_INT, PDO::PARAM_STR gibi bir tip sabitidir ve varsayılan değeri PDO::PARAM_STR olur. execute() dizisiyle verilen bütün değerler ise kılavuza göre PDO::PARAM_STR olarak ele alınır; bu yüzden LIMIT gibi tam sayı bekleyen yerlerde bindValue(..., PDO::PARAM_INT) tercih edilir.

php
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE t (n INTEGER)');
$s = $pdo->prepare('SELECT * FROM t LIMIT :adet');
$s->bindValue(':adet', 5, PDO::PARAM_INT);
var_dump($s->execute()); // çıktı: bool(true)

$s = $pdo->prepare('INSERT INTO t (n) VALUES (:n)');
$s->bindParam(':n', $n, PDO::PARAM_INT);
foreach ([1, 2, 3] as $n) {
    $s->execute();
}
echo $pdo->query('SELECT SUM(n) FROM t')->fetchColumn(), "\n"; // çıktı: 6

Referansların nasıl çalıştığını merak ediyorsanız PHP referansları yazısına bakabilirsiniz.

Sonuçları almak: fetch modları

fetch() bir sonraki satırı, fetchAll() kalan tüm satırları döndürür. Satır kalmadığında fetch() false döner. Varsayılan mod PDO::FETCH_BOTH olup her değeri hem sütun adıyla hem sütun numarasıyla verir; bu da her satırda değerlerin iki kez yer alması demektir. Bu nedenle bağlantıda PDO::ATTR_DEFAULT_FETCH_MODE ile FETCH_ASSOC seçmek yaygındır.

php
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE urunler (id INTEGER PRIMARY KEY, ad TEXT, fiyat REAL, stok INTEGER)');
$pdo->exec("INSERT INTO urunler (ad, fiyat, stok) VALUES ('Çay', 45.5, 120), ('Kahve', 210, 0), ('Şeker', 38.75, 60)");

$s = $pdo->query('SELECT id, ad FROM urunler WHERE id = 1');
print_r($s->fetch());
/*
Array
(
    [id] => 1
    [0] => 1
    [ad] => Çay
    [1] => Çay
)
*/
print_r($pdo->query('SELECT id, ad FROM urunler')->fetchAll(PDO::FETCH_KEY_PAIR));
/*
Array
(
    [1] => Çay
    [2] => Kahve
    [3] => Şeker
)
*/
print_r($pdo->query('SELECT ad FROM urunler')->fetchAll(PDO::FETCH_COLUMN));
/*
Array
(
    [0] => Çay
    [1] => Kahve
    [2] => Şeker
)
*/
var_dump($pdo->query('SELECT COUNT(*) FROM urunler')->fetchColumn()); // çıktı: int(3)

$o = $pdo->query('SELECT ad, fiyat FROM urunler WHERE id = 2')->fetch(PDO::FETCH_OBJ);
echo $o->ad, ' ', $o->fiyat, "\n"; // çıktı: Kahve 210
var_dump($pdo->query('SELECT ad FROM urunler WHERE id = 99')->fetch()); // çıktı: bool(false)
ModDöndürdüğüNot
FETCH_ASSOCsütun adlı dizien yaygın tercih
FETCH_BOTHad ve numara ile dizivarsayılan, değerler iki kez
FETCH_NUMnumaralı dizisütun sırası sorguya bağlı
FETCH_OBJstdClass nesnesi$satir->ad ile erişim
FETCH_CLASSbelirtilen sınıfın nesnesiözellikler sütunlardan doldurulur
FETCH_COLUMNtek sütunfetchAll ile düz liste
FETCH_KEY_PAIRilk sütun anahtar, ikinci değeryalnızca iki sütun, fetchAll ile

FETCH_KEY_PAIR sonucun tam olarak iki sütun içermesini ister; ilk sütun benzersiz değilse bazı değerler kaybolur ve hangilerinin kaybolacağı tanımsızdır. İki tabloyu birleştirirken aynı adlı sütunlar varsa (örneğin her iki tabloda created_at) dizide yalnızca biri kalır; kılavuz bu durumda sütunlara AS ile takma ad vermenizi önerir. Tek bir değere ihtiyacınız olduğunda fetchColumn() en kısa yoldur; ancak aynı satırdan başka bir sütunu sonradan almanın yolu yoktur.

Satırları kendi sınıfınıza doldurmak

php
<?php
final class Urun
{
    public int $id;
    public string $ad;
    public float $fiyat;

    public function etiket(): string
    {
        return sprintf('%s (%.2f TL)', $this->ad, $this->fiyat);
    }
}

$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE urunler (id INTEGER PRIMARY KEY, ad TEXT, fiyat REAL)');
$pdo->exec("INSERT INTO urunler (ad, fiyat) VALUES ('Çay', 45.5), ('Şeker', 38.75)");

$s = $pdo->query('SELECT id, ad, fiyat FROM urunler ORDER BY id');
foreach ($s->fetchAll(PDO::FETCH_CLASS, Urun::class) as $urun) {
    echo $urun->etiket(), "\n";
}
/*
Çay (45.50 TL)
Şeker (38.75 TL)
*/

FETCH_CLASS sütun değerlerini aynı adlı özelliklere yazar. Kılavuza göre sınıfta karşılığı olmayan bir sütun gelirse özellik dinamik olarak oluşturulur; bu davranış kullanımdan kaldırılmıştır ve PHP 9.0'da hataya dönüşecektir. Bu yüzden sorguda yalnızca sınıfta tanımlı sütunları seçin. Yapıcının özellikler atanmadan önce çalışmasını istiyorsanız PDO::FETCH_PROPS_LATE bayrağını eklersiniz. Sınıf ve özellik tanımlama PHP sınıf ve nesne, sprintf() biçimleri sprintf ve number_format yazısında.

Transaction: hep birlikte ya da hiç

PDO bağlantıyı "auto-commit" modunda açar: Her sorgu, veritabanı destekliyorsa kendi örtük transaction'ı içinde hemen kalıcı olur. Birden çok sorgunun bir bütün olarak uygulanması gerekiyorsa beginTransaction() ile bir transaction başlatır, her şey yolunda giderse commit(), bir hata olursa rollBack() çağırırsınız. Kılavuz, betik bittiğinde ya da bağlantı kapanırken tamamlanmamış bir transaction varsa PDO'nun onu otomatik olarak geri aldığını belirtir. Aşağıdaki para transferi örneğinde ikinci transfer, bakiyenin negatife düşmesini engelleyen CHECK kısıtına takılır ve geri alınır.

php
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE hesaplar (ad TEXT PRIMARY KEY, bakiye INTEGER NOT NULL CHECK (bakiye >= 0))');
$pdo->exec("INSERT INTO hesaplar VALUES ('ayse', 100), ('ali', 20)");

function transfer(PDO $pdo, string $kimden, string $kime, int $tutar): void
{
    $pdo->beginTransaction();
    try {
        $dus = $pdo->prepare('UPDATE hesaplar SET bakiye = bakiye - ? WHERE ad = ?');
        $dus->execute([$tutar, $kimden]);
        $ekle = $pdo->prepare('UPDATE hesaplar SET bakiye = bakiye + ? WHERE ad = ?');
        $ekle->execute([$tutar, $kime]);
        $pdo->commit();
    } catch (PDOException $e) {
        $pdo->rollBack();
        throw $e;
    }
}

transfer($pdo, 'ayse', 'ali', 30);
try {
    transfer($pdo, 'ali', 'ayse', 500);
} catch (PDOException $e) {
    echo 'Hata: ', $e->getMessage(), "\n";
    echo 'SQLSTATE: ', $e->getCode(), "\n";
}
var_dump($pdo->inTransaction());
print_r($pdo->query('SELECT ad, bakiye FROM hesaplar ORDER BY ad')->fetchAll(PDO::FETCH_KEY_PAIR));
/*
Hata: SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: bakiye >= 0
SQLSTATE: 23000
bool(false)
Array
(
    [ali] => 50
    [ayse] => 70
)
*/

İlk transfer başarılı oldu: Ayşe'nin bakiyesi 70'e indi, Ali'ninki 50'ye çıktı. İkinci transferde Ali'den 500 düşmek kısıtı ihlal etti, rollBack() çağrıldı ve iki hesap da önceki değerinde kaldı. catch içinde istisnayı yeniden fırlatmak, hatanın çağıran koda ulaşmasını sağlar; yalnızca geri alıp sessiz kalmak sorunları gizler. inTransaction() açık bir transaction olup olmadığını söyler; burada false döndü çünkü transaction geri alınarak kapandı.

exec, query ve prepare: hangisi ne zaman?

exec, query ve prepare: hangisi ne zaman? exec, query, prepare + execute
MetotDöndürdüğüNe zaman
execetkilenen satır sayısı (int) veya falseparametresiz DDL ve sabit komutlar
queryPDOStatement veya falseparametresiz, bir kez çalışacak SELECT
prepare + executePDOStatement / booldışarıdan değer alan her sorgu

exec() etkilenen satır yoksa 0 döndürür; kılavuz bu değerin false ile karışmaması için === kullanılmasını önerir. Kural basittir: Sorguya dışarıdan bir değer giriyorsa, bu değer formdan, URL'den, dosyadan ya da başka bir sistemden geliyor olsun, prepare() kullanın. Girdinin biçimini önceden doğrulamak için filter_var, kullanıcı şifrelerini veritabanında saklamak için password_hash yazılarına bakabilirsiniz.

Sık yapılan hatalar

  • Kullanıcı girdisini "... WHERE ad = '$ad'" biçiminde SQL metnine eklemek. Her dış değeri yer tutucuyla gönderin.
  • Tablo veya sütun adını yer tutucuyla vermeye çalışmak. Yer tutucu yalnızca değerin yerine geçer; adları izin listesiyle doğrulayın.
  • LIKE '%?%' yazmak. Joker karakterleri değere ekleyin: execute(['%' . $ara . '%']).
  • SELECT sonrası satır saymak için rowCount() kullanmak. Davranış sürücüye bağlıdır; COUNT(*) kullanın.
  • Varsayılan FETCH_BOTH ile çalışıp her değerin iki kez geldiğini fark etmemek. ATTR_DEFAULT_FETCH_MODE ayarlayın.
  • Transaction içinde hata yakalayıp rollBack() çağırmayı unutmak ya da istisnayı sessizce yutmak.
  • Bağlantı hatası mesajını olduğu gibi ziyaretçiye göstermek. Ayrıntıyı günlüğe yazın, kullanıcıya genel bir mesaj verin.

Sık Sorulan Sorular

PDO ile mysqli arasındaki fark nedir?

mysqli yalnızca MySQL ve uyumlu sunucular için bir eklentidir. PDO ise sürücü tabanlı ortak bir arayüzdür; aynı prepare, execute ve fetch çağrıları SQLite, PostgreSQL ve MySQL'de çalışır. Bu seri dil ve standart kütüphane odaklı olduğu için örneklerde PDO kullanıldı.

PDO hata modunu ayarlamak hâlâ gerekli mi?

PHP 8.0'dan beri varsayılan ERRMODE_EXCEPTION olduğu için zorunlu değildir. Yine de seçenek dizisinde açıkça yazmak niyetinizi belgeler ve kodun farklı ortamlarda aynı davranmasını sağlar.

Hazırlanmış sorgu her durumda SQL enjeksiyonunu engeller mi?

Değerler yalnızca yer tutucuyla gönderildiğinde evet. Ancak kılavuzun da belirttiği gibi sorgunun başka bir parçası (tablo adı, sıralama, ham SQL parçası) kaçırılmamış girdiyle kuruluyorsa risk devam eder.

Bellek içi SQLite veritabanı ne zaman kaybolur?

sqlite::memory: ile açılan veritabanı yalnızca bellekte durur; kalıcı veri için sqlite: önekinden sonra diskteki dosyanın mutlak yolunu verin.

PDO::connect kullanmam gerekiyor mu?

Hayır. new PDO(...) çalışmaya devam eder. PDO::connect() PHP 8.4 ile geldi ve sürücüye özel alt sınıfın metotlarına erişmek istediğinizde işe yarar.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: PHP öğrenme rehberi · SQL injection önleme · SQLite nedir · PHP password_hash · PHP try/catch