Filter & Validation #

Any data entering a PHP application from outside — HTML forms, URL query strings, HTTP headers, cookies, JSON from APIs — can’t be trusted as-is. PHP provides the filter extension that’s often overlooked even though it’s very powerful: filter_var() and filter_input() with dozens of built-in filters for validation (is the value valid?) and sanitization (clean the value to make it safe). Using PHP’s built-in filter functions is better than writing manual validation because they’re extensively tested, handle unexpected edge cases, and follow the correct RFC standards. This article covers all the important filters and how to use them correctly — including the traps that make developers think their data is safe when it isn’t.

The Two Main Functions #

flowchart LR
    A[External Input] --> B{Input source?}
    B -- "PHP variable\n(from anywhere)" --> C["filter_var(\$value, FILTER)"]
    B -- "Superglobal\n\$_GET/\$_POST/\$_COOKIE/etc." --> D["filter_input(INPUT_*, 'name', FILTER)"]
    C --> E{Valid?}
    D --> E
    E -- Yes --> F[Use the value]
    E -- No --> G[Reject / Default]

    style C fill:#dcfce7
    style D fill:#dcfce7
    style G fill:#fee2e2
<?php
// filter_var() — filter a PHP value already in a variable
$email = "[email protected]";
$valid = filter_var($email, FILTER_VALIDATE_EMAIL);
// returns the original value if valid, false if not

// filter_input() — fetch AND filter from a superglobal in one step
// Safer because it never touches $_GET/$_POST directly
$email   = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$page    = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
$token   = filter_input(INPUT_COOKIE, 'token', FILTER_SANITIZE_ENCODED);

// The INPUT_* constants
// INPUT_GET     → $_GET
// INPUT_POST    → $_POST
// INPUT_COOKIE  → $_COOKIE
// INPUT_SERVER  → $_SERVER
// INPUT_ENV     → $_ENV

FILTER_VALIDATE_* — Validation #

Validation filters return the original value if valid, or false if not. Always use === false to check for validation failure.

Email #

<?php
// FILTER_VALIDATE_EMAIL
$valid   = filter_var("[email protected]", FILTER_VALIDATE_EMAIL);
// "[email protected]" — valid, returns the original value

$invalid = filter_var("not-an-email", FILTER_VALIDATE_EMAIL);
// false

$invalid2 = filter_var("@example.com", FILTER_VALIDATE_EMAIL);
// false

// Example of correct usage
$emailInput = $_POST['email'] ?? '';
$email      = filter_var(trim($emailInput), FILTER_VALIDATE_EMAIL);

if ($email === false) {
    $errors['email'] = "Invalid email format";
} else {
    // $email is validated, safe to use
    saveEmail($email);
}

// WARNING: filter_var email does NOT verify the domain exists
// For domain verification, add checkdnsrr():
function validateEmailFull(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 — all schemes are accepted by default

$invalid = filter_var("not a url", FILTER_VALIDATE_URL);
// false

// With flags to filter the allowed schemes
$options = [
    'flags' => FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED,
];

// Only allow http and https
function validateHttpUrl(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(validateHttpUrl("https://example.com")); // true
var_dump(validateHttpUrl("ftp://example.com"));   // false
var_dump(validateHttpUrl("javascript:alert(1)")); // false — prevent XSS via URLs

// IMPORTANT: filter_var URL doesn't mean the URL is safe to redirect to!
// Always validate the scheme explicitly before header('Location: ...')

Integers and Floats #

<?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

// With a min/max range
$options = [
    'options' => [
        'min_range' => 1,
        'max_range' => 100,
    ],
];

$page    = filter_var("50", FILTER_VALIDATE_INT, $options); // int(50)
$invalid = filter_var("150", FILTER_VALIDATE_INT, $options); // false — exceeds 100
$invalid2 = filter_var("0", FILTER_VALIDATE_INT, $options);  // false — below 1

// Practical use: pagination parameters
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'default' => 1],
]);
$page = $page ?: 1; // use default 1 if null or 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, // allow commas as thousands separators
]);

// With a comma decimal (European/Indonesian format)
$options = ['options' => ['decimal' => ',']]; // comma as the decimal separator
$valid4  = filter_var("1.500,75", FILTER_VALIDATE_FLOAT, $options); // 1500.75

Booleans #

