HTML5 memberi kita atribut required, minlength, dan pattern yang ramah terhadap pengguna — pesan kesalahan muncul instan tanpa satu pun request ke server. Nyatanya semua itu hanyalah lapisan pengalaman pengguna, bukan pertahanan. Browser bisa dimatikan JavaScript-nya, atribut bisa dihapus lewat developer tools, dan permintaan bisa dikirim langsung tanpa browser sama sekali. Pada chapter ini kita membangun pertahanan yang sesungguhnya di sisi server: aturan-aturan pemeriksaan, fungsi validasi yang mengumpulkan error ke satu array, cara menampilkannya, dan alur handler yang memvalidasi lebih dulu sebelum menyentuh data.

Validasi di Client Hanyalah Pengalaman Pengguna #

Mari kita buktikan klaim pembuka dengan eksperimen. Form berikut memakai atribut required sehingga browser menolak submit dengan nama kosong:

html
<form action="/daftar.php" method="post">
    <label for="nama">Nama lengkap</label>
    <input type="text" id="nama" name="nama" required>
    <button type="submit">Daftar</button>
</form>

Handler di sisi server tetap harus siap menerima kiriman tanpa nama, karena tidak semua pengirim adalah browser yang patuh:

php
<?php
// daftar.php — jalankan lewat built-in server: php -S 127.0.0.1:8099
header('Content-Type: text/plain; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nama = trim($_POST['nama'] ?? '');

    if ($nama === '') {
        http_response_code(422);
        echo 'TOLAK: nama wajib diisi';
        exit;
    }

    echo "TERIMA: pendaftar $nama tersimpan";
    exit;
}

echo 'Tampilkan form';
bash
curl -X POST -d "nama=" http://127.0.0.1:8099/daftar.php
curl -X POST -d "nama=Budi" http://127.0.0.1:8099/daftar.php

Kami menjalankan keduanya lewat built-in web server. Kiriman nama= dijawab TOLAK: nama wajib diisi dengan status HTTP 422, sedangkan nama=Budi dijawab TERIMA: pendaftar Budi tersimpan. Perhatikan: curl tidak pernah melihat atribut required — perintah ini mengirim body request apa adanya, persis seperti yang bisa dilakukan siapa pun dengan sedikit kemampuan teknis. Kesimpulannya bukan “jangan pakai validasi client”, melainkan: validasi client untuk kenyamanan, validasi server untuk kebenaran.

Aturan Validasi yang Paling Sering Dibutuhkan #

Hampir semua kebutuhan validasi form bisa dirangkum ke dalam tujuh keluarga aturan berikut:

AturanAlat PHPCatatan
Wajib diisitrim($x) === ''spasi saja dianggap kosong
Panjang minimum/maksimummb_strlen()menghitung karakter Unicode dengan benar
Angka bulat dalam rentangfilter_var() + FILTER_VALIDATE_INTmin_range/max_range, string '25' menjadi int 25
Angka desimalfilter_var() + FILTER_VALIDATE_FLOAThati-hati is_numeric() juga menerima notasi 1e5
Sesuai polapreg_match()bahasan Chapter Regular Expressions
Anggota daftar yang diizinkanin_array() dengan strict modebahasan Chapter Menangani Checkbox dan Select
Email atau URLfilter_var() + FILTER_VALIDATE_EMAIL/URLbahasan Chapter Validasi Email dan URL

Pilihan alat selalu mengikuti bentuk data: teks diperiksa panjangnya dengan mb_strlen(), angka divalidasi dengan filter_var() sekaligus rentangnya, dan pilihan terbatas diperiksa keanggotaannya di daftar. Dua aturan terakhir di tabel mendapat chapter khusus karena memiliki banyak sudut kepribadian.

Fungsi validasi yang Mengumpulkan Error #

Tulang punggung validasi server-side adalah satu fungsi murni: menerima seluruh input, memeriksa aturan satu per satu, dan mengembalikan array error — array kosong berarti lolos. Bentuk ini mudah diuji dan mudah bertambah:

php
<?php
function validasiPendaftaran(array $input): array
{
    $errors = [];

    $nama = trim($input['nama'] ?? '');
    if ($nama === '') {
        $errors['nama'] = 'Nama wajib diisi.';
    } elseif (mb_strlen($nama) < 3) {
        $errors['nama'] = 'Nama minimal 3 karakter.';
    }

    $umur = filter_var($input['umur'] ?? '', FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 17, 'max_range' => 99],
    ]);
    if ($umur === false) {
        $errors['umur'] = 'Umur harus berupa angka antara 17 dan 99.';
    }

    return $errors;
}

print_r(validasiPendaftaran(['nama' => 'Bu', 'umur' => '15']));
print_r(validasiPendaftaran(['nama' => 'Budi', 'umur' => '25']));

Output yang Dihasilkan:

text
Array
(
    [nama] => Nama minimal 3 karakter.
    [umur] => Umur harus berupa angka antara 17 dan 99.
)
Array
(
)

Mari kita bedah keputusan desain di balik fungsi tersebut:

  1. Kunci array = nama field: ['nama' => ..., 'umur' => ...] membuat pesan error mudah ditempel tepat di sebelah field yang bermasalah saat form ditampilkan ulang.
  2. trim($input['nama'] ?? ''): input dirapikan dulu sehingga ' ' dianggap kosong, dan kunci yang absen tidak memicu Warning — pola dari Chapter Membaca Form Input .
  3. Urutan if/elseif: aturan paling mendasar (wajib diisi) diperiksa lebih dulu; satu field hanya mengumpulkan satu pesan agar pengguna tidak diserang lima keluhan untuk satu kesalahan ketik.
  4. filter_var() dengan rentang: string '15' yang lolos format tetapi di luar rentang 17-99 menghasilkan false — format dan rentang diperiksa dalam satu panggilan.
  5. Return [] = lolos: pemanggil cukup membandingkan $errors === [] untuk memutuskan lanjut atau tidak.

Menampilkan Error Pertama atau Semua #

Array error memberi dua strategi tampilan. Menampilkan error pertama saja membuat pengguna memperbaiki satu hal dalam satu waktu; menampilkan semua sekaligus menghemat putaran submit untuk form yang panjang:

php
<?php
$errors = [
    'nama' => 'Nama minimal 3 karakter.',
    'umur' => 'Umur harus berupa angka antara 17 dan 99.',
];

$kunciPertama = array_key_first($errors);
echo 'Error pertama [' . $kunciPertama . ']: ' . $errors[$kunciPertama] . "\n";

foreach ($errors as $field => $pesan) {
    echo "- [$field] $pesan\n";
}

Output yang Dihasilkan:

text
Error pertama [nama]: Nama minimal 3 karakter.
- [nama] Nama minimal 3 karakter.
- [umur] Umur harus berupa angka antara 17 dan 99.

array_key_first() mengambil kunci pertama tanpa menggerakkan pointer internal array, lalu $errors[$kunciPertama] mengambil pesannya. Perulangan foreach mengambil jalur kedua: setiap pasangan field-pesan dicetak sebagai daftar. Kombinasi yang umum di aplikasi nyata adalah keduanya — ringkasan semua error di atas form, plus pesan singkat tepat di samping tiap field; pola render lengkapnya dibangun pada Chapter Menampilkan Validation Errors .

Menampilkan Ulang Form di Halaman yang Sama #

Saat validasi gagal, pengalaman terbaik adalah menampilkan ulang form di halaman yang sama — dengan nilai lama tetap terisi supaya pengguna tidak mengetik ulang dari awal:

html
<form action="/daftar.php" method="post">
    <label for="nama">Nama lengkap</label>
    <input type="text" id="nama" name="nama"
           value="<?= htmlspecialchars($namaLama ?? '') ?>">

    <?php if (isset($errorNama)): ?>
        <p style="color: red;"><?= htmlspecialchars($errorNama) ?></p>
    <?php endif; ?>

    <label for="umur">Umur</label>
    <input type="text" id="umur" name="umur"
           value="<?= htmlspecialchars($umurLama ?? '') ?>">

    <?php if (isset($errorUmur)): ?>
        <p style="color: red;"><?= htmlspecialchars($errorUmur) ?></p>
    <?php endif; ?>

    <button type="submit">Daftar</button>
</form>

Dua hal wajib pada pola ini. Pertama, setiap nilai lama digema lewat htmlspecialchars() — nilai tersebut berasal dari pengguna, dan menggemanya tanpa escape membuka pintu XSS; alasan mendalam di balik aturan ini dibahas pada Chapter Sanitasi vs Validasi . Kedua, jangan pernah mengisi ulang field kata sandi — nilai sandi yang gagal divalidasi tidak boleh dikirim bolak-balik ke browser.

Alur Lengkap: Validasi Dulu, Baru Proses #

Seluruh bagian sebelumnya menyusun menjadi satu alur baku. Kode pemrosesan — menyimpan ke database, mengirim email — hanya boleh berjalan setelah seluruh aturan lolos:

Diagram di atas membaca: request POST masuk, input dibaca dan divalidasi, lalu cabang error menampilkan ulang form di halaman yang sama sedangkan cabang lolos melanjutkan ke pemrosesan. Wujud kodenya dalam bentuk yang bisa diuji dari CLI:

php
<?php
function prosesAtauTolak(array $post): string
{
    // Tahap 1: validasi dulu, sebelum menyentuh data apa pun
    $nama = trim($post['nama'] ?? '');
    if ($nama === '') {
        return 'TOLAK: nama kosong — form ditampilkan ulang';
    }

    if (mb_strlen($nama) < 3) {
        return 'TOLAK: nama terlalu pendek — form ditampilkan ulang';
    }

    // Tahap 2: baru diproses
    return "TERIMA: pendaftar $nama tersimpan";
}

echo prosesAtauTolak([]), "\n";
echo prosesAtauTolak(['nama' => 'Bu']), "\n";
echo prosesAtauTolak(['nama' => 'Budi']), "\n";

Output yang Dihasilkan:

text
TOLAK: nama kosong — form ditampilkan ulang
TOLAK: nama terlalu pendek — form ditampilkan ulang
TERIMA: pendaftar Budi tersimpan

Pola “tahap 1 menolak lebih dulu, tahap 2 memproses” menjaga agar tidak ada baris pemrosesan yang berjalan dengan data setengah divalidasi. Pada server nyata, tahap 2 sering diakhiri redirect untuk mencegah submit ganda saat refresh — tuntas di Chapter Post/Redirect/Get . Berkas contoh.php pada bagian bawah halaman merangkai fungsi validasiPendaftaran(), penampil error, dan keputusan proses/tolak dalam satu program utuh.

Ringkasan #

Validasi server-side adalah satu-satunya gerbang kebenaran data: client-side hanyalah lapisan kenyamanan yang bisa dilewati siapa pun. Pola intinya berdiri di atas fungsi murni yang mengembalikan array error berkunci nama field, aturan yang dirangkai dari mb_strlen(), filter_var(), hingga in_array() strict, serta alur validasi-dulu-baru-proses dengan form yang ditampilkan ulang lengkap dengan nilainya. Satu pertanyaan masih menggantung: ketika input hampir benar — email dengan huruf kapital atau spasi berlebih — apakah sebaiknya ditolak atau dirapikan? Perbedaan validasi dan sanitasi itulah topik chapter berikutnya.

Referensi #

Berkas Sumber

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

contoh.php

Unduh berkas
php
<?php
// contoh.php — alur lengkap handler form: baca, validasi, tampilkan error, proses.
// CLI tidak menerima request, jadi kiriman form disimulasikan sebagai array.

/**
 * Memvalidasi kiriman form pendaftaran.
 * Mengembalikan array pesan error berkunci nama field; array kosong = lolos.
 */
function validasiPendaftaran(array $input): array
{
    $errors = [];

    $nama = trim($input['nama'] ?? '');
    if ($nama === '') {
        $errors['nama'] = 'Nama wajib diisi.';
    } elseif (mb_strlen($nama) < 3) {
        $errors['nama'] = 'Nama minimal 3 karakter.';
    }

    $umur = filter_var($input['umur'] ?? '', FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 17, 'max_range' => 99],
    ]);
    if ($umur === false) {
        $errors['umur'] = 'Umur harus berupa angka antara 17 dan 99.';
    }

    $email = trim($input['email'] ?? '');
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
        $errors['email'] = 'Format email tidak sah.';
    }

    return $errors;
}

/**
 * Handler lengkap: validasi dulu, baru memproses.
 */
function tanganiPendaftaran(array $post): string
{
    $errors = validasiPendaftaran($post);

    if ($errors !== []) {
        $baris = "TOLAK: " . count($errors) . " error — form ditampilkan ulang\n";
        foreach ($errors as $field => $pesan) {
            $baris .= "  - [$field] $pesan\n";
        }

        return rtrim($baris);
    }

    $nama = trim($post['nama']);

    return "TERIMA: pendaftar $nama (umur {$post['umur']}, {$post['email']}) tersimpan";
}

// Dua skenario: kiriman bermasalah dan kiriman yang lolos
$kirimanBuruk = ['nama' => 'Bu', 'umur' => '15', 'email' => 'budi@'];
$kirimanSah = ['nama' => 'Budi Santoso', 'umur' => '25', 'email' => '[email protected]'];

foreach ([$kirimanBuruk, $kirimanSah] as $kiriman) {
    echo tanganiPendaftaran($kiriman), "\n";
}