Field teks selalu ikut terkirim walau kosong, sehingga pola $post['nama'] ?? '' dari Chapter Membaca Form Input cukup untuk hampir semua kasus. Checkbox, radio, dan select hidup dengan aturan berbeda: field yang tidak dipilih bisa tidak terkirim sama sekali, beberapa field bisa berbagi satu nama, dan nilai pilihan sejatinya cuma saran dari browser — bukan kebenaran. Bug klasik lahir dari sini: pesanan diproses tanpa persetujuan karena kunci setuju absen dianggap string kosong. Pada chapter ini kita petakan perilaku tiap widget, lalu membangun pertahanan yang menyertainya: validasi terhadap daftar pilihan yang diizinkan.

Checkbox yang Tidak Dicentang Tidak Terkirim #

Inilah perilaku paling penting di seluruh chapter: browser hanya mengirim checkbox yang dicentang. Checkbox yang dibiarkan kosong tidak mengirim nilai apa pun — bukan string kosong, bukan "off" — kuncinya benar-benar absen dari kiriman:

html
<form action="/daftar.php" method="post">
    <label>
        <input type="checkbox" name="setuju" value="1">
        Saya menyetujui ketentuan layanan
    </label>
    <button type="submit">Daftar</button>
</form>

Konsekuensinya, pemeriksaan yang benar bukan membandingkan nilai, melainkan memeriksa keberadaan kunci:

php
<?php
function bacaPersetujuan(array $post): string
{
    if (isset($post['setuju'])) {
        return 'Persetujuan diberikan.';
    }

    return 'Persetujuan belum diberikan.';
}

echo bacaPersetujuan(['setuju' => '1']), "\n";
echo bacaPersetujuan([]), "\n";

Output yang Dihasilkan:

text
Persetujuan diberikan.
Persetujuan belum diberikan.

Saat checkbox dicentang, $_POST['setuju'] berisi '1' — nilai dari atribut value yang kita tulis eksplisit. Saat tidak dicentang, kunci tersebut tidak ada dan isset() mengembalikan false. Atribut value="1" sendiri adalah konvensi yang membuat maksud kode jelas; tanpa atribut ini browser mengirim nilai bawaan "on". Pola isset() inilah cara benar bertanya “apakah kotak ini dicentang?” — membandingkan $post['setuju'] === '1' juga bekerja untuk kasus ini, tetapi isset() menyampaikan maksudnya paling langsung.

Grup Checkbox dengan Notasi hobi[] #

Satu checkbox menyimpan satu keputusan boolean. Untuk pertanyaan seperti “pilih semua hobi yang Anda sukai”, kita butuh beberapa kotak sekaligus — dan notasi kurung siku pada name mengumpulkannya menjadi satu array:

html
<form action="/hobi.php" method="post">
    <label><input type="checkbox" name="hobi[]" value="membaca"> Membaca</label>
    <label><input type="checkbox" name="hobi[]" value="bersepeda"> Bersepeda</label>
    <label><input type="checkbox" name="hobi[]" value="memasak"> Memasak</label>
    <button type="submit">Simpan</button>
</form>

Bila kotak pertama dan ketiga dicentang, $_POST['hobi'] berisi ['membaca', 'memasak']; bila tidak ada satu pun yang dicentang, kunci hobi absen — persis seperti kasus satu checkbox. Pembacaannya mengikuti pola helper dari chapter sebelumnya:

php
<?php
function bacaHobi(array $post): array
{
    $hobi = $post['hobi'] ?? [];

    if (!is_array($hobi)) {
        return [];
    }

    return $hobi;
}

print_r(bacaHobi(['hobi' => ['membaca', 'bersepeda']]));
print_r(bacaHobi([]));

Output yang Dihasilkan:

text
Array
(
    [0] => membaca
    [1] => bersepeda
)
Array
(
)