<?php
// FILTER_VALIDATE_BOOLEAN — smarter than (bool)$value
// Returns true, false, or null (for ambiguous values)

// Values treated as 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)

// Values treated as 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)

// AMBIGUOUS values return null (not false!)
var_dump(filter_var("maybe", FILTER_VALIDATE_BOOLEAN)); // NULL
var_dump(filter_var("",      FILTER_VALIDATE_BOOLEAN)); // NULL

// With FILTER_NULL_ON_FAILURE — null for ambiguous, false only for false
$result = filter_var("invalid", FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
// NULL — ambiguous

// Useful for configuration from env variables
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
// getenv returns a string — filter_var converts it to the correct boolean

IP Addresses #

<?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

// Flags for more specific filtering
// IPv4 only
$ipv4 = filter_var("192.168.1.1", FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
// IPv6 only
$ipv6 = filter_var("::1", FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);

// Reject private IPs (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
$public = filter_var("192.168.1.1", FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);
// false — because 192.168.x.x is private

// Reject reserved IPs (loopback 127.x.x.x, etc.)
$public2 = filter_var("127.0.0.1", FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE);
// false

// Validate the real client IP (behind a 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]); // take the first IP
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                return $ip;
            }
        }
    }

    // Fall back to REMOTE_ADDR without the public filter
    $ip = $_SERVER['REMOTE_ADDR'] ?? null;
    return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : null;
}

Others #

<?php
// FILTER_VALIDATE_DOMAIN — validate a domain name
$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 addresses
$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 — validation with a custom regex
$options = ['options' => ['regexp' => '/^\d{5}$/']] ; // 5-digit postal code
$valid   = filter_var("12345", FILTER_VALIDATE_REGEXP, $options); // "12345"
$invalid = filter_var("1234",  FILTER_VALIDATE_REGEXP, $options); // false

// FILTER_VALIDATE_REGEXP vs direct preg_match
// Use preg_match for more flexibility
// Use FILTER_VALIDATE_REGEXP for consistency with the rest of your filter pipeline

FILTER_SANITIZE_* — Sanitization #

Sanitization filters clean values — removing or encoding unwanted characters. They do not validate whether a value is valid, only clean it.

<?php
// FILTER_SANITIZE_EMAIL
// Remove all characters except those valid in an email
$clean = filter_var("budi @ex ample.com!", FILTER_SANITIZE_EMAIL);
// "[email protected]" — spaces and ! are removed
// WARNING: the result isn't necessarily a valid email! Still validate after sanitizing.

// FILTER_SANITIZE_URL
$clean = filter_var("https://example.com/path with spaces", FILTER_SANITIZE_URL);
// "https://example.com/path%20with%20spaces" — URL-encodes spaces
// But it doesn't encode all dangerous characters — better to use urlencode/rawurlencode

// FILTER_SANITIZE_NUMBER_INT
// Remove all characters except digits, + and -
$clean = filter_var("Rp 15.000.000,-", FILTER_SANITIZE_NUMBER_INT);
// "15000000" — removes all non-digits except + and -

$clean2 = filter_var("+62 812-3456-789", FILTER_SANITIZE_NUMBER_INT);
// "+628****5789"

// FILTER_SANITIZE_NUMBER_FLOAT
// Like NUMBER_INT but keeps the decimal point
$clean = filter_var("Rp 15.000,50", FILTER_SANITIZE_NUMBER_FLOAT, [
    'flags' => FILTER_FLAG_ALLOW_FRACTION,  // allow the decimal point/comma
]);

// FILTER_SANITIZE_SPECIAL_CHARS
// Encode special HTML characters into HTML entities
$clean = filter_var('<script>alert("xss")</script>', FILTER_SANITIZE_SPECIAL_CHARS);
// "&#60;script&#62;alert(&#34;xss&#34;)&#60;/script&#62;"

// But for HTML output, better to use:
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

// FILTER_SANITIZE_ADD_SLASHES (PHP 7.3+, the MAGIC_QUOTES replacement)
$clean = filter_var("It's a \"test\"", FILTER_SANITIZE_ADD_SLASHES);
// "It\'s a \"test\""
// WARNING: addslashes() isn't the right way to prevent SQL injection!
// Use prepared statements!

// FILTER_SANITIZE_ENCODED (URL encoding)
$clean = filter_var("my name & friend", FILTER_SANITIZE_ENCODED);
// "my%20name%20%26%20friend"

// FILTER_DEFAULT (alias of FILTER_UNSAFE_RAW)
// Does nothing — returns the value as-is
$raw = filter_input(INPUT_POST, 'data', FILTER_DEFAULT);
// Same as $_POST['data'] but safer against undefined indexes

filter_input_array() — Validating Many Inputs at Once #

<?php
// Define validation rules for the entire form
$rules = [
    'name'    => FILTER_SANITIZE_SPECIAL_CHARS,
    'email'   => FILTER_VALIDATE_EMAIL,
    'age'     => [
        'filter'  => FILTER_VALIDATE_INT,
        'options' => ['min_range' => 1, 'max_range' => 120],
    ],
    'website' => [
        'filter' => FILTER_VALIDATE_URL,
        'flags'  => FILTER_FLAG_SCHEME_REQUIRED,
    ],
    'active'  => FILTER_VALIDATE_BOOLEAN,
    'score'   => [
        'filter'  => FILTER_VALIDATE_FLOAT,
        'options' => ['min_range' => 0, 'max_range' => 100],
    ],
    'tags'    => [
        'filter' => FILTER_SANITIZE_SPECIAL_CHARS,
        'flags'  => FILTER_REQUIRE_ARRAY, // input is an array
    ],
];

// Process all POST inputs at once
$data = filter_input_array(INPUT_POST, $rules);

// Check the result
if ($data === null) {
    // filter_input_array returns null if the superglobal doesn't exist
    throw new \RuntimeException("No POST data");
}

// $data is now:
// ['name' => 'Budi', 'email' => 'budi@...', 'age' => 28, ...]
// Invalid fields: false
// Missing fields: null

$errors = [];
if ($data['email'] === false) {
    $errors['email'] = "Invalid email format";
}
if ($data['age'] === false) {
    $errors['age'] = "Age must be between 1 and 120";
}
if ($data['website'] === false) {
    $errors['website'] = "Invalid URL";
}

if (empty($errors)) {
    // All valid — process the data
    processRegistration($data);
}

Custom Validation with Callbacks #

For validations not available as built-in filters, use FILTER_CALLBACK:

<?php
// Validate an Indonesian phone number
$validatePhone = function(string $number): string|false {
    // Clean: remove spaces, hyphens, and parentheses
    $clean = preg_replace('/[\s\-\(\)]/', '', $number);

    // Convert +62 to 0
    if (str_starts_with($clean, '+62')) {
        $clean = '0' . substr($clean, 3);
    }

    // Validate: must start with 08, 10-13 digits long
    if (!preg_match('/^08[1-9]\d{7,10}$/', $clean)) {
        return false;
    }

    return $clean; // return the cleaned value
};

$phone  = filter_var("0812-3456-789", FILTER_CALLBACK, ['options' => $validatePhone]);
// "081234567890" — valid and cleaned

$invalid = filter_var("12345", FILTER_CALLBACK, ['options' => $validatePhone]);
// false

// Validate an Indonesian postal code (5 digits)
$validatePostalCode = fn($code) => preg_match('/^\d{5}$/', $code) ? $code : false;
$postalCode = filter_var("12345", FILTER_CALLBACK, ['options' => $validatePostalCode]);

// Validate a NIK (16 digits)
$validateNIK = function(string $nik): string|false {
    $nik = trim($nik);
    return preg_match('/^\d{16}$/', $nik) ? $nik : false;
};

$nik = filter_var("3201234567890001", FILTER_CALLBACK, ['options' => $validateNIK]);

A Complete Form Validation Pattern #

<?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
    {
        $value = trim($this->input[$field] ?? '');
        if ($value === '') {
            $this->errors[$field] = "$label is required";
        } else {
            $this->data[$field] = $value;
        }
        return $this;
    }

    public function email(string $field, string $label): static
    {
        $value = filter_var(trim($this->input[$field] ?? ''), FILTER_VALIDATE_EMAIL);
        if ($value === false) {
            $this->errors[$field] = "$label is invalid";
        } else {
            $this->data[$field] = $value;
        }
        return $this;
    }

    public function integer(string $field, string $label, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): static
    {
        $value = filter_var(
            $this->input[$field] ?? '',
            FILTER_VALIDATE_INT,
            ['options' => ['min_range' => $min, 'max_range' => $max]]
        );
        if ($value === false) {
            $this->errors[$field] = "$label must be a number between $min and $max";
        } else {
            $this->data[$field] = $value;
        }
        return $this;
    }

    public function url(string $field, string $label): static
    {
        $raw     = trim($this->input[$field] ?? '');
        $value   = filter_var($raw, FILTER_VALIDATE_URL);
        $scheme  = parse_url($raw, PHP_URL_SCHEME);

        if ($value === false || !in_array($scheme, ['http', 'https'], true)) {
            $this->errors[$field] = "$label must be a valid http/https URL";
        } else {
            $this->data[$field] = $value;
        }
        return $this;
    }

    public function custom(string $field, string $label, callable $validator): static
    {
        $value = filter_var(
            $this->input[$field] ?? '',
            FILTER_CALLBACK,
            ['options' => $validator]
        );
        if ($value === false) {
            $this->errors[$field] = "$label is invalid";
        } else {
            $this->data[$field] = $value;
        }
        return $this;
    }

    public function isValid(): bool
    {
        return empty($this->errors);
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function getData(): array
    {
        return $this->data;
    }
}

// Usage
$validator = new FormValidator($_POST);
$validator
    ->required('name',     'Name')
    ->email('email',       'Email')
    ->integer('age',       'Age', min: 1, max: 120)
    ->url('website',       'Website')
    ->custom('phone',      'Phone', 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();
saveUser($data);

Common Validation Anti-Patterns #

<?php
// ✗ Anti-pattern 1: accessing superglobals directly without validation
$email = $_POST['email']; // undefined index, not validated, not sanitized

// ✓ Use filter_input + validation
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
    $errors['email'] = "Invalid email";
}

// ✗ Anti-pattern 2: using == false to check validation
$email = filter_var($input, FILTER_VALIDATE_EMAIL);
if ($email == false) { } // "" == false is also true! An empty email passes!

// ✓ Always use === false
if ($email === false) {
    $errors['email'] = "Invalid email";
}

// ✗ Anti-pattern 3: sanitizing only, without validating
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
saveEmail($email); // the result might not be a valid email!

// ✓ Sanitize then validate (or just validate, which is often enough)
$email = filter_var(
    filter_var($_POST['email'], FILTER_SANITIZE_EMAIL),
    FILTER_VALIDATE_EMAIL
);

// ✗ Anti-pattern 4: using FILTER_SANITIZE_STRING (deprecated in PHP 8.1)
$name = filter_var($input, FILTER_SANITIZE_STRING); // deprecated!

// ✓ Use htmlspecialchars for HTML output
$name = htmlspecialchars(trim($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');

// ✗ Anti-pattern 5: assuming filter_var is enough to prevent SQL injection
$id = filter_var($_GET['id'], FILTER_VALIDATE_INT);
$db->query("SELECT * FROM users WHERE id = $id"); // still dangerous!

// ✓ Validation + prepared statements
$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]);

Summary #

  • filter_input() is better than $_POST['name'] — it fetches and filters at once, doesn’t throw warnings when the key is missing, and is more explicit about the data source.
  • === false, not == false — validation filters return the original value (which could be an empty string, 0, or a literal false) or false when invalid. The empty string "" equals false when compared with ==.
  • Sanitization ≠ ValidationFILTER_SANITIZE_EMAIL cleans characters but doesn’t guarantee the result is a valid email. Always validate after sanitizing, or just validate.
  • FILTER_VALIDATE_BOOLEAN is far smarter than casting with (bool) — it recognizes "true", "yes", "on", "1" as true and their opposites as false. Very useful for env variables and configuration.
  • filter_input_array() for validating a form at once — define the rules as an array, process all inputs in one call, get an array with validated values or false/null.
  • FILTER_CALLBACK for custom validation — use a closure as the validator, return the cleaned value if valid or false if not.
  • Filters aren’t a replacement for prepared statements — even after validating an integer with FILTER_VALIDATE_INT, still use prepared statements for database queries. Validation prevents wrong logic, prepared statements prevent SQL injection.
  • FILTER_SANITIZE_STRING is deprecated since PHP 8.1 — use htmlspecialchars() for HTML output escaping, or strip_tags() for removing HTML tags.

← Previous: Math   Next: Advanced Array Functions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact