PDO (PHP Data Objects) adalah cara bawaan untuk berkomunikasi dengan database. Buat koneksi dengan new PDO($dsn, $user, $password), lalu jalankan setiap query yang berisi nilai dengan prepare() dan execute(), agar nilainya dikirim terpisah dari SQL:
<?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
Contoh-contoh di halaman ini membutuhkan server database, jadi ditampilkan sebagai kode biasa. Contoh SQL injection di bagian bawah bisa dijalankan.
Koneksi ke MySQL dengan PDO
Argumen pertama adalah DSN: nama driver, lalu detail koneksinya. Taruh koneksinya di satu tempat dan include di tempat yang membutuhkannya.
<?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=utf8mb4harus ada di DSN. Tanpanya, huruf beraksen dan emoji bisa datang sebagai?atau byte yang rusak.- Jangan pernah mencetak
$e->getMessage()ke pengunjung: isinya bisa memuat host dan nama user. Catat di log saja. - Jauhkan password dari kode, simpan di environment variable atau file konfigurasi di luar web root.
Database lain hanya mengubah DSN-nya: pgsql:host=localhost;dbname=shop untuk PostgreSQL, sqlite:/var/data/app.db untuk file SQLite. Masing-masing membutuhkan driver PDO-nya terpasang (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m menampilkan driver yang kamu punya.
Prepared statement dengan ? dan :name
Placeholder menandai tempat sebuah nilai dimasukkan. Pakai placeholder posisional ? dengan list, atau placeholder bernama dengan array asosiatif:
<?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();
Jangan beri tanda kutip di sekitar placeholder: WHERE name = '?' membandingkan dengan teks literal ?. Prepared statement juga bisa dijalankan berkali-kali dengan nilai yang berbeda, jadi siapkan sekali sebelum loop, bukan di dalamnya.
Kenapa menyambung string di SQL berbahaya
Inilah satu-satunya blok di halaman ini yang bisa dijalankan: blok ini menyusun query dengan cara yang salah dan cara yang benar untuk input yang sama. Ubah $email lalu jalankan lagi.
Di query hasil penyambungan, tanda kutip di input menutup string-nya, dan OR '1'='1' membuat kondisinya true untuk setiap baris: query itu mengembalikan semua user. Di form login, itu berarti bisa masuk tanpa mengetahui password-nya, dan jika driver menerima beberapa pernyataan dalam satu pemanggilan, input seperti '; DROP TABLE users; -- menjalankan pernyataan kedua yang dipilih penyerang. Di versi prepared, database menerima SQL dan nilainya secara terpisah, jadi tanda kutip itu hanya sebuah karakter di dalam email yang tidak cocok dengan siapa pun.
Fungsi escaping seperti addslashes() bukan solusi: fungsi itu melewatkan kasus-kasus yang bergantung pada character set koneksi. Pakai placeholder untuk setiap nilai, termasuk angka dan nilai yang "berasal dari kodemu sendiri".
Mengambil baris: fetch, fetchAll, dan 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', ...]
query() boleh dipakai saat SQL-nya tidak berisi nilai dari luar. Begitu ada variabel yang terlibat, beralihlah ke prepare().
Insert, update, dan delete
<?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() mengembalikan string, jadi lakukan cast. Dengan MySQL, rowCount() setelah UPDATE menghitung baris yang benar-benar berubah, jadi mengisi nama dengan nilai yang sudah dimilikinya menghasilkan 0. Simpan password dengan password_hash, jangan pernah sebagai teks biasa.
Transaksi
Transaksi membuat beberapa pernyataan berhasil atau gagal bersama-sama. Jika ada yang melempar exception, lakukan rollback dan tidak ada yang tersimpan:
<?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;
}
Cara ini bergantung pada error yang dilempar sebagai exception (default sejak PHP 8.0), jadi pernyataan yang gagal langsung melompat ke catch. Dengan MySQL, tabelnya harus memakai InnoDB; MyISAM mengabaikan transaksi. Halaman try/catch membahas Throwable dan melempar ulang exception.
WHERE IN dengan array
Satu ? hanya menampung satu nilai, jadi IN (?) dengan array tidak berfungsi. Buat satu placeholder untuk setiap nilai:
<?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
}
String yang diinterpolasi ke dalam SQL hanya berisi tanda tanya dan koma, jadi query-nya tetap aman.
Kesalahan umum dengan placeholder
- Nama tabel dan kolom.
ORDER BY ?mengurutkan berdasarkan string konstan, bukan berdasarkan kolom. Pilih identifier dari allowlist:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, lalu masukkan$sortke SQL. LIMIT ?dengan emulated prepares. DenganATTR_EMULATE_PREPARESdefault di MySQL,execute(['10'])mengirimLIMIT '10', yang merupakan syntax error. Bind denganPDO::PARAM_INTatau matikan emulasinya.- Named placeholder yang sama dipakai dua kali.
WHERE first = :q OR last = :qhanya berfungsi dengan emulated prepares. Dengan prepare sungguhan, kamu mendapatSQLSTATE[HY093]: Invalid parameter number; pakai:q1dan:q2. - Lupa bahwa
fetch()mengembalikanfalse.$user['name']padafalsemenghasilkan warning dannull; periksa hasilnya lebih dulu.
Pertanyaan yang sering diajukan
Bagaimana cara terhubung ke MySQL dengan PDO?
Buat object PDO dengan DSN, user, dan password: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Koneksi yang gagal melempar PDOException; tangkap, catat pesannya di log, dan tampilkan error umum kepada pengunjung.
Apakah prepared statement mencegah SQL injection?
Ya, untuk nilai: dengan prepare('... WHERE id = ?') dan execute([$id]), nilainya dikirim sebagai data dan tidak pernah bisa mengubah query. Placeholder tidak bisa mewakili nama tabel, nama kolom, atau ASC/DESC, jadi periksa hal-hal itu terhadap daftar nama yang diizinkan sebelum memasukkannya ke SQL.
Apa perbedaan fetch dan fetchAll di PDO?
fetch() mengembalikan baris berikutnya (atau false saat tidak ada lagi), jadi cocok untuk satu baris atau perulangan atas hasil yang besar. fetchAll() mengembalikan semua baris dalam array sekaligus. fetchColumn() mengembalikan satu nilai dari baris berikutnya, praktis untuk SELECT COUNT(*).
Bagaimana cara memakai WHERE IN dengan array di PDO?
Buat satu ? untuk setiap nilai lalu berikan array-nya ke execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Periksa dulu bahwa array-nya tidak kosong, karena IN () adalah syntax error.
Sebaiknya saya memakai PDO atau mysqli?
Pakai PDO kecuali kamu butuh fitur yang khusus MySQL. API-nya sama untuk MySQL, PostgreSQL, SQLite, dan lainnya, mendukung named placeholder, dan sejak PHP 8.0 melempar exception saat terjadi error secara default. mysqli hanya bekerja dengan MySQL dan MariaDB.