Saya lagi bangun fitur listing di sebuah multi-listing site, dan ada satu query yang harusnya sepele: ambil sekumpulan baris dari tabel custom berdasarkan daftar ID. Klasik IN (...). Saya punya array ID, saya lempar ke $wpdb->prepare(), lalu panggil get_col(). Hasilnya? Array kosong. Bukan error, bukan warning, bukan exception. Cuma kosong. Padahal saya tahu persis barisnya ada di database, karena saya baru saja SELECT manual lewat klien MySQL dan datanya nongol semua.
Ini kira-kira kode yang saya tulis pertama kali:
$ids = array( 12, 47, 103 );
$sql = $wpdb->prepare(
"SELECT id FROM $t WHERE pid IN (%s)",
$ids
);
$rows = $wpdb->get_col( $sql );
// $rows => array() kosong, padahal pid 12, 47, 103 jelas adaSaya sempat coba beberapa variasi: ganti %s jadi %d, bungkus $ids jadi argumen tunggal, sampai bongkar pasang tanda kurung. Tetap kosong. Yang bikin frustrasi, tidak ada satu pun sinyal merah. PHP diam, WordPress diam, MySQL diam. Query jalan, cuma nggak ada yang match.
Kenapa ini terjadi
Akar masalahnya: $wpdb->prepare() tidak tahu cara memperlakukan array untuk satu placeholder tunggal. Asumsi saya salah dari awal. Saya pikir kalau saya kasih %s dan sebuah array, dia bakal pintar lalu membentangkannya jadi 12, 47, 103. Tidak begitu cara kerjanya.
Yang sebenarnya terjadi, prepare() melihat satu placeholder %s dan satu array. Dia coba memasukkan array itu ke posisi placeholder, dan hasil akhirnya placeholder-nya jadi malformed — bukan daftar nilai yang rapi, melainkan sesuatu yang tidak pernah cocok dengan pid mana pun di tabel. Query yang dihasilkan secara teknis valid sebagai SQL, jadi MySQL tidak protes. Dia cuma mengembalikan nol baris karena memang tidak ada pid yang sama dengan sampah hasil substitusi tadi.
Inilah jebakan sesungguhnya: hasil kosong yang senyap. Kalau prepare() melempar error atau setidaknya warning, saya akan langsung tahu placeholder-nya bermasalah. Tapi karena dia mengembalikan string SQL yang valid dan get_col() dengan patuh mengembalikan array kosong, gejalanya nyaris tidak bisa dibedakan dari "memang datanya tidak ada". Saya sempat curiga ke koneksi database, ke nama tabel, bahkan ke cache, sebelum akhirnya sadar masalahnya ada di langkah prepare().
Pelajaran konsep yang penting: prepare() itu fungsi placeholder satu-banding-satu. Satu placeholder mengikat satu nilai skalar. Array bukan nilai skalar. Untuk klausa IN, kita harus mengakali sendiri ekspansinya.
Perbaikannya
Ada dua cara yang saya pakai, dan keduanya menghasilkan SQL yang aman.
Cara pertama, bangun daftar IN secara manual sambil meng-escape tiap nilai dengan esc_sql():
$ids = array( 12, 47, 103 );
$in = "'" . implode( "','", array_map( 'esc_sql', $ids ) ) . "'";
$sql = "SELECT id FROM $t WHERE pid IN ($in)";
$rows = $wpdb->get_col( $sql );Di sini setiap ID dilewatkan ke esc_sql() lalu disambung dengan ',', jadi hasil akhirnya berbentuk '12','47','103'. Karena tiap nilai sudah di-escape, klausa ini aman dari injeksi meskipun saya tidak melewati prepare() untuk bagian ini.
Cara kedua, dan ini yang biasanya lebih saya suka kalau ID-nya pasti integer, adalah membuat sejumlah placeholder yang jumlahnya pas dengan jumlah elemen array, lalu menyebar array sebagai argumen:
$ids = array( 12, 47, 103 );
$ph = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
// $ph => "%d,%d,%d"
$sql = $wpdb->prepare(
"SELECT id FROM $t WHERE pid IN ($ph)",
$ids
);
$rows = $wpdb->get_col( $sql );Kuncinya di array_fill( 0, count( $ids ), '%d' ): dia menghasilkan satu %d untuk tiap ID, jadi prepare() melihat tiga placeholder dan tiga nilai — pasangan satu-banding-satu yang dia memang mengerti. Array $ids disebar sebagai argumen, dan tiap elemennya mengikat ke satu placeholder. Begitu jumlah placeholder cocok dengan jumlah nilai, prepare() langsung bekerja seperti yang diharapkan dan get_col() mengembalikan ketiga baris.
Pilih %d kalau ID dijamin numerik (paling aman, sekaligus memaksa tipe), atau %s kalau bisa berupa string. Yang penting jumlah placeholder harus sama persis dengan jumlah elemen.
Pelajaran
$wpdb->prepare() tidak secara native menerima array untuk satu placeholder. Untuk klausa IN, ada dua jalan: esc_sql() + implode() untuk merangkai daftarnya sendiri, atau bangun deretan %d/%s yang jumlahnya sesuai lalu sebarkan array sebagai argumen. Jangan sekali-kali sambungkan ID mentah langsung ke SQL tanpa salah satu dari dua pengaman ini.
Dan pelajaran debugging yang lebih dalam: hasil kosong yang senyap jauh lebih licik daripada error yang berisik. Error menunjuk lokasi masalah; hasil kosong membiarkan kita menebak-nebak. Lain kali sebuah query mengembalikan nol baris padahal kamu yakin datanya ada, jangan langsung curiga ke datanya — cetak dulu SQL final yang dihasilkan prepare(). Sembilan dari sepuluh kali, jawabannya ada di string yang malformed itu.
