Database dengan PDO
Koneksi PDO, prepared statement, fetch mode, transaksi, penanganan error, dan pola repository sederhana.
Apa itu PDO?
PDO (PHP Data Objects) adalah antarmuka tunggal untuk mengakses banyak database (MySQL/MariaDB, PostgreSQL, SQLite, SQL Server). Cukup ganti DSN-nya, kode query tetap sama. Jangan pakai ekstensi mysql_* lama (sudah dihapus); pakai PDO atau MySQLi.
Koneksi
$dsn = 'mysql:host=127.0.0.1;dbname=toko;charset=utf8mb4';
$pdo = new PDO($dsn, 'user', 'rahasia', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // error jadi PDOException
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // hasil berupa array asosiatif
PDO::ATTR_EMULATE_PREPARES => false, // prepared statement sungguhan
]);
// untuk belajar tanpa server: SQLite
$pdo = new PDO('sqlite:' . __DIR__ . '/toko.sqlite');
Simpan kredensial di .env/konfigurasi, bukan di kode. Tiga opsi di atas adalah standar yang direkomendasikan.
Prepared statement — pertahanan SQL Injection
// SALAH — rawan injeksi
$pdo->query("SELECT * FROM users WHERE email = '$email'");
// BENAR — data dipisah dari perintah SQL
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND aktif = :aktif');
$stmt->execute(['email' => $email, 'aktif' => 1]);
$user = $stmt->fetch(); // satu baris, atau false
$semua = $pdo->prepare('SELECT id, nama FROM produk WHERE harga <= ?');
$semua->execute([100000]);
foreach ($semua->fetchAll() as $p) { echo $p['nama']; }
Dengan placeholder (:nama atau ?), database menerima SQL dan data secara terpisah sehingga data tidak pernah ditafsirkan sebagai perintah. Hanya nilai yang boleh jadi placeholder — nama tabel/kolom tidak bisa. Untuk ORDER BY dinamis, cocokkan dengan daftar putih.
CRUD
// Create
$pdo->prepare('INSERT INTO produk (nama, harga) VALUES (?, ?)')->execute(['Mouse', 75000]);
$idBaru = (int) $pdo->lastInsertId();
// Update
$st = $pdo->prepare('UPDATE produk SET harga = :harga WHERE id = :id');
$st->execute(['harga' => 80000, 'id' => $idBaru]);
echo $st->rowCount(); // baris yang terpengaruh
// Delete
$pdo->prepare('DELETE FROM produk WHERE id = ?')->execute([$idBaru]);
Fetch mode
| Mode | Hasil |
|---|---|
PDO::FETCH_ASSOC | ['nama' => 'Mouse'] |
PDO::FETCH_OBJ | Objek anonim: $row->nama |
PDO::FETCH_CLASS | Langsung jadi objek class milikmu (cocok dengan constructor promotion/DTO) |
PDO::FETCH_COLUMN | Satu kolom saja → daftar nilai |
PDO::FETCH_KEY_PAIR | [id => nama] |
Transaksi — semua berhasil atau tidak sama sekali
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE rekening SET saldo = saldo - ? WHERE id = ?')->execute([50000, 1]);
$pdo->prepare('UPDATE rekening SET saldo = saldo + ? WHERE id = ?')->execute([50000, 2]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
Tanpa transaksi, jika proses mati di tengah, uang bisa hilang dari rekening 1 tapi belum masuk rekening 2. Transaksi menjamin sifat ACID. Gunakan untuk setiap rangkaian perubahan yang harus konsisten (membuat pesanan + mengurangi stok).
Pola Repository
Jangan menyebar SQL ke seluruh aplikasi. Kumpulkan akses data di class khusus:
final class ProdukRepository
{
public function __construct(private PDO $pdo) {}
/** @return Produk[] */
public function semua(): array
{
return $this->pdo->query('SELECT id, nama, harga FROM produk ORDER BY nama')
->fetchAll(PDO::FETCH_CLASS, Produk::class);
}
public function cari(int $id): ?Produk
{
$st = $this->pdo->prepare('SELECT id, nama, harga FROM produk WHERE id = ?');
$st->execute([$id]);
return $st->fetchObject(Produk::class) ?: null;
}
}
Keuntungannya: SQL terpusat, mudah diganti/diuji, dan bagian lain aplikasi cukup memanggil $repo->cari(5). Framework seperti Laravel membungkus pola ini dalam ORM (Eloquent), tetapi memahami PDO membuatmu mengerti apa yang terjadi di baliknya.
Praktik baik
- Satu koneksi per request (jangan buat PDO berulang).
- Pilih kolom yang dibutuhkan, hindari
SELECT *di kode produksi. - Tambahkan indeks pada kolom yang sering di-
WHERE; waspadai query N+1 di dalam loop. - Gunakan migrasi (Phinx, Laravel migration) untuk mengelola skema.