Prepared statement adalah cara membangun query yang mengirim teks SQL dan datanya melalui jalur yang terpisah. Topik ini membahas dua bentuk parameter terikat, yaitu parameter bernama dan parameter posisional, serta perubahan makna query ketika teks SQL disusun langsung dari input. Bagian terakhir membahas mekanisme persiapan yang bekerja di belakang prepared statement. Setelah topik ini dipelajari, parameter terikat menjadi satu-satunya pola untuk menulis query yang datanya berasal dari luar.
Dua bentuk parameter terikat#
Prepared statement dibuat dengan metode prepare() yang menerima templat SQL berisi tanda tempat (placeholder) sebagai ganti nilai nyata. Parameter bernama ditulis dengan awalan titik dua, misalnya :tingkat, sehingga terbaca seperti nama. Parameter posisional ditulis dengan tanda tanya dan diisi menurut urutannya. Nilai keduanya dikirim melalui array yang diberikan kepada metode execute(), yaitu array asosiatif untuk bentuk bernama dan array berurutan untuk bentuk posisional. Karena nilai tidak pernah menjadi bagian teks SQL, tanda kutip maupun karakter lain di dalam nilai diperlakukan sebagai data murni.
Skrip berikut membuat tabel anggota dengan empat baris, lalu menyelesaikan dua pencarian yang maknanya sama dengan dua bentuk parameter berbeda. Bagian akhir skrip menjalankan query yang sama dua kali, sekali dengan teks SQL yang disusun dari input dan sekali dengan parameter terikat, agar perbedaannya terlihat berdampingan.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec('CREATE TABLE anggota (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, tingkat TEXT NOT NULL)');
$pdo->exec("INSERT INTO anggota (nama, tingkat) VALUES
('Ali', 'pelajar'),
('Budi', 'pelajar'),
('Citra', 'umum'),
('Dewi', 'umum')");
$stmt = $pdo->prepare('SELECT nama FROM anggota WHERE tingkat = :tingkat');
$stmt->execute([':tingkat' => 'pelajar']);
echo 'Bernama: ', implode(', ', $stmt->fetchAll(PDO::FETCH_COLUMN)), "\n";
$stmt = $pdo->prepare('SELECT nama FROM anggota WHERE nama = ?');
$stmt->execute(['Budi']);
echo 'Posisional: ', $stmt->fetchColumn(), "\n";
$input = "' OR '1'='1";
$queryLangsung = $pdo->query("SELECT COUNT(*) FROM anggota WHERE nama = '" . $input . "'");
echo 'Tanpa persiapan: ', $queryLangsung->fetchColumn(), ' baris', "\n";
$stmt = $pdo->prepare('SELECT COUNT(*) FROM anggota WHERE nama = :nama');
$stmt->execute([':nama' => $input]);
echo 'Dengan persiapan: ', $stmt->fetchColumn(), ' baris', "\n";Bernama: Ali, Budi
Posisional: Budi
Tanpa persiapan: 4 baris
Dengan persiapan: 0 barisDua baris pertama keluaran menunjukkan bahwa parameter bernama dan posisional menghasilkan jawaban yang sama. Metode fetchAll() dengan konstanta PDO::FETCH_COLUMN mengembalikan kolom pertama dari setiap baris, sedangkan fetchColumn() mengembalikan satu nilai saja. Pemilihan bentuk parameter menjadi soal keterbacaan, yaitu nama yang jelas untuk query dengan banyak nilai dan tanda tanya untuk query pendek.
Input yang menyusun query mengubah maknanya#
Dua baris terakhir keluaran di atas memperlihatkan bahaya penyusunan teks SQL dari input. Input yang dipakai adalah "' OR '1'='1, dan setelah digabungkan ke dalam teks SQL, templatnya menjadi WHERE nama = '' OR '1'='1'. Syarat itu selalu benar, sehingga query menghitung empat baris, yaitu seluruh tabel, padahal maksud aslinya adalah mencari nama yang persis sama. Input yang sama dikirim sebagai parameter pada query berikutnya dan hasilnya nol baris, karena nilai tersebut diperlakukan sebagai satu string utuh yang tidak menyamai nama anggota mana pun.
Perbedaan hasil kedua query itu jauh lebih penting daripada sekadar jumlah baris. Apabila input berasal dari pengguna web, penggabungan string memberi orang luar kendali atas logika query, mulai dari membaca data yang seharusnya tersembunyi hingga mengubah atau menghapus isi tabel. Penyalahgunaan celah ini dikenal sebagai SQL injection. Parameter terikat menutup celah itu secara struktural karena data tidak pernah diurai sebagai SQL, tanpa bergantung pada pembersihan karakter satu per satu.
Mekanisme persiapan dan emulasi#
Metode prepare() menyerahkan templat query untuk diproses, lalu mengembalikan objek PDOStatement yang menyimpan hasil persiapannya. Templat diparsing satu kali, sedangkan execute() mengirim nilai parameternya pada jalur data yang terpisah, sehingga nilai apa pun diperlakukan sebagai data murni. Pengutipan dan pelolosan (escaping) dikerjakan oleh PDO, bukan oleh kode pemanggilnya, dan pekerjaan yang sama tidak perlu diulang untuk setiap nilai.
PDO menyediakan opsi PDO::ATTR_EMULATE_PREPARES untuk memilih cara persiapan dijalankan. Bila emulasi aktif, PDO menggantikan parameter di sisi klien menggunakan pengutipan yang aman milik driver, sedangkan bila emulasi dinonaktifkan, driver meneruskan templatnya ke server basis data untuk diparsing sungguhan. Driver SQLite PDO selalu menyiapkan query di sisi klien karena SQLite tidak menyediakan protokol persiapan tersendiri, sedangkan driver MySQL dapat memilih salah satu dari keduanya. Kedua pilihan menjamin hal yang sama, yaitu nilai parameter tidak pernah diurai sebagai SQL.
Nilai parameter juga dapat dikirim satu per satu melalui bindValue() sebelum execute() dipanggil tanpa argumen. Selain itu, bindParam() mengikat variabel PHP sehingga nilainya dibaca pada saat eksekusi. Untuk kebutuhan sehari-hari, array pada execute() sudah mencukupi karena seluruh nilai terlihat dalam satu baris kode.
Kesalahan umum#
- Menyusun query dari input dengan penggabungan string. Kesalahan ini sering lolos pengujian karena input ujinya bersih, tetapi makna query berubah begitu pengguna nyata mengirim tanda kutip. Bangun templatnya dengan
prepare()dan kirim nilainya padaexecute(), tanpa kecuali untuk nilai yang berasal dari luar skrip. - Mengutip ulang parameter pada templat. Tulisan
WHERE nama = ':nama'membuat tanda kutip menjadi bagian templat, sehingga nilainya terbungkus dua lapis dan pencarian gagal. Parameter ditulis tanpa tanda kutip karena pengutipannya dikerjakan PDO. - Mencampur parameter bernama dan posisional dalam satu templat. PDO menolak templat yang memuat
:namasekaligus?, sehingga query gagal dipersiapkan. Pilih satu bentuk untuk satu templat. - Mengirim jumlah nilai yang tidak cocok dengan parameternya. Templat dengan dua tanda tempat menuntut dua elemen pada array
execute(), dan ketidakcocokannya melemparPDOExceptionberisi pesan jumlah variabel yang tidak sesuai. Hitung tanda tempat pada templat setiap kali argumenexecute()diubah, terutama setelah kolom baru ditambahkan ke daftarSELECT.
Ringkasan#
prepare()menerima templat SQL dengan parameter bernama (:nama) atau posisional (?), danexecute()mengirim nilainya sebagai array.- Query yang menyusun teks SQL dari input berubah maknanya mengikuti isi input, sedangkan parameter terikat menjamin input selalu diperlakukan sebagai data.
- Persiapan dapat diemulasi di sisi klien atau dikerjakan server menurut opsi
PDO::ATTR_EMULATE_PREPARESdan dukungan driver, dengan jaminan keamanan yang sama. - Array pada
execute()mencakup kebutuhan umum, sedangkanbindValue()danbindParam()menyediakan pengiriman nilai yang lebih eksplisit.
Referensi#
- Prepared statements and stored procedures — Manual PHP — Menjelaskan manfaat prepared statement dan cara kerja parameternya.
- PDO::prepare — Manual PHP — Mendokumentasikan templat SQL dan aturan parameter bernama maupun posisional.
- PDOStatement::execute — Manual PHP — Menjelaskan pengiriman nilai parameter melalui array maupun binding.