Prepared statement memisahkan struktur SQL dari data, tetapi pemisahan tersebut baru terasa manfaatnya kalau kita punya cara yang rapi untuk mengirim nilai PHP ke setiap penanda parameter. Mekanisme itu bernama parameter binding: proses mengikat nilai ke penanda posisi (?) atau penanda bernama (:nama) sebelum statement dieksekusi. Pada chapter ini kita akan membedah tiga cara binding di PDO — bindValue(), bindParam(), dan array pada execute() — termasuk perbedaan perilakunya yang sering menjebak pemula. Seluruh contoh tetap memakai SQLite in-memory supaya dapat langsung Anda jalankan tanpa server database terpisah.
Mengikat Nilai dengan bindValue #
bindValue() mengirim satu nilai ke satu penanda parameter pada saat pemanggilan dilakukan. Untuk penanda posisi ?, argumen pertamanya adalah nomor parameter yang dihitung mulai dari 1, bukan 0. Argumen kedua adalah nilai yang ingin dikirim, dan argumen ketiga yang opsional adalah tipe parameternya.
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, harga INTEGER)");
$pdo->exec("INSERT INTO produk (nama, harga) VALUES
('Kopi Hitam', 18000),
('Teh Melati', 12000),
('Roti Bakar', 25000)");
$stmt = $pdo->prepare("SELECT nama, harga FROM produk WHERE id = ?");
$stmt->bindValue(1, 2, PDO::PARAM_INT);
$stmt->execute();
$produk = $stmt->fetch(PDO::FETCH_ASSOC);
echo "{$produk['nama']} - Rp{$produk['harga']}\n";Output yang Dihasilkan:
Teh Melati - Rp12000Mari kita telaah baris-baris pentingnya:
prepare(...)menghasilkan objekPDOStatementdengan satu penanda posisi yang menunggu nilai.bindValue(1, 2, PDO::PARAM_INT)mengikat integer2ke penanda pertama; nomor1merujuk posisi?, bukan nilai id.execute()dijalankan tanpa argumen karena semua parameter sudah terikat.fetch(PDO::FETCH_ASSOC)mengambil satu baris hasil sebagai array asosiatif.
Perhatikan bahwa kita menyetel PDO::ATTR_ERRMODE ke PDO::ERRMODE_EXCEPTION di awal. Mode ini membuat kegagalan query langsung melempar PDOException alih-alih diam saja; pembahasan tuntasnya ada di chapter Database Errors
.
Parameter Bernama dan Array execute #
Ketika query memiliki banyak parameter, penanda posisi mudah tertukar urutannya. Penanda bernama (:nama) membuat kode lebih terbaca, dan execute() menerima array asosiatif yang memetakan nama parameter ke nilai:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, harga INTEGER)");
$pdo->exec("INSERT INTO produk (nama, harga) VALUES
('Kopi Hitam', 18000),
('Teh Melati', 12000),
('Roti Bakar', 25000)");
$stmt = $pdo->prepare("SELECT nama, harga FROM produk WHERE harga <= :maks ORDER BY harga");
$stmt->execute(['maks' => 15000]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo "{$row['nama']} - Rp{$row['harga']}\n";
}Output yang Dihasilkan:
Teh Melati - Rp12000Kunci array ('maks') harus cocok persis dengan nama penanda tanpa awalan titik dua, dan tanda kutip pada kunci boleh dipakai atau tidak. Cara ini paling ringkas untuk query sekali jalan, tetapi punya satu keterbatasan: seluruh nilai dikirim sebagai string karena execute() tidak menerima informasi tipe. Ketika tipe data penting — misalnya pada klausa LIMIT — kita kembali ke bindValue() dengan konstanta tipe, seperti dicontohkan pada bagian berikutnya.
Perbedaan bindValue dan bindParam #
Perbedaan paling fundamental dari kedua method ini adalah kapan nilai parameter dibaca oleh PDO:
| Aspek | bindValue() | bindParam() |
|---|---|---|
| Waktu nilai dibaca | saat bindValue() dipanggil | saat execute() dipanggil |
| Bentuk argumen | nilai apa pun (literal, hasil ekspresi) | hanya variabel, dikirim by reference |
| Kegunaan khas | nilai tetap per eksekusi | variabel yang isinya berubah antar eksekusi |
Karena bindParam() menerima variabel secara by reference, PDO membaca isi variabel pada saat execute() dijalankan, bukan saat binding dilakukan. Perhatikan eksperimen berikut:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, nama TEXT, jumlah INTEGER)");
$nama = "Teh Hijau";
$jumlah = 10;
// bindParam: nilai variabel dibaca saat execute()
$stmt1 = $pdo->prepare("INSERT INTO pesanan (nama, jumlah) VALUES (?, ?)");
$stmt1->bindParam(1, $nama);
$stmt1->bindParam(2, $jumlah, PDO::PARAM_INT);
$nama = "Teh Melati";
$jumlah = 25;
$stmt1->execute();
// bindValue: nilai disalin saat bindValue() dipanggil
$stmt2 = $pdo->prepare("INSERT INTO pesanan (nama, jumlah) VALUES (?, ?)");
$stmt2->bindValue(1, $nama);
$stmt2->bindValue(2, $jumlah, PDO::PARAM_INT);
$nama = "Kopi Hitam";
$jumlah = 99;
$stmt2->execute();
foreach ($pdo->query("SELECT id, nama, jumlah FROM pesanan", PDO::FETCH_ASSOC) as $row) {
echo "pesanan #{$row['id']}: {$row['nama']} x {$row['jumlah']}\n";
}Output yang Dihasilkan:
pesanan #1: Teh Melati x 25
pesanan #2: Teh Melati x 25Kedua baris output sengaja memuat nilai yang sama, dan justru di situlah buktinya bekerja:
- Pesanan #1 berisi
Teh Melati x 25— bukanTeh Hijau x 10. Perubahan$namadan$jumlahsetelahbindParam()tetap terbaca karena nilai baru dibaca ketikaexecute()dipanggil. - Pesanan #2 tetap
Teh Melati x 25meskipun variabel diubah menjadiKopi Hitam x 99setelahbindValue(). Nilai sudah disalin pada saat binding, sehingga perubahan variabel berikutnya diabaikan.
Menentukan Tipe Parameter #
PDO mengirim setiap parameter dengan tipe yang kita nyatakan lewat konstanta berikut:
| Konstanta | Untuk nilai |
|---|---|
PDO::PARAM_STR | string (tipe bawaan jika tidak ditentukan) |
PDO::PARAM_INT | integer |
PDO::PARAM_BOOL | boolean |
PDO::PARAM_NULL | null |
Tipe menjadi penting ketika database memperlakukan string dan integer secara berbeda. Boolean yang dikirim dengan PDO::PARAM_BOOL akan tersimpan sebagai 1 atau 0 pada kolom integer SQLite:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengaturan (kunci TEXT PRIMARY KEY, aktif INTEGER)");
$stmt = $pdo->prepare("INSERT INTO pengaturan (kunci, aktif) VALUES (:kunci, :aktif)");
$stmt->bindValue(':kunci', "notifikasi_email", PDO::PARAM_STR);
$stmt->bindValue(':aktif', true, PDO::PARAM_BOOL);
$stmt->execute();
$nilai = $pdo->query("SELECT aktif FROM pengaturan WHERE kunci = 'notifikasi_email'")->fetchColumn();
echo "aktif = " . var_export((bool) $nilai, true) . "\n";Output yang Dihasilkan:
aktif = trueKasus klasik lainnya adalah klausa LIMIT. Nilai limit yang dikirim sebagai string dapat ditolak oleh sebagian driver, sehingga pola yang aman adalah bindValue() dengan PDO::PARAM_INT:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, harga INTEGER)");
$pdo->exec("INSERT INTO produk (nama, harga) VALUES
('Kopi Hitam', 18000), ('Teh Melati', 12000), ('Roti Bakar', 25000)");
$stmt = $pdo->prepare("SELECT nama FROM produk ORDER BY harga DESC LIMIT ?");
$stmt->bindValue(1, 2, PDO::PARAM_INT);
$stmt->execute();
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $nama) {
echo "$nama\n";
}Output yang Dihasilkan:
Roti Bakar
Kopi HitamPada pdo_sqlite contoh di atas berjalan mulus karena driver ini longgar terhadap tipe. Pada driver lain — terutama MySQL versi lama dengan emulasi prepared statement aktif — LIMIT ? yang dikirim sebagai string memicu kegagalan query, sehingga kebiasaan menuliskan PDO::PARAM_INT sejak awal menjaga kode Anda tetap portabel.
Binding di dalam Loop #
Skenario nyata yang paling sering muncul adalah menyimpan banyak baris sekaligus, misalnya hasil unggahan keranjang belanja. Pola yang benar adalah menyiapkan statement satu kali, mengikat variabel dengan bindParam() satu kali, lalu memanggil execute() berulang kali:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, nama TEXT, jumlah INTEGER)");
$stmt = $pdo->prepare("INSERT INTO pesanan (nama, jumlah) VALUES (:nama, :jumlah)");
$stmt->bindParam(':nama', $nama);
$stmt->bindParam(':jumlah', $jumlah, PDO::PARAM_INT);
$daftar = [
["Kopi Hitam", 2],
["Teh Melati", 3],
["Roti Bakar", 1],
];
foreach ($daftar as [$nama, $jumlah]) {
$stmt->execute();
}
echo "total pesanan: " . $pdo->query("SELECT COUNT(*) FROM pesanan")->fetchColumn() . "\n";
foreach ($pdo->query("SELECT nama, jumlah FROM pesanan", PDO::FETCH_ASSOC) as $row) {
echo "{$row['nama']} x {$row['jumlah']}\n";
}Output yang Dihasilkan:
total pesanan: 3
Kopi Hitam x 2
Teh Melati x 3
Roti Bakar x 1Urutan peristiwa pada contoh di atas berjalan seperti ini:
bindParam()dipanggil sekali sebelum loop; PDO menyimpan referensi ke$namadan$jumlah.foreach ($daftar as [$nama, $jumlah])menugaskan ulang kedua variabel tersebut di setiap iterasi.execute()di dalam loop membaca isi terkini dari kedua variabel, sehingga tiga baris berbeda tersimpan dari satu statement yang sama.
Jebakan Umum Parameter Binding #
Beberapa perilaku PDO hanya terlihat ketika diuji. Pertama, satu penanda bernama boleh dipakai lebih dari sekali dalam satu query; PDO memetakan semua kemunculannya ke satu nilai:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT :salam || ', ' || :salam AS ucapan");
$stmt->execute(['salam' => "Halo"]);
echo $stmt->fetchColumn() . "\n";Output yang Dihasilkan:
Halo, HaloKedua, jangan mencampur penanda posisi dan penanda bernama dalam satu statement yang sama. pdo_sqlite kebetulkan toleran dan menerima campuran tersebut, tetapi driver lain seperti pdo_mysql menolaknya dengan galat SQLSTATE[HY093]: Invalid parameter number: mixed named and positional parameters. Pilih satu gaya penanda per statement agar kode tetap portabel antar database.
Ketiga, ingat bahwa array pada execute() mengikat semua nilai sebagai string, dan hasil execute() bernilai boolean — true saat sukses, false saat gagal pada mode error non-exception. Kedua fakta ini menjadi alasan method bindValue() dengan tipe eksplisit tetap relevan meskipun cara array lebih ringkas.
Ringkasan #
bindValue() menyalin nilai saat binding, bindParam() mengikat variabel yang dibaca saat execute(), dan array pada execute() adalah jalan pintas untuk nilai string. Tipe parameter dijaga lewat konstanta seperti PDO::PARAM_INT, dan pola prepare-sekali-eksekusi-berkali menjadikan penyimpanan massal tetap efisien. Setelah data tersimpan dengan aman, pertanyaan berikutnya adalah bagaimana mengambilnya kembali dalam bentuk yang paling nyaman diproses — array, object, sampai class buatan sendiri — yang akan kita bahas pada chapter Fetching Results
.