Menu

PHP PDO: MySQL'e Bağlanma ve Hazırlanmış İfadeler

PDO, PHP'nin veritabanı katmanıdır: new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass) ile bağlanın, sonra değer içeren her sorguyu prepare() ve execute([$value]) üzerinden çalıştırın. Bağlanmayı, hazırlanmış ifadeleri, satır getirmeyi, eklemeleri, işlemleri (transaction) ve WHERE IN kullanımını öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

PDO (PHP Data Objects) bir veritabanıyla konuşmanın yerleşik yoludur. new PDO($dsn, $user, $password) ile bağlanın, sonra değer içeren her sorguyu prepare() ve execute() ile çalıştırın, böylece değer SQL'den ayrı gönderilir:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();      // an array, or false if no row matched

Bu sayfadaki örnekler bir veritabanı sunucusu gerektirdiği için düz kod olarak gösterilir. Aşağıdaki SQL injection örneği çalışır.

PDO ile MySQL'e bağlanma

İlk argüman bir DSN'dir: sürücü adı, ardından bağlantı ayrıntıları. Bağlantıyı tek bir yere koyun ve ihtiyaç duyduğunuz yerde dahil edin.

<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // the default since PHP 8.0
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // rows as ['id' => 1, ...]
    PDO::ATTR_EMULATE_PREPARES   => false,                   // real prepared statements, real int types
];

try {
    $pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    http_response_code(500);
    exit('Database unavailable.');
}

return $pdo;
  • charset=utf8mb4 DSN'ye aittir. O olmadan aksanlı harfler ve emoji ? veya bozuk baytlar olarak gelebilir.
  • $e->getMessage() değerini asla ziyaretçilere yazdırmayın: sunucu ve kullanıcı adını içerebilir. Onu loglayın.
  • Parolayı koddan uzak tutun, bir ortam değişkeninde veya web kökünün dışındaki bir yapılandırma dosyasında.

Diğer veritabanları yalnızca DSN'yi değiştirir: PostgreSQL için pgsql:host=localhost;dbname=shop, bir SQLite dosyası için sqlite:/var/data/app.db. Her biri kendi PDO sürücüsünün kurulu olmasını gerektirir (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m sahip olduklarınızı listeler.

? ve :name ile hazırlanmış ifadeler

Bir yer tutucu bir değerin nereye gideceğini işaretler. Bir listeyle konumsal ? yer tutucularını ya da ilişkisel bir diziyle adlandırılmış olanları kullanın:

<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);

// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);

// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();

Bir yer tutucunun etrafına tırnak koymayın: WHERE name = '?' düz ? metniyle karşılaştırır. Hazırlanmış bir ifade farklı değerlerle birçok kez de çalıştırılabilir, bu yüzden onu bir döngünün içinde değil, döngüden önce bir kez hazırlayın.

SQL'de dizge birleştirme neden tehlikelidir

Bu, sayfada çalışan tek bloktur: aynı girdi için sorguyu yanlış ve doğru şekilde oluşturur. $email değerini değiştirin ve tekrar çalıştırın.

Birleştirilmiş sorguda girdideki tırnak dizgeyi kapatır ve OR '1'='1' koşulu her satır için doğru yapar: sorgu tüm kullanıcıları döndürür. Bir giriş formunda bu parolayı bilmeden giriş yapmak demektir ve sürücü tek çağrıda birkaç ifade kabul ediyorsa '; DROP TABLE users; -- gibi bir girdi saldırganın seçtiği ikinci bir ifadeyi çalıştırır. Hazırlanmış sürümde veritabanı SQL'i ve değeri ayrı alır, bu yüzden tırnak kimseyle eşleşmeyen bir e-postanın içindeki sıradan bir karakterdir.

addslashes() gibi kaçışlama fonksiyonları bir çözüm değildir: bağlantının karakter kümesine bağlı durumları kaçırırlar. Sayılar ve "kendi kodunuzdan gelen" değerler dahil her değer için yer tutucular kullanın.

Satır getirme: fetch, fetchAll ve fetchColumn

<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch();                 // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
    http_response_code(404);
}

// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();

// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
    echo $row['name'], "\n";
}

// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();

// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR);  // [42 => 'Ada', ...]

SQL dışarıdan değer içermediğinde query() sorun değildir. İşin içine bir değişken girdiği anda prepare() kullanın.

Ekleme, güncelleme ve silme

<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();

$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount();                 // rows changed

$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();

