Menu

PDO PHP: Koneksi MySQL dan Prepared Statement

PDO adalah lapisan database PHP: buat koneksi dengan new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass), lalu jalankan setiap query yang berisi nilai lewat prepare() dan execute([$value]). Pelajari koneksi, prepared statement, mengambil baris, insert, transaksi, dan WHERE IN.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

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=utf8mb4 harus 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 $sort ke SQL.
  • LIMIT ? dengan emulated prepares. Dengan ATTR_EMULATE_PREPARES default di MySQL, execute(['10']) mengirim LIMIT '10', yang merupakan syntax error. Bind dengan PDO::PARAM_INT atau matikan emulasinya.
  • Named placeholder yang sama dipakai dua kali. WHERE first = :q OR last = :q hanya berfungsi dengan emulated prepares. Dengan prepare sungguhan, kamu mendapat SQLSTATE[HY093]: Invalid parameter number; pakai :q1 dan :q2.
  • Lupa bahwa fetch() mengembalikan false. $user['name'] pada false menghasilkan warning dan null; 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.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai