Filter & Validation #
Setiap data yang masuk ke aplikasi PHP dari luar — form HTML, query string URL, header HTTP, cookie, JSON dari API — tidak bisa dipercaya begitu saja. PHP menyediakan ekstensi filter yang sering diabaikan padahal sangat powerful: filter_var() dan filter_input() dengan puluhan filter bawaan untuk validasi (apakah nilai valid?) dan sanitasi (bersihkan nilai agar aman). Menggunakan fungsi filter bawaan PHP lebih baik dari menulis validasi manual karena mereka sudah diuji secara ekstensif, menangani edge case yang tidak terduga, dan mengikuti standar RFC yang benar. Artikel ini membahas semua filter penting beserta cara menggunakannya dengan benar — termasuk jebakan yang sering membuat developer mengira data sudah aman padahal belum.
Dua Fungsi Utama #
flowchart LR
A[Input Eksternal] --> B{Sumber input?}
B -- "Variabel PHP\n(bisa dari mana saja)" --> C["filter_var(\$nilai, FILTER)"]
B -- "Superglobal\n$_GET/$_POST/$_COOKIE/dll." --> D["filter_input(INPUT_*, 'nama', FILTER)"]
C --> E{Valid?}
D --> E
E -- Ya --> F[Gunakan nilai]
E -- Tidak --> G[Tolak / Default]
style C fill:#dcfce7
style D fill:#dcfce7
style G fill:#fee2e2<?php
// filter_var() — filter nilai PHP yang sudah ada di variabel
$email = "[email protected]";
$valid = filter_var($email, FILTER_VALIDATE_EMAIL);
// return nilai asli jika valid, false jika tidak
// filter_input() — ambil DAN filter dari superglobal sekaligus
// Lebih aman karena tidak pernah menyentuh $_GET/$_POST langsung
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$halaman = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
$token = filter_input(INPUT_COOKIE, 'token', FILTER_SANITIZE_ENCODED);
// Konstanta INPUT_*
// INPUT_GET → $_GET
// INPUT_POST → $_POST
// INPUT_COOKIE → $_COOKIE
// INPUT_SERVER → $_SERVER
// INPUT_ENV → $_ENV
FILTER_VALIDATE_* — Validasi #
Filter validasi mengembalikan nilai asli jika valid, atau false jika tidak valid. Selalu gunakan === false untuk mengecek kegagalan validasi.
Email #
<?php
// FILTER_VALIDATE_EMAIL
$valid = filter_var("[email protected]", FILTER_VALIDATE_EMAIL);
// "[email protected]" — valid, mengembalikan nilai aslinya
$invalid = filter_var("bukan-email", FILTER_VALIDATE_EMAIL);
// false
$invalid2 = filter_var("@example.com", FILTER_VALIDATE_EMAIL);
// false
// Contoh penggunaan yang benar
$emailInput = $_POST['email'] ?? '';
$email = filter_var(trim($emailInput), FILTER_VALIDATE_EMAIL);
if ($email === false) {
$errors['email'] = "Format email tidak valid";
} else {
// $email sudah tervalidasi, aman digunakan
simpanEmail($email);
}
// PERHATIAN: filter_var email TIDAK memverifikasi domain exists
// Untuk verifikasi domain, tambahkan checkdnsrr():
function validasiEmailLengkap(string $email): bool
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
$domain = substr($email, strrpos($email, '@') + 1);
return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}
URL #
<?php
// FILTER_VALIDATE_URL
$valid = filter_var("https://example.com", FILTER_VALIDATE_URL);
// "https://example.com"
$valid2 = filter_var("ftp://files.example.com/data.zip", FILTER_VALIDATE_URL);
// valid — semua scheme diterima secara default
$invalid = filter_var("bukan url", FILTER_VALIDATE_URL);
// false
// Dengan flag untuk memfilter scheme yang diizinkan
$options = [
'flags' => FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED,
];
// Hanya izinkan http dan https
function validasiUrlHttp(string $url): bool
{
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
$scheme = parse_url($url, PHP_URL_SCHEME);
return in_array($scheme, ['http', 'https'], strict: true);
}
var_dump(validasiUrlHttp("https://example.com")); // true
var_dump(validasiUrlHttp("ftp://example.com")); // false
var_dump(validasiUrlHttp("javascript:alert(1)")); // false — cegah XSS via URL
// PENTING: filter_var URL tidak berarti URL aman untuk di-redirect!
// Selalu validasi scheme secara eksplisit sebelum header('Location: ...')
Integer dan Float #
<?php
// FILTER_VALIDATE_INT
$valid = filter_var("42", FILTER_VALIDATE_INT); // int(42)
$valid2 = filter_var("-10", FILTER_VALIDATE_INT); // int(-10)
$invalid = filter_var("3.14", FILTER_VALIDATE_INT); // false
$invalid2 = filter_var("abc", FILTER_VALIDATE_INT); // false
// Dengan rentang min/max
$options = [
'options' => [
'min_range' => 1,
'max_range' => 100,
],
];
$halaman = filter_var("50", FILTER_VALIDATE_INT, $options); // int(50)
$invalid = filter_var("150", FILTER_VALIDATE_INT, $options); // false — melebihi 100
$invalid2 = filter_var("0", FILTER_VALIDATE_INT, $options); // false — di bawah 1
// Penggunaan praktis: parameter pagination
$halaman = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'default' => 1],
]);
$halaman = $halaman ?: 1; // gunakan default 1 jika null atau false
// FILTER_VALIDATE_FLOAT
$valid = filter_var("3.14", FILTER_VALIDATE_FLOAT); // float(3.14)
$valid2 = filter_var("1.5e3", FILTER_VALIDATE_FLOAT); // float(1500)
$valid3 = filter_var("1,500.75", FILTER_VALIDATE_FLOAT, [
'flags' => FILTER_FLAG_ALLOW_THOUSAND, // izinkan koma sebagai pemisah ribuan
]);
// Dengan desimal koma (format Eropa/Indonesia)
$options = ['options' => ['decimal' => ',']]; // koma sebagai desimal
$valid4 = filter_var("1.500,75", FILTER_VALIDATE_FLOAT, $options); // 1500.75
Boolean #
<?php
// FILTER_VALIDATE_BOOLEAN — lebih cerdas dari (bool)$nilai
// Mengembalikan true, false, atau null (jika nilai ambigu)
// Nilai yang dianggap TRUE:
// "true", "on", "yes", "1", 1, true (case-insensitive)
var_dump(filter_var("true", FILTER_VALIDATE_BOOLEAN)); // bool(true)
var_dump(filter_var("yes", FILTER_VALIDATE_BOOLEAN)); // bool(true)
var_dump(filter_var("on", FILTER_VALIDATE_BOOLEAN)); // bool(true)
var_dump(filter_var("1", FILTER_VALIDATE_BOOLEAN)); // bool(true)
var_dump(filter_var(true, FILTER_VALIDATE_BOOLEAN)); // bool(true)
// Nilai yang dianggap FALSE:
// "false", "off", "no", "0", 0, false (case-insensitive)
var_dump(filter_var("false", FILTER_VALIDATE_BOOLEAN)); // bool(false)
var_dump(filter_var("no", FILTER_VALIDATE_BOOLEAN)); // bool(false)
var_dump(filter_var("0", FILTER_VALIDATE_BOOLEAN)); // bool(false)
// Nilai AMBIGU mengembalikan null (bukan false!)
var_dump(filter_var("maybe", FILTER_VALIDATE_BOOLEAN)); // NULL
var_dump(filter_var("", FILTER_VALIDATE_BOOLEAN)); // NULL
// Dengan FILTER_NULL_ON_FAILURE — null untuk ambigu, false hanya untuk false
$result = filter_var("invalid", FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
// NULL — ambigu
// Berguna untuk konfigurasi dari env variable
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
// getenv mengembalikan string — filter_var mengkonversinya ke boolean yang benar
IP Address #
<?php
// FILTER_VALIDATE_IP
$valid = filter_var("192.168.1.1", FILTER_VALIDATE_IP); // "192.168.1.1"
$valid2 = filter_var("::1", FILTER_VALIDATE_IP); // "::1" (IPv6)
$invalid = filter_var("999.999.999.999", FILTER_VALIDATE_IP); // false
// Flag untuk filter lebih spesifik
// Hanya IPv4
$ipv4 = filter_var("192.168.1.1", FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
// Hanya IPv6
$ipv6 = filter_var("::1", FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
// Tolak IP privat (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
$publik = filter_var("192.168.1.1", FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);
// false — karena 192.168.x.x adalah privat
// Tolak IP reserved (loopback 127.x.x.x, dll.)
$publik2 = filter_var("127.0.0.1", FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE);
// false
// Validasi IP client yang nyata (di balik proxy/load balancer)
function getClientIp(): ?string
{
$headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ip = trim(explode(',', $_SERVER[$header])[0]); // ambil IP pertama
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
// Fallback ke REMOTE_ADDR tanpa filter publik
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : null;
}
Lainnya #
<?php
// FILTER_VALIDATE_DOMAIN — validasi nama domain
$valid = filter_var("example.com", FILTER_VALIDATE_DOMAIN); // "example.com"
$valid2 = filter_var("sub.example.com", FILTER_VALIDATE_DOMAIN); // valid
$invalid = filter_var("not a domain!", FILTER_VALIDATE_DOMAIN); // false
// FILTER_VALIDATE_MAC — MAC address
$valid = filter_var("AA:BB:CC:DD:EE:FF", FILTER_VALIDATE_MAC); // valid
$valid2 = filter_var("AA-BB-CC-DD-EE-FF", FILTER_VALIDATE_MAC); // valid
// FILTER_VALIDATE_REGEXP — validasi dengan regex kustom
$options = ['options' => ['regexp' => '/^\d{5}$/']] ; // kode pos 5 digit
$valid = filter_var("12345", FILTER_VALIDATE_REGEXP, $options); // "12345"
$invalid = filter_var("1234", FILTER_VALIDATE_REGEXP, $options); // false
// FILTER_VALIDATE_REGEXP vs preg_match langsung
// Gunakan preg_match untuk fleksibilitas lebih
// Gunakan FILTER_VALIDATE_REGEXP untuk konsistensi dengan pipeline filter lainnya
FILTER_SANITIZE_* — Sanitasi #
Filter sanitasi membersihkan nilai — menghapus atau encode karakter yang tidak diinginkan. Mereka tidak memvalidasi apakah nilai valid, hanya membersihkannya.
<?php
// FILTER_SANITIZE_EMAIL
// Hapus semua karakter selain yang valid di email
$bersih = filter_var("budi @ex ample.com!", FILTER_SANITIZE_EMAIL);
// "[email protected]" — spasi dan ! dihapus
// PERHATIAN: hasilnya belum tentu email valid! Tetap validasi setelah sanitasi.
// FILTER_SANITIZE_URL
$bersih = filter_var("https://example.com/path with spaces", FILTER_SANITIZE_URL);
// "https://example.com/path%20with%20spaces" — URL encode spasi
// Tapi ini tidak enkode semua karakter berbahaya — lebih baik gunakan urlencode/rawurlencode
// FILTER_SANITIZE_NUMBER_INT
// Hapus semua karakter selain digit, + dan -
$bersih = filter_var("Rp 15.000.000,-", FILTER_SANITIZE_NUMBER_INT);
// "15000000" — hapus semua non-digit kecuali + dan -
$bersih2 = filter_var("+62 812-3456-789", FILTER_SANITIZE_NUMBER_INT);
// "+62812345789"
// FILTER_SANITIZE_NUMBER_FLOAT
// Seperti NUMBER_INT tapi pertahankan titik desimal
$bersih = filter_var("Rp 15.000,50", FILTER_SANITIZE_NUMBER_FLOAT, [
'flags' => FILTER_FLAG_ALLOW_FRACTION, // izinkan titik/koma desimal
]);
// FILTER_SANITIZE_SPECIAL_CHARS
// Encode karakter HTML spesial menjadi entitas HTML
$bersih = filter_var('<script>alert("xss")</script>', FILTER_SANITIZE_SPECIAL_CHARS);
// "<script>alert("xss")</script>"
// Tapi untuk output ke HTML, lebih baik gunakan:
$aman = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// FILTER_SANITIZE_ADD_SLASHES (PHP 7.3+, pengganti MAGIC_QUOTES)
$bersih = filter_var("It's a \"test\"", FILTER_SANITIZE_ADD_SLASHES);
// "It\'s a \"test\""
// PERHATIAN: addslashes() bukan cara yang benar untuk mencegah SQL injection!
// Gunakan prepared statement!
// FILTER_SANITIZE_ENCODED (URL encoding)
$bersih = filter_var("nama saya & teman", FILTER_SANITIZE_ENCODED);
// "nama%20saya%20%26%20teman"
// FILTER_DEFAULT (alias FILTER_UNSAFE_RAW)
// Tidak melakukan apapun — return nilai apa adanya
$raw = filter_input(INPUT_POST, 'data', FILTER_DEFAULT);
// Sama dengan $_POST['data'] tapi lebih aman dari undefined index
filter_input_array() — Validasi Banyak Input Sekaligus
#
<?php
// Definisikan aturan validasi untuk seluruh form
$aturan = [
'nama' => FILTER_SANITIZE_SPECIAL_CHARS,
'email' => FILTER_VALIDATE_EMAIL,
'umur' => [
'filter' => FILTER_VALIDATE_INT,
'options' => ['min_range' => 1, 'max_range' => 120],
],
'website' => [
'filter' => FILTER_VALIDATE_URL,
'flags' => FILTER_FLAG_SCHEME_REQUIRED,
],
'aktif' => FILTER_VALIDATE_BOOLEAN,
'skor' => [
'filter' => FILTER_VALIDATE_FLOAT,
'options' => ['min_range' => 0, 'max_range' => 100],
],
'tags' => [
'filter' => FILTER_SANITIZE_SPECIAL_CHARS,
'flags' => FILTER_REQUIRE_ARRAY, // input adalah array
],
];
// Proses semua input POST sekaligus
$data = filter_input_array(INPUT_POST, $aturan);
// Cek hasil
if ($data === null) {
// filter_input_array mengembalikan null jika superglobal tidak ada
throw new \RuntimeException("Tidak ada data POST");
}
// $data sekarang:
// ['nama' => 'Budi', 'email' => 'budi@...', 'umur' => 28, ...]
// Field yang tidak valid: false
// Field yang tidak ada: null
$errors = [];
if ($data['email'] === false) {
$errors['email'] = "Format email tidak valid";
}
if ($data['umur'] === false) {
$errors['umur'] = "Umur harus antara 1 dan 120";
}
if ($data['website'] === false) {
$errors['website'] = "URL tidak valid";
}
if (empty($errors)) {
// Semua valid — proses data
prosesRegistrasi($data);
}
Validasi Custom dengan Callback #
Untuk validasi yang tidak tersedia sebagai filter bawaan, gunakan FILTER_CALLBACK:
<?php
// Validasi nomor telepon Indonesia
$validasiTelepon = function(string $nomor): string|false {
// Bersihkan: hapus spasi, tanda hubung, dan tanda kurung
$bersih = preg_replace('/[\s\-\(\)]/', '', $nomor);
// Konversi +62 ke 0
if (str_starts_with($bersih, '+62')) {
$bersih = '0' . substr($bersih, 3);
}
// Validasi: harus dimulai 08, panjang 10-13 digit
if (!preg_match('/^08[1-9]\d{7,10}$/', $bersih)) {
return false;
}
return $bersih; // return nilai yang sudah dibersihkan
};
$telepon = filter_var("0812-3456-789", FILTER_CALLBACK, ['options' => $validasiTelepon]);
// "081234567890" — valid dan dibersihkan
$invalid = filter_var("12345", FILTER_CALLBACK, ['options' => $validasiTelepon]);
// false
// Validasi kode pos Indonesia (5 digit)
$validasiKodePos = fn($kode) => preg_match('/^\d{5}$/', $kode) ? $kode : false;
$kodePos = filter_var("12345", FILTER_CALLBACK, ['options' => $validasiKodePos]);
// Validasi NIK (16 digit)
$validasiNIK = function(string $nik): string|false {
$nik = trim($nik);
return preg_match('/^\d{16}$/', $nik) ? $nik : false;
};
$nik = filter_var("3201234567890001", FILTER_CALLBACK, ['options' => $validasiNIK]);
Pola Validasi Form yang Lengkap #
<?php
class FormValidator
{
private array $errors = [];
private array $data = [];
private array $input;
public function __construct(array $input)
{
$this->input = $input;
}
public function required(string $field, string $label): static
{
$nilai = trim($this->input[$field] ?? '');
if ($nilai === '') {
$this->errors[$field] = "$label wajib diisi";
} else {
$this->data[$field] = $nilai;
}
return $this;
}
public function email(string $field, string $label): static
{
$nilai = filter_var(trim($this->input[$field] ?? ''), FILTER_VALIDATE_EMAIL);
if ($nilai === false) {
$this->errors[$field] = "$label tidak valid";
} else {
$this->data[$field] = $nilai;
}
return $this;
}
public function integer(string $field, string $label, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): static
{
$nilai = filter_var(
$this->input[$field] ?? '',
FILTER_VALIDATE_INT,
['options' => ['min_range' => $min, 'max_range' => $max]]
);
if ($nilai === false) {
$this->errors[$field] = "$label harus angka antara $min dan $max";
} else {
$this->data[$field] = $nilai;
}
return $this;
}
public function url(string $field, string $label): static
{
$raw = trim($this->input[$field] ?? '');
$nilai = filter_var($raw, FILTER_VALIDATE_URL);
$scheme = parse_url($raw, PHP_URL_SCHEME);
if ($nilai === false || !in_array($scheme, ['http', 'https'], true)) {
$this->errors[$field] = "$label harus URL http/https yang valid";
} else {
$this->data[$field] = $nilai;
}
return $this;
}
public function custom(string $field, string $label, callable $validator): static
{
$nilai = filter_var(
$this->input[$field] ?? '',
FILTER_CALLBACK,
['options' => $validator]
);
if ($nilai === false) {
$this->errors[$field] = "$label tidak valid";
} else {
$this->data[$field] = $nilai;
}
return $this;
}
public function isValid(): bool
{
return empty($this->errors);
}
public function getErrors(): array
{
return $this->errors;
}
public function getData(): array
{
return $this->data;
}
}
// Penggunaan
$validator = new FormValidator($_POST);
$validator
->required('nama', 'Nama')
->email('email', 'Email')
->integer('umur', 'Umur', min: 1, max: 120)
->url('website', 'Website')
->custom('telepon', 'Telepon', fn($v) => preg_match('/^08\d{9,11}$/', preg_replace('/\D/', '', $v))
? preg_replace('/\D/', '', $v) : false);
if (!$validator->isValid()) {
http_response_code(422);
echo json_encode(['errors' => $validator->getErrors()]);
exit;
}
$data = $validator->getData();
simpanUser($data);
Anti-Pattern Validasi yang Sering Ditemui #
<?php
// ✗ Anti-pattern 1: akses superglobal langsung tanpa validasi
$email = $_POST['email']; // undefined index, tidak divalidasi, tidak disanitasi
// ✓ Gunakan filter_input + validasi
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
$errors['email'] = "Email tidak valid";
}
// ✗ Anti-pattern 2: gunakan == false untuk cek validasi
$email = filter_var($input, FILTER_VALIDATE_EMAIL);
if ($email == false) { } // "" == false juga true! Email kosong lolos!
// ✓ Selalu gunakan === false
if ($email === false) {
$errors['email'] = "Email tidak valid";
}
// ✗ Anti-pattern 3: sanitasi saja tanpa validasi
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
simpanEmail($email); // mungkin hasilnya bukan email valid!
// ✓ Sanitasi lalu validasi (atau validasi saja yang sudah cukup)
$email = filter_var(
filter_var($_POST['email'], FILTER_SANITIZE_EMAIL),
FILTER_VALIDATE_EMAIL
);
// ✗ Anti-pattern 4: gunakan FILTER_SANITIZE_STRING (deprecated PHP 8.1)
$nama = filter_var($input, FILTER_SANITIZE_STRING); // deprecated!
// ✓ Gunakan htmlspecialchars untuk output ke HTML
$nama = htmlspecialchars(trim($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');
// ✗ Anti-pattern 5: menganggap filter_var cukup untuk mencegah SQL injection
$id = filter_var($_GET['id'], FILTER_VALIDATE_INT);
$db->query("SELECT * FROM users WHERE id = $id"); // masih berbahaya!
// ✓ Validasi + prepared statement
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
http_response_code(400);
exit;
}
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
Ringkasan #
filter_input()lebih baik dari$_POST['nama']— mengambil dan memfilter sekaligus, tidak melempar warning jika key tidak ada, dan lebih eksplisit soal sumber data.=== falsebukan== false— filter validasi mengembalikan nilai asli (bisa string kosong, 0, false literal) ataufalsejika invalid. String kosong""sama denganfalsejika dibandingkan dengan==.- Sanitasi ≠ Validasi —
FILTER_SANITIZE_EMAILmembersihkan karakter tapi tidak memastikan hasilnya adalah email valid. Selalu validasi setelah sanitasi, atau cukup validasi saja.FILTER_VALIDATE_BOOLEANjauh lebih cerdas dari casting(bool)— ia mengenali"true","yes","on","1"sebagaitruedan kebalikannya sebagaifalse. Sangat berguna untuk env variable dan konfigurasi.filter_input_array()untuk validasi form sekaligus — definisikan aturan sebagai array, proses semua input dalam satu pemanggilan, hasilkan array dengan nilai tervalidasi ataufalse/null.FILTER_CALLBACKuntuk validasi custom — gunakan closure sebagai validator, kembalikan nilai yang dibersihkan jika valid ataufalsejika tidak.- Filter bukan pengganti prepared statement — meski sudah memvalidasi integer dengan
FILTER_VALIDATE_INT, tetap gunakan prepared statement untuk query database. Validasi mencegah logika salah, prepared statement mencegah SQL injection.FILTER_SANITIZE_STRINGsudah deprecated sejak PHP 8.1 — gunakanhtmlspecialchars()untuk escape output HTML, ataustrip_tags()untuk menghapus tag HTML.