lastInsertId() bir dizge döndürür, bu yüzden onu dönüştürün. MySQL ile bir UPDATE sonrasında rowCount() gerçekten değişen satırları sayar, bu yüzden bir ismi zaten sahip olduğu değere ayarlamak 0 verir. Parolaları asla düz metin olarak değil, password_hash ile saklayın.

İşlemler (transaction)

Bir işlem birkaç ifadenin birlikte başarılı ya da birlikte başarısız olmasını sağlar. Herhangi bir şey hata fırlatırsa geri alın ve hiçbir şey kaydedilmez:

<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
    $pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    throw $e;
}

Bu, hataların istisna fırlatmasına dayanır (PHP 8.0'dan beri varsayılan), böylece başarısız bir ifade catch bloğuna atlar. MySQL ile tablolar InnoDB kullanmalıdır; MyISAM işlemleri yok sayar. try/catch sayfası Throwable ve yeniden fırlatmayı anlatır.

Bir diziyle WHERE IN

Tek bir ? tek bir değer tutar, bu yüzden bir diziyle IN (?) çalışmaz. Değer başına bir yer tutucu oluşturun:

<?php
$ids = [3, 8, 15];

if ($ids) {
    $placeholders = implode(',', array_fill(0, count($ids), '?'));   // "?,?,?"
    $stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
    $stmt->execute($ids);
    $products = $stmt->fetchAll();
} else {
    $products = [];                     // IN () would be a syntax error
}

SQL'e eklenen dizge yalnızca soru işaretleri ve virgüllerden oluşur, bu yüzden sorgu yine güvenlidir.

Yer tutucularla sık yapılan hatalar

  • Tablo ve sütun adları. ORDER BY ? sütuna göre değil sabit bir dizgeye göre sıralar. Tanımlayıcıları bir izin listesinden seçin: $sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, sonra $sort değerini SQL'e koyun.
  • Taklit edilen hazırlamalarla LIMIT ?. MySQL'de varsayılan ATTR_EMULATE_PREPARES ile execute(['10']) bir söz dizimi hatası olan LIMIT '10' gönderir. Onu PDO::PARAM_INT ile bağlayın ya da taklidi kapatın.
  • Aynı adlandırılmış yer tutucuyu iki kez kullanmak. WHERE first = :q OR last = :q yalnızca taklit edilen hazırlamalarla çalışır. Gerçek hazırlamalarla SQLSTATE[HY093]: Invalid parameter number alırsınız; :q1 ve :q2 kullanın.
  • fetch() fonksiyonunun false döndürdüğünü unutmak. false üzerinde $user['name'] bir uyarı ve null olur; önce sonucu kontrol edin.

Sıkça Sorulan Sorular

PDO ile MySQL'e nasıl bağlanırım?

Bir DSN, kullanıcı ve parolayla bir PDO nesnesi oluşturun: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Başarısız bir bağlantı bir PDOException fırlatır; onu yakalayın, mesajı loglayın ve ziyaretçiye genel bir hata gösterin.

Hazırlanmış ifadeler SQL injection'ı önler mi?

Evet, değerler için: prepare('... WHERE id = ?') ve execute([$id]) ile değer veri olarak gönderilir ve sorguyu asla değiştiremez. Yer tutucular tablo adları, sütun adları veya ASC/DESC yerine geçemez, bu yüzden onları SQL'e koymadan önce izin verilen adlar listesiyle kontrol edin.

PDO'da fetch ile fetchAll arasındaki fark nedir?

fetch() bir sonraki satırı (ya da başka satır kalmadığında false) döndürür, bu yüzden tek bir satıra veya büyük bir sonuç üzerinde bir döngüye uygundur. fetchAll() her satırı tek seferde bir dizide döndürür. fetchColumn() bir sonraki satırdan tek bir değer döndürür, SELECT COUNT(*) için kullanışlıdır.

PDO'da WHERE IN'i bir diziyle nasıl kullanırım?

Değer başına bir ? oluşturun ve diziyi execute() fonksiyonuna verin: $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Önce dizinin boş olmadığını kontrol edin, çünkü IN () bir söz dizimi hatasıdır.

PDO mu yoksa mysqli mi kullanmalıyım?

Yalnızca MySQL'e özel bir özelliğe ihtiyacınız yoksa PDO kullanın. API'si MySQL, PostgreSQL, SQLite ve diğerleri için aynıdır, adlandırılmış yer tutucuları destekler ve PHP 8.0'dan beri hatalarda varsayılan olarak istisna fırlatır. mysqli yalnızca MySQL ve MariaDB ile çalışır.

Coddy programlama dilleri çizimi

Coddy ile kodlamayı öğren

BAŞLA