Aplikasi nyata hampir selalu mencampur SQL milik program dengan data dari luar: kata kunci pencarian, nomor halaman, isi formulir. Merangkai keduanya menjadi satu string terasa mudah, tetapi cara itu membuka celah keamanan serius bernama SQL injection sekaligus membuang performa. Prepared statements adalah solusi PDO untuk keduanya: struktur SQL dan data dikirim terpisah, sehingga database tidak pernah mencampuradukkan perintah dengan isi data. Pada chapter ini kita pelajari mekanisme prepare() dan execute(), dua gaya placeholder, eksekusi berulang, dan kesalahan-kesalahan umumnya.

Memisahkan Struktur dan Data #

Prepared statement bekerja dua tahap. Pertama, prepare() mengirim template SQL berisi placeholder ke database — database menerjemahkan dan menyusun rencana eksekusinya sekali di awal. Kedua, execute() mengirim nilai untuk para placeholder; nilai-nilai tersebut dipakai apa adanya sebagai data, tidak pernah diperiksa ulang sebagai perintah SQL. PDO menyediakan dua gaya placeholder — posisi dengan tanda ? dan bernama dengan awalan :. Mari mulai dari placeholder posisi:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, harga INTEGER)");
$pdo->exec("INSERT INTO produk (nama, harga) VALUES
    ('Mouse Wireless', 85000),
    ('Keyboard Mekanik', 450000)");

// Template SQL disiapkan dengan placeholder posisi (?)
$stmt = $pdo->prepare("SELECT nama, harga FROM produk WHERE harga <= ?");
$stmt->execute([100000]);

foreach ($stmt as $row) {
    echo "{$row['nama']} - Rp{$row['harga']}\n";
}

Output yang Dihasilkan:

text
Mouse Wireless - Rp85000

Telaah baris per baris:

  1. prepare("... harga <= ?"): database menerima template dengan satu placeholder posisi dan menyusun rencana eksekusinya; prepare() mengembalikan objek PDOStatement bernama $stmt.
  2. execute([100000]): nilai 100000 dikirim untuk mengisi placeholder pertama (dan satu-satunya). Untuk placeholder posisi, urutan nilai di array harus mengikuti urutan tanda ?.
  3. foreach ($stmt as $row): seperti hasil query(), objek PDOStatement bisa langsung diiterasi untuk membaca baris-baris hasilnya.

Placeholder Bernama #

Ketika query punya banyak parameter, placeholder bernama membuat kode lebih terbaca: setiap placeholder diberi nama, dan array execute() berpasangan kunci-nilai yang eksplisit:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, harga INTEGER)");
$pdo->exec("INSERT INTO produk (nama, harga) VALUES
    ('Mouse Wireless', 85000),
    ('Keyboard Mekanik', 450000)");

$stmt = $pdo->prepare(
    "SELECT nama, harga FROM produk
     WHERE harga <= :hargaMaks AND nama LIKE :polaNama"
);
$stmt->execute(["hargaMaks" => 100000, "polaNama" => "M%"]);

foreach ($stmt as $row) {
    echo "{$row['nama']} - Rp{$row['harga']}\n";
}

Output yang Dihasilkan:

text
Mouse Wireless - Rp85000

Placeholder :hargaMaks dan :polaNama diisi lewat array asosiatif — kuncinya persis nama placeholder tanpa titik dua ("hargaMaks", bukan ":hargaMaks"). Nilai "M%" untuk LIKE tetap berupa data biasa: database mencocokkan polanya tanpa mengeksekusi isinya sebagai SQL. Pilih satu gaya per statement; PDO tidak mengizinkan tanda ? dan placeholder bernama dicampur dalam satu template yang sama.

Eksekusi Berulang dengan Data Berbeda #

Karena template cukup disiapkan sekali, prepared statement sangat efisien untuk pekerjaan berulang — memasukkan banyak baris, misalnya:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE tugas (id INTEGER PRIMARY KEY, judul TEXT)");

// Template disiapkan SEKALI di luar loop
$stmt = $pdo->prepare("INSERT INTO tugas (judul) VALUES (?)");

foreach (["Belajar PDO", "Belajar SQL", "Belajar Transaksi"] as $judul) {
    $stmt->execute([$judul]); // dieksekusi berulang dengan data berbeda
}

foreach ($pdo->query("SELECT id, judul FROM tugas ORDER BY id") as $row) {
    echo "[{$row['id']}] {$row['judul']}\n";
}

Output yang Dihasilkan:

text
[1] Belajar PDO
[2] Belajar SQL
[3] Belajar Transaksi

Pemanggilan prepare() sengaja diletakkan di luar loop: database menerima dan menerjemahkan template satu kali, lalu execute() berjalan tiga kali dengan isi berbeda. Bila prepare() berada di dalam loop, pekerjaan penerjemahan diulang setiap iterasi dan keuntungan performanya hilang. Untuk ratusan atau ribuan baris, selisihnya nyata terasa; pada database server seperti MySQL dan PostgreSQL, server juga dapat menyimpan rencana eksekusi untuk dipakai ulang antar-eksekusi.

Melindungi dari SQL Injection #

SQL injection terjadi ketika input pengguna ikut diterjemahkan sebagai bagian dari perintah SQL. Perhatikan simulasi berikut — bagian pertama menunjukkan pola yang rawan, bagian kedua menunjukkan versi amannya:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE pengguna (nama TEXT, sandi TEXT)");
$pdo->exec("INSERT INTO pengguna (nama, sandi) VALUES ('budi', 'rahasia123')");

// Simulasi input berbahaya dari luar program
$inputNama = "budi' OR '1'='1";

// RAWAN: nilai input dirangkai langsung ke dalam string SQL
$sqlRawan = "SELECT nama FROM pengguna WHERE nama = '$inputNama'";
foreach ($pdo->query($sqlRawan) as $row) {
    echo "Versi rawan : login berhasil sebagai {$row['nama']}\n";
}

// AMAN: placeholder memaksa input diperlakukan sebagai data murni
$stmt = $pdo->prepare("SELECT nama FROM pengguna WHERE nama = ?");
$stmt->execute([$inputNama]);

if ($stmt->fetch() === false) {
    echo "Versi aman : login ditolak untuk input yang sama\n";
}

Output yang Dihasilkan:

text
Versi rawan : login berhasil sebagai budi
Versi aman : login ditolak untuk input yang sama

Pada versi rawan, tanda kutip pada $inputNama menutup string di SQL dan menyisipkan kondisi OR '1'='1 yang selalu benar — query mengembalikan baris yang bukan milik input tersebut, dan “login” pun berhasil. Pada versi aman, nilai yang sama dikirim lewat execute(); database mencari baris yang kolom nama-nya persis berisi teks aneh itu, tidak menemukannya, dan mengembalikan false. Kedua versi sengaja ditampilkan berdampingan sebagai konteks edukasi — di kode nyata, hanya tulis pola versi aman.

quote() Bukan Alat Utama #

PDO menyediakan metode quote() yang meng-escape string agar aman dirangkai ke SQL. Hasilnya memang valid, tetapi pendekatan ini bukan alat utama:

php
<?php
$pdo = new PDO("sqlite::memory:");

$teks = "O'Reilly";
echo "Asli    : $teks\n";
echo "quote() : " . $pdo->quote($teks) . "\n";

Output yang Dihasilkan:

text
Asli    : O'Reilly
quote() : 'O''Reilly'

Tanda kutip tunggal pada nilai digandakan oleh quote() sehingga string hasil rangkaian tidak rusak. Masalahnya, keamanan bergantung pada kedisiplinan memanggil quote() di setiap titik penyusunan SQL — satu tempat terlewat, celah terbuka. Prepared statement menutup kategori kesalahan ini sepenuhnya karena tidak ada perangkaian string sama sekali. Gunakan quote() hanya untuk kasus sungguhan yang tidak bisa memakai placeholder (misalnya menyusun query untuk dijalankan di alat lain).

Kesalahan Umum #

Kesalahan paling umum berputar di sekitar jumlah dan padanan placeholder. Nilai yang lebih banyak dari jumlah placeholder langsung memicu PDOException, sedangkan nilai yang kurang lebih berbahaya karena tidak selalu error:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengguna (nama TEXT NOT NULL)");

// Terlalu banyak nilai: satu placeholder menerima dua nilai
$stmt = $pdo->prepare("INSERT INTO pengguna (nama) VALUES (?)");
try {
    $stmt->execute(["Budi", "Siti"]);
} catch (PDOException $e) {
    echo "Kesalahan: " . $e->getMessage() . "\n";
}

// Terlalu sedikit nilai: placeholder yang kosong dikirim sebagai NULL
$stmt = $pdo->prepare("INSERT INTO pengguna (nama) VALUES (?)");
try {
    $stmt->execute([]);
} catch (PDOException $e) {
    echo "Kesalahan: " . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
Kesalahan: SQLSTATE[HY000]: General error: 25 column index out of range
Kesalahan: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: pengguna.nama

Eksekusi pertama melempar column index out of range karena satu placeholder menerima dua nilai. Eksekusi kedua justru lebih licik: placeholder yang tidak diisi dikirim sebagai NULL, dan baru ketahuan saat menabrak batasan NOT NULL pada kolom — tanpa batasan itu, baris berisi NULL akan tersimpan tanpa suara. Daftar jebakan lain yang sebaiknya dihafal:

  • Kunci array execute() tidak cocok dengan nama placeholder (misalnya "namanya" untuk :nama) — PDO menganggap ada nilai lebih dan melempar error yang sama seperti kelebihan nilai.
  • Merangkai variabel langsung ke string SQL ("... WHERE nama = '$input'") — pola injection yang dibahas di atas, tanpa peduli seberapa “terpercaya” sumber datanya.
  • Menaruh prepare() di dalam loop — sah secara sintaks, tetapi membuang keunggulan performa prepared statements.

Ringkasan #

Prepared statements memisahkan struktur SQL dari data: prepare() menyimpan template dengan placeholder, execute() mengirim nilainya terpisah. Placeholder posisi (?) mengikuti urutan array, placeholder bernama (:nama) memakai kunci yang eksplisit, dan keduanya menjamin input diperlakukan sebagai data murni — inilah pertahanan utama menghadapi SQL injection. Eksekusi berulang dengan satu template juga memberi keuntungan performa yang nyata. Hingga di sini kita selalu mengirim seluruh nilai lewat array execute(); PDO masih menyediakan cara yang lebih terkontrol untuk mengikat nilai satu per satu dengan tipe yang eksplisit — itu topik chapter Parameter Binding .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.php

Unduh berkas
php
<?php
/**
 * contoh.php - rangkuman Chapter Prepared Statements.
 *
 * CRUD ringkas di atas SQLite memori memakai prepared statements:
 * INSERT dengan placeholder bernama, eksekusi berulang, perlindungan
 * dari SQL injection, dan penanganan ketidakcocokan jumlah parameter.
 */

$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, nama TEXT, email TEXT)");

// prepare() sekali, execute() berulang dengan data berbeda
$stmt = $pdo->prepare("INSERT INTO pengguna (nama, email) VALUES (:nama, :email)");
foreach ([
    ["nama" => "Budi", "email" => "[email protected]"],
    ["nama" => "Siti", "email" => "[email protected]"],
    ["nama" => "Rina", "email" => "[email protected]"],
] as $penggunaBaru) {
    $stmt->execute($penggunaBaru);
}

// SELECT dengan placeholder bernama
$stmt = $pdo->prepare("SELECT id, nama, email FROM pengguna WHERE email LIKE :pola");
$stmt->execute(["pola" => "s%@example.com"]);
foreach ($stmt as $row) {
    echo "Ditemukan: {$row['nama']} <{$row['email']}>\n";
}

// Placeholder selalu memperlakukan input sebagai data, bukan perintah SQL
$stmt = $pdo->prepare("SELECT nama FROM pengguna WHERE nama = ?");
$stmt->execute(["Budi' OR '1'='1"]);
echo "Input berbahaya ditolak: " . var_export($stmt->fetch(), true) . "\n";

// Jumlah nilai yang melebihi jumlah placeholder memicu PDOException
try {
    $stmt = $pdo->prepare("SELECT nama FROM pengguna WHERE nama = ?");
    $stmt->execute(["Budi", "Siti"]);
} catch (PDOException $e) {
    echo "Kesalahan: " . $e->getMessage() . "\n";
}