Helper bacaHobi() selalu mengembalikan array — nilai bawaan [] untuk kunci yang absen, dan pengaman is_array() untuk kiriman palsu yang mengirim hobi sebagai string. Pengguna foreach dan count() di kode berikutnya tidak perlu berpikir dua kali.

Validasi Terhadap Daftar yang Diizinkan #

Ada satu kesalahpahaman yang harus runtuh di sini: nilai value pada HTML hanyalah saran. Pengirim jahat bisa memodifikasi form lewat developer tools dan mengirim value apa pun — termasuk meretas pada contoh berikut. Karena itu setiap pilihan harus diperiksa terhadap allowlist — daftar nilai yang memang kita sediakan:

php
<?php
$hobiTersedia = ['membaca', 'ngoding', 'bersepeda', 'memasak'];

$kiriman = ['membaca', 'meretas', 'ngoding'];

$sah = array_values(array_filter($kiriman, fn ($h) => in_array($h, $hobiTersedia, true)));
$ditolak = array_values(array_diff($kiriman, $hobiTersedia));

echo 'Yang diterima: ' . implode(', ', $sah) . "\n";
echo 'Yang ditolak : ' . implode(', ', $ditolak) . "\n";

Output yang Dihasilkan:

text
Yang diterima: membaca, ngoding
Yang ditolak : meretas

array_filter() menyaring kiriman dengan syarat keanggotaan di $hobiTersedia, sedangkan array_diff() mengambil sisanya untuk bisa dilaporkan. Nilai palsu meretas gagal masuk allowlist dan berakhir di daftar tolakan. Satu detail kecil yang menentukan: parameter ketiga true pada in_array() mengaktifkan perbandingan strict. Tanpa parameter tersebut, PHP memakai perbandingan longgar yang bisa menipu:

php
<?php
var_dump(in_array('1', [1, 2, 3]));
var_dump(in_array('1', [1, 2, 3], true));

Output yang Dihasilkan:

text
bool(true)
bool(false)

String '1' dianggap sama dengan integer 1 oleh perbandingan longgar — ingat bahwa semua nilai form tiba sebagai string, sehingga kecocokan tak disengaja seperti ini sepenuhnya mungkin terjadi. Mode strict membandingkan nilai dan tipe, dan itulah mode yang selalu layak dipakai untuk allowlist.

Elemen select menyajikan daftar pilihan dalam bentuk dropdown. Yang terkirim ke server adalah atribut value dari option yang terpilih — bukan teks labelnya:

html
<form action="/kirim.php" method="post">
    <label for="kota">Kota pengiriman</label>
    <select id="kota" name="kota">
        <option value="">— Pilih kota —</option>
        <option value="jakarta">Jakarta</option>
        <option value="bandung">Bandung</option>
        <option value="surabaya">Surabaya</option>
    </select>
    <button type="submit">Kirim</button>
</form>

option pertama sengaja diberi value kosong sebagai penanda “belum memilih”. Tanpa atribut selected pada option mana pun, option pertama memang menjadi terpilih secara bawaan — sehingga pola ini membuat pilihan kosong terlihat jelas bagi pengguna. Validasinya menggabungkan pemeriksaan wajib-isi dengan allowlist:

php
<?php
function validasiKota(array $post): string
{
    $kotaTersedia = ['jakarta', 'bandung', 'surabaya'];

    $kota = $post['kota'] ?? '';

    if ($kota === '') {
        return 'ERROR: kota belum dipilih.';
    }

    if (!in_array($kota, $kotaTersedia, true)) {
        return "ERROR: kota '$kota' tidak dikenal.";
    }

    return "Kota pengiriman: $kota";
}

echo validasiKota([]), "\n";
echo validasiKota(['kota' => 'bandung']), "\n";
echo validasiKota(['kota' => 'london']), "\n";

Output yang Dihasilkan:

text
ERROR: kota belum dipilih.
Kota pengiriman: bandung
ERROR: kota 'london' tidak dikenal.

Tiga cabangnya tegas: kunci absen atau string kosong berarti belum memilih, nilai di luar allowlist ditolak sebagai nilai palsu, dan hanya anggota allowlist yang diterima. Bandingkan 'london' dan 'bandung' — keduanya string sama-sama valid secara tipe, tetapi hanya yang kedua yang pernah kita sediakan di form.

Multi-Select dengan Atribut multiple #

Menambahkan atribut multiple mengubah select menjadi kotak pilihan ganda — pengguna menahan Ctrl/Cmd (atau geser di perangkat sentuh) untuk memilih beberapa sekaligus. Nama field-nya wajib memakai notasi [] agar semua pilihan terkirim:

html
<label for="minuman">Pilih minuman (tahan Ctrl/Cmd untuk beberapa)</label>
<select id="minuman" name="minuman[]" multiple size="4">
    <option value="teh">Teh</option>
    <option value="kopi">Kopi</option>
    <option value="susu">Susu</option>
    <option value="air">Air mineral</option>
</select>

Multi-select berperilaku seperti grup checkbox dari sisi PHP: terkirim sebagai array, dan bila tidak ada satu pun yang terpilih, kuncinya absen. Pola pembacaan sekaligus validasinya pun pernikahan dari dua pola sebelumnya:

php
<?php
$minumanTersedia = ['teh', 'kopi', 'susu', 'air'];

function bacaMinuman(array $post, array $tersedia): array
{
    $kiriman = $post['minuman'] ?? [];

    if (!is_array($kiriman)) {
        return [];
    }

    return array_values(array_filter($kiriman, fn ($m) => in_array($m, $tersedia, true)));
}

print_r(bacaMinuman(['minuman' => ['kopi', 'air', 'es krim']], $minumanTersedia));
print_r(bacaMinuman(['minuman' => []], $minumanTersedia));

Output yang Dihasilkan:

text
Array
(
    [0] => kopi
    [1] => air
)
Array
(
)

Kiriman ['kopi', 'air', 'es krim'] disaring menjadi ['kopi', 'air'] — pilihan palsu es krim dibuang diam-diam oleh allowlist. Pada praktik nyata, nilai yang ditolak sebaiknya juga dilaporkan sebagai pesan error agar pengguna tahu pilihannya tidak dikenali, bukan sekadar hilang; pola pelaporannya mengikuti fungsi validasiPendaftaran() dari Chapter Validasi Server-Side .

Grup Radio Button #

Radio button memakai satu name untuk seluruh grup sehingga pengguna hanya bisa memilih satu — dan yang terkirim selalu tepat satu nilai dari radio yang terpilih. Atribut checked menandai pilihan bawaan:

html
<label><input type="radio" name="paket" value="reguler" checked> Reguler</label>
<label><input type="radio" name="paket" value="kilat"> Kilat</label>
<label><input type="radio" name="paket" value="same-day"> Same day</label>

Bedanya dengan select: bila tidak ada satu pun radio yang terpilih (misalnya kita lupa menandai checked), kunci paket absen dari kiriman — perilaku checkbox, bukan select. Pembacaan yang tahan banting memberi nilai bawaan sekaligus memvalidasi allowlist:

php
<?php
function bacaPaket(array $post): string
{
    $paketTersedia = ['reguler', 'kilat', 'same-day'];

    $paket = $post['paket'] ?? 'reguler';

    if (!in_array($paket, $paketTersedia, true)) {
        return 'reguler';
    }

    return $paket;
}

echo bacaPaket(['paket' => 'kilat']), "\n";
echo bacaPaket([]), "\n";
echo bacaPaket(['paket' => 'kilat-jet']), "\n";

Output yang Dihasilkan:

text
kilat
reguler
reguler

Kiriman 'kilat' diterima apa adanya. Kiriman tanpa kunci jatuh ke nilai bawaan 'reguler'. Kiriman palsu 'kilat-jet' — yang tidak pernah kita sediakan di HTML — juga jatuh ke 'reguler'. Strategi “ganti dengan nilai bawaan” cocok untuk pilihan yang dampaknya ringan seperti paket pengiriman; untuk keputusan yang mengubah hak akses atau harga besar, keputusan yang tepat adalah menolak seluruh kiriman seperti pada pola error di chapter validasi.

Berkas contoh.php pada bagian bawah halaman merangkai seluruh widget — persetujuan checkbox, paket radio, kota select, dan minuman multi-select — menjadi satu form pemesanan lengkap beserta validasinya.

Ringkasan #

Checkbox yang tidak dicentang tidak terkirim, grup checkbox dan multi-select terkirim sebagai array lewat notasi [], radio dan select terkirim sebagai satu nilai, dan nilai value selalu hanya saran yang harus divalidasi terhadap allowlist dengan in_array() mode strict. Pola isset() menjawab pertanyaan “apakah dipilih?”, ?? [] menjawab “apa bawaan kalau tidak ada?”, dan allowlist menjawab “apakah nilai ini pernah kita sediakan?”. Dengan semua jenis input kini terpetakan, perjalanan kita lanjut ke input yang paling istimewa — berkas yang diunggah pengguna — pada Chapter Menangani File Upload .

Referensi #

Berkas Sumber

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

contoh.php

Unduh berkas
php
<?php
// contoh.php — memproses form pesanan dengan checkbox, radio, select, dan multi-select.
// CLI tidak menerima request, jadi kiriman form disimulasikan sebagai array.

/**
 * Memvalidasi kiriman form pesanan.
 * Mengembalikan daftar pesan error; daftar kosong berarti lolos.
 */
function validasiPesanan(array $post): array
{
    $errors = [];

    $paketTersedia = ['reguler', 'kilat', 'same-day'];
    $kotaTersedia = ['jakarta', 'bandung', 'surabaya'];
    $minumanTersedia = ['teh', 'kopi', 'susu', 'air'];

    // Checkbox persetujuan: kunci absen berarti tidak dicentang
    if (!isset($post['setuju'])) {
        $errors[] = 'Anda belum menyetujui ketentuan.';
    }

    // Radio paket: satu nilai, wajib anggota allowlist
    $paket = $post['paket'] ?? '';
    if (!in_array($paket, $paketTersedia, true)) {
        $errors[] = 'Paket pengiriman tidak sah.';
    }

    // Select kota: nilai kosong berarti belum dipilih
    $kota = $post['kota'] ?? '';
    if (!in_array($kota, $kotaTersedia, true)) {
        $errors[] = 'Kota pengiriman tidak sah.';
    }

    // Multi-select minuman: array, tiap elemen diperiksa ke allowlist
    $minuman = $post['minuman'] ?? [];
    if (is_array($minuman)) {
        foreach ($minuman as $satu) {
            if (!in_array($satu, $minumanTersedia, true)) {
                $errors[] = "Minuman tidak dikenal: $satu";
            }
        }
    }

    return $errors;
}

// Simulasi kiriman form yang lengkap dan sah
$kirimanSah = [
    'setuju' => '1',
    'paket' => 'kilat',
    'kota' => 'bandung',
    'minuman' => ['kopi', 'air'],
];

// Simulasi kiriman form yang bermasalah
$kirimanBuruk = [
    'paket' => 'kilat-jet',
    'kota' => 'london',
    'minuman' => ['es krim'],
];

foreach ([$kirimanSah, $kirimanBuruk] as $kiriman) {
    $errors = validasiPesanan($kiriman);

    if ($errors === []) {
        echo "TERIMA: pesanan diproses (paket {$kiriman['paket']} ke {$kiriman['kota']})\n";
    } else {
        echo 'TOLAK: ' . count($errors) . " masalah ditemukan\n";
        foreach ($errors as $pesan) {
            echo "  - $pesan\n";
        }
    }
}