Prinsip tertua pengolahan data berbunyi garbage in, garbage out: seberapa canggih pun pipeline kita, hasil akhirnya tetap sampah apabila datanya sampah. Berkas CSV dari klien bisa kehilangan kolom, baris kelima bisa berisi umur berupa teks, dan tanggal pembayaran bisa lebih awal daripada tanggal pesanan dibuat. Validasi pada Chapter ini berbeda dari validasi form: kita memeriksa dataset — ratusan hingga jutaan baris sekaligus — dan pelaporannya harus menyebut baris mana yang bermasalah beserta alasannya. Kita akan membangun validator baris demi baris, mendefinisikan kebijakan nilai kosong, memeriksa rentang dan koherensi, lalu memilih antara membersihkan atau menolak data bermasalah.
Validasi Pipeline vs Validasi Form #
Validasi form (Section Form Handling dan Validation) berjalan interaktif: satu permintaan dari satu pengguna, galat langsung ditampilkan di layar, dan pengguna memperbaiki inputnya sendiri. Validasi pipeline bekerja dalam batch: data sudah ada, tidak ada manusia yang menunggu balasan, dan keputusan harus diambil secara otomatis untuk setiap baris. Akibatnya, tiga kebutuhan berikut menjadi krusial dalam konteks pipeline: kecepatan pemeriksaan per baris, laporan galat yang menjelaskan lokasi persis masalah, dan kebijakan yang jelas untuk data yang gagal. Konsep dasar sanitasi versus validasi tetap sama seperti pada Chapter Validasi Server Side — kita hanya menerapkannya pada array data alih-alih $_POST.
Memeriksa Skema Setiap Baris #
Pemeriksaan pertama mirip dengan schema database: kolom wajib harus ada, dan isinya harus berupa tipe yang benar. Karena sumber seperti CSV menghasilkan baris array, validator kita menerima satu baris dan mengembalikan daftar galat:
<?php
$barisData = [
["nama" => "Budi Santoso", "email" => "[email protected]", "umur" => "28", "gaji" => "8500000"],
["nama" => "", "email" => "[email protected]", "umur" => "31", "gaji" => "7800000"],
["nama" => "Rina", "email" => "bukan-email", "umur" => "17", "gagi" => "5000000"],
["nama" => "Tono", "email" => "[email protected]", "umur" => "dua puluh", "gaji" => "9000000"],
];
function validateRow(array $row): array
{
$galat = [];
if (trim($row["nama"] ?? "") === "") {
$galat[] = "nama wajib diisi";
}
if (!filter_var($row["email"] ?? "", FILTER_VALIDATE_EMAIL)) {
$galat[] = "email tidak valid";
}
if (!ctype_digit($row["umur"] ?? "")) {
$galat[] = "umur harus berupa angka positif";
}
if (!isset($row["gaji"])) {
$galat[] = "kolom gaji hilang";
}
return $galat;
}
foreach ($barisData as $nomor => $row) {
$galat = validateRow($row);
if ($galat === []) {
echo "Baris " . ($nomor + 1) . ": valid\n";
} else {
echo "Baris " . ($nomor + 1) . ": " . implode("; ", $galat) . "\n";
}
}Output yang Dihasilkan:
Baris 1: valid
Baris 2: nama wajib diisi
Baris 3: email tidak valid; kolom gaji hilang
Baris 4: umur harus berupa angka positifMari kita bedah aturan-aturan di dalam validateRow():
- Kolom wajib:
trim($row["nama"] ?? "") === ""memakai operator??agar kolom yang hilang sekalipun tidak memicu warning undefined key, lalu memperlakukan spasi kosong sama seperti kosong. - Format email:
filter_var()denganFILTER_VALIDATE_EMAILmengembalikan email yang sah ataufalse; negasinya menandai baris ketiga yang berisibukan-email. - Tipe angka:
ctype_digit()memastikan umur tersusun dari digit — baris keempat berisi teks"dua puluh"sehingga gagal. - Kolom hilang: baris ketiga menyimpan kunci
gagi(salah ketik), sehinggaisset($row["gaji"])bernilaifalse— kekeliruan nama kolom tertangkap, bukan diam-diam diabaikan.
Perulangan di luar fungsi mencetak nomor baris ($nomor + 1 karena array mulai dari nol) beserta seluruh alasannya. Nomor baris inilah mata dan telinga pengguna pipeline: tanpa itu, mencari satu baris bermasalah di antara 50.000 baris sama sulitnya dengan mencari jarum di jerami.
Kebijakan Nilai Kosong #
Sebelum memutuskan baris mana yang ditolak, kita harus mendefinisikan apa itu “kosong” — dan PHP punya beberapa wujudnya. Contoh berikut membandingkan penilaian eksplisit dengan hasil fungsi empty():
<?php
$kolomContoh = [
"telepon" => "0",
"fax" => "",
"npwp" => null,
"catatan" => "pelanggan langganan",
];
foreach ($kolomContoh as $kolom => $isi) {
$status = match (true) {
$isi === null => "null",
$isi === "" => "string kosong",
$isi === "0" => "string nol",
default => "terisi",
};
$menurutEmpty = empty($isi) ? "dianggap kosong" : "dianggap terisi";
echo "$kolom: $status ($menurutEmpty oleh empty)\n";
}Output yang Dihasilkan:
telepon: string nol (dianggap kosong oleh empty)
fax: string kosong (dianggap kosong oleh empty)
npwp: null (dianggap kosong oleh empty)
catatan: terisi (dianggap terisi oleh empty)Ekspresi match (true) memetakan pemeriksaan berurutan yang pertama cocok — pola yang rapi untuk klasifikasi bertingkat. Perhatikan jebakan pada kolom telepon: string "0" dianggap kosong oleh empty(), padahal nomor telepon yang sah bisa saja dimulai dari nol atau memang bernilai nol.
Kebijakan yang sehat biasanya membedakan tiga kondisi: null berarti “data tidak pernah dikirim” (boleh kosong untuk kolom opsional), string kosong berarti “dikirim tanpa isi” (perlu keputusan: isi default atau tolak), dan nilai terisi masuk pemeriksaan berikutnya. Kuncinya adalah menuliskan kebijakan tersebut secara eksplisit di kode, bukan membiarkan perilaku bawaan fungsi yang memutuskan.
Validasi Rentang dan Koherensi Antar Kolom #
Nilai yang bertipe benar belum tentu masuk akal. Pemeriksaan berikutnya membandingkan nilai terhadap rentang wajar dan terhadap kolom lain di baris yang sama:
<?php
function validasiPesanan(array $p): array
{
$galat = [];
if ($p["qty"] < 1 || $p["qty"] > 100) {
$galat[] = "qty harus di antara 1-100";
}
if ($p["harga"] <= 0) {
$galat[] = "harga harus lebih besar dari 0";
}
$dibuat = DateTimeImmutable::createFromFormat("Y-m-d", $p["tanggal_dibuat"]);
$dibayar = DateTimeImmutable::createFromFormat("Y-m-d", $p["tanggal_bayar"]);
if ($dibuat === false || $dibayar === false) {
$galat[] = "format tanggal harus Y-m-d";
} elseif ($dibayar < $dibuat) {
$galat[] = "tanggal_bayar tidak boleh sebelum tanggal_dibuat";
}
return $galat;
}
$pesanans = [
["qty" => 3, "harga" => 55000, "tanggal_dibuat" => "2026-01-10", "tanggal_bayar" => "2026-01-12"],
["qty" => 250, "harga" => -5000, "tanggal_dibuat" => "2026-01-10", "tanggal_bayar" => "2026-01-08"],
];
foreach ($pesanans as $i => $p) {
$galat = validasiPesanan($p);
echo "Pesanan #" . ($i + 1) . ": " . ($galat === [] ? "valid" : implode("; ", $galat)) . "\n";
}Output yang Dihasilkan:
Pesanan #1: valid
Pesanan #2: qty harus di antara 1-100; harga harus lebih besar dari 0; tanggal_bayar tidak boleh sebelum tanggal_dibuatPemeriksaan rentang menolak qty 250 (di luar 1-100) dan harga negatif. Pemeriksaan koherensi kemudian membandingkan dua kolom tanggal: createFromFormat() gagal menjadi false apabila formatnya salah, dan hanya setelah kedua tanggal berhasil dibuat, perbandingan $dibayar < $dibuat dijalankan — pesanan kedua tercatat dibayar dua hari sebelum dibuat, hal yang mustahil dan hampir pasti gejala kesalahan data. Pola if ... elseif ini penting: membandingkan objek waktu yang gagal dibuat tidak ada artinya, jadi pemeriksaan format selalu didahulukan.
Membersihkan atau Menolak #
Untuk galat ringan yang sumbernya jelas — spasi berlebih, kapitalisasi email yang tidak konsisten, angka berupa string — membersihkan data lebih murah daripada menolaknya:
<?php
$mentah = [
[" Budi Santoso ", " [email protected] ", "28"],
["Siti Rahayu", "[email protected]", "31"],
];
$bersih = array_map(
fn (array $row): array => [
"nama" => trim($row[0]),
"email" => strtolower(trim($row[1])),
"umur" => (int) $row[2],
],
$mentah
);
foreach ($bersih as $row) {
echo sprintf("%s <%s> (%d tahun)\n", $row["nama"], $row["email"], $row["umur"]);
}Output yang Dihasilkan:
Budi Santoso <[email protected]> (28 tahun)
Siti Rahayu <[email protected]> (31 tahun)array_map() menerapkan closure yang sama ke setiap baris: trim() memangkas spasi, strtolower() menyeragamkan email ke huruf kecil, dan (int) mengonversi string angka menjadi integer. Aturan praktisnya sederhana: bersihkan hanya transformasi yang lossless dan tidak mengubah makna (spasi, kapitalisasi, tipe angka), sedangkan data yang mengubah makna — email yang tidak valid, tanggal yang saling bertentangan, harga negatif — harus ditolak, bukan “diperbaiki” diam-diam. Pemisahan tegas antara langkah pembersihan dan langkah validasi juga membuat pipeline mudah diuji: bersihkan dulu, validasi sesudahnya.
Fail-Fast atau Collect-All #
Dua strategi berlawanan menentukan apa yang terjadi saat galat pertama ditemukan. Fail-fast langsung melempar exception dan menghentikan seluruh proses — cocok ketika data yang bermasalah membuat seluruh hasil tidak berguna, misalnya impor ke database transaksional yang harus utuh semua atau tidak sama sekali. Collect-all seperti contoh validateRow() di awal chapter mengumpulkan seluruh galat dulu, baru melaporkannya — cocok untuk laporan kualitas data karena pengguna melihat semua masalah sekaligus dan bisa memperbaiki berkas asalnya dalam satu putaran.
Pilihan di antara keduanya adalah keputusan bisnis, bukan teknis. Kombinasi keduanya juga umum: pipeline besar memakai collect-all untuk menghasilkan laporan lengkap, lalu proses lanjutan yang memakai data bersih memakai fail-fast sebagai jaring pengaman terakhir — baris yang lolos laporan tetap ditolak keras apabila ternyata bermasalah saat dipakai.
Ringkasan #
Validator per baris memeriksa skema (kolom wajib, tipe, format), kebijakan nilai kosong didefinisikan eksplisit dengan perbandingan ketat alih-alih empty(), dan pemeriksaan rentang serta koherensi menjamin data masuk akal lintas kolom. Laporan galat selalu menyebut nomor baris dan alasan, sementara keputusan membersihkan versus menolak mengikuti prinsip transformasi tanpa mengubah makna. Setelah data bersih dan terpercaya, langkah berikutnya adalah mengubah bentuknya — normalisasi, turunan, dan penggabungan — yang akan kita bahas pada Chapter Data Transformation.