Filters & filter_var()
PHP has a built-in filter extension for checking and cleaning data from outside your program: form fields, query strings, cookies, API payloads. One function, filter_var(), can check that a value is a whole number in a range, a well-formed email address, a URL, an IP address or a yes/no answer, and it converts the value to the right type while doing so.
Form Validation & Escaping showed the basic use of filter_var() and filter_input(). This lesson is the full tour: the difference between validating and sanitizing, how flags and options work, the URL trap that catches many developers, your own rules with FILTER_CALLBACK, and checking a whole form in one call.
Validate or sanitize?
The extension has two families of filters, and they do very different jobs:
| Validate filters | Sanitize filters | |
|---|---|---|
| Names | FILTER_VALIDATE_INT, FILTER_VALIDATE_EMAIL... | FILTER_SANITIZE_NUMBER_INT, FILTER_SANITIZE_EMAIL... |
| Question they answer | "Is this value acceptable?" | "What is left after removing bad characters?" |
| Result for bad input | false (or null with a flag) | A changed string, never false for a string |
| Type of result | Converted: int, float, bool or string | Always a string |
The safe habit is: validate input, escape output. Validation rejects bad data and tells the user. Sanitizing quietly changes what the user typed, which can turn nonsense into something that looks valid. Use sanitize filters only when you truly want to strip characters, for example removing spaces and dashes from a phone number before you validate it.
Flags and options
filter_var() takes up to three arguments: the value, the filter, and either a flag (a constant that switches on an extra behaviour) or an array with 'options' and 'flags' keys. Options are settings with values, such as a minimum and maximum:
<?php
declare(strict_types=1);
// Range checks with options
$age = ['options' => ['min_range' => 18, 'max_range' => 60]];
var_dump(filter_var('25', FILTER_VALIDATE_INT, $age));
var_dump(filter_var('16', FILTER_VALIDATE_INT, $age));
// A default instead of false
var_dump(filter_var('abc', FILTER_VALIDATE_INT, ['options' => ['default' => 1]]));
// Whitespace is fine, leading zeros are not
var_dump(filter_var(' 42 ', FILTER_VALIDATE_INT));
var_dump(filter_var('042', FILTER_VALIDATE_INT));
// A flag on its own: accept hexadecimal
var_dump(filter_var('0x1A', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX));
// Floats: thousands separators and a custom decimal mark
var_dump(filter_var('1,299.50', FILTER_VALIDATE_FLOAT));
var_dump(filter_var('1,299.50', FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND));
var_dump(filter_var('12,5', FILTER_VALIDATE_FLOAT, ['options' => ['decimal' => ',']]));int(25) bool(false) int(1) int(42) bool(false) int(26) bool(false) float(1299.5) float(12.5)
Flags can be combined with |, as in FILTER_FLAG_ALLOW_HEX | FILTER_FLAG_ALLOW_OCTAL. Inside an options array, write them under the 'flags' key. Notice that '042' is rejected: FILTER_VALIDATE_INT does not allow a leading zero, so check PIN codes and phone numbers as text with a pattern, not as integers.
Booleans: three possible answers
Checkboxes and settings arrive as text like 'on', 'yes' or '1'. FILTER_VALIDATE_BOOL (a shorter name for FILTER_VALIDATE_BOOLEAN, added in PHP 8.0) understands them. Without a flag it returns false for anything it does not recognise, which hides typos. With FILTER_NULL_ON_FAILURE, unknown values become null:
<?php
declare(strict_types=1);
foreach (['yes', 'On', '1', 'true', 'no', 'off', '0', '', 'maybe'] as $raw) {
$plain = filter_var($raw, FILTER_VALIDATE_BOOL);
$strict = filter_var($raw, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
printf("%-7s plain: %-5s with NULL_ON_FAILURE: %s\n", "'$raw'", var_export($plain, true), var_export($strict, true));
}'yes' plain: true with NULL_ON_FAILURE: true 'On' plain: true with NULL_ON_FAILURE: true '1' plain: true with NULL_ON_FAILURE: true 'true' plain: true with NULL_ON_FAILURE: true 'no' plain: false with NULL_ON_FAILURE: false 'off' plain: false with NULL_ON_FAILURE: false '0' plain: false with NULL_ON_FAILURE: false '' plain: false with NULL_ON_FAILURE: false 'maybe' plain: false with NULL_ON_FAILURE: NULL
Matching ignores upper and lower case, and the empty string counts as "no", which suits an unticked checkbox.
Emails, URLs and other formats
FILTER_VALIDATE_EMAIL checks the format of an address only; the mailbox may not exist. FILTER_VALIDATE_URL checks the general shape of a URL, and that is a trap: it accepts any scheme, including ftp: and javascript:. If you print a user's "website" as a link, a javascript: URL can run code in your visitors' browsers. Always check the scheme yourself:
<?php
declare(strict_types=1);
function safeWebsite(string $raw): ?string
{
$url = filter_var(trim($raw), FILTER_VALIDATE_URL);
if ($url === false) {
return null;
}
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
return in_array($scheme, ['http', 'https'], true) ? $url : null;
}
$tests = ['https://meera-sweets.in/menu', 'meera-sweets.in', 'ftp://files.example.in/a.zip', 'javascript://x%0Aalert(1)'];
foreach ($tests as $raw) {
$raw_ok = filter_var($raw, FILTER_VALIDATE_URL) !== false ? 'valid' : 'invalid';
printf("%-30s filter: %-8s safe: %s\n", $raw, $raw_ok, safeWebsite($raw) ?? 'rejected');
}https://meera-sweets.in/menu filter: valid safe: https://meera-sweets.in/menu meera-sweets.in filter: invalid safe: rejected ftp://files.example.in/a.zip filter: valid safe: rejected javascript://x%0Aalert(1) filter: valid safe: rejected
A URL without a scheme, such as meera-sweets.in, is invalid. You can ask for more with FILTER_FLAG_PATH_REQUIRED or FILTER_FLAG_QUERY_REQUIRED. Other useful validators:
FILTER_VALIDATE_IPchecks IPv4 and IPv6 addresses. AddFILTER_FLAG_IPV4orFILTER_FLAG_IPV6to allow only one kind,FILTER_FLAG_NO_PRIV_RANGEto reject private addresses such as192.168.1.10, orFILTER_FLAG_GLOBAL_RANGE(PHP 8.2) to allow only public addresses.FILTER_VALIDATE_DOMAINwithFILTER_FLAG_HOSTNAMEchecks a host name such asshop-1.example.in.FILTER_VALIDATE_REGEXPchecks a value against your own pattern, given as the'regexp'option. For a six-digit Indian PIN code that cannot start with 0:['options' => ['regexp' => '/^[1-9][0-9]{5}$/']]. Patterns are explained in Regular Expressions.
Sanitizing filters
Sanitize filters remove or encode characters. They never say "invalid", so look carefully at what they return:
<?php
declare(strict_types=1);
var_dump(filter_var('+91 98765-43210', FILTER_SANITIZE_NUMBER_INT));
var_dump(filter_var('₹1,299.50', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION));
var_dump(filter_var('12abc34', FILTER_SANITIZE_NUMBER_INT));
var_dump(filter_var(' meera (at) example.in ', FILTER_SANITIZE_EMAIL));
var_dump(filter_var('<b>Rohan & Sons</b>', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
var_dump(filter_var("Line1\x07Bell", FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW));string(14) "+9198765-43210" string(7) "1299.50" string(4) "1234" string(17) "meeraatexample.in" string(35) "<b>Rohan & Sons</b>" string(9) "Line1Bell"
Look at the results. FILTER_SANITIZE_NUMBER_INT keeps plus and minus signs, so the phone number is still not clean. '12abc34' silently became '1234', a number the user never typed. The "email" is now a string that is not an email at all. Sanitizing is not validation: if you sanitize, validate the result afterwards.
FILTER_SANITIZE_FULL_SPECIAL_CHARS encodes HTML like htmlspecialchars() does, but do this when you print a value, not when you store it. FILTER_UNSAFE_RAW (also called FILTER_DEFAULT) does nothing by default; with FILTER_FLAG_STRIP_LOW it removes invisible control characters such as the bell character \x07 above.
Your own rules with FILTER_CALLBACK
When no built-in filter fits, pass your own function with FILTER_CALLBACK. The callable goes in 'options', receives the value as a string, and whatever it returns is the result. If you give it an array, it runs on every item:
<?php
declare(strict_types=1);
function normaliseMobile(string $raw): string|false
{
$digits = preg_replace('/\D/', '', $raw); // keep digits only
$digits = match (true) {
strlen($digits) === 12 && str_starts_with($digits, '91') => substr($digits, 2),
strlen($digits) === 11 && str_starts_with($digits, '0') => substr($digits, 1),
default => $digits,
};
return preg_match('/^[6-9][0-9]{9}$/', $digits) ? $digits : false;
}
foreach (['+91 98765-43210', '098765 43210', '9876543210', '12345'] as $raw) {
$result = filter_var($raw, FILTER_CALLBACK, ['options' => 'normaliseMobile']);
printf("%-16s => %s\n", $raw, var_export($result, true));
}
$tags = filter_var([' php ', 'MySQL', ' laravel'], FILTER_CALLBACK, [
'options' => fn(string $t): string => strtolower(trim($t)),
]);
print_r($tags);+91 98765-43210 => '9876543210'
098765 43210 => '9876543210'
9876543210 => '9876543210'
12345 => false
Array
(
[0] => php
[1] => mysql
[2] => laravel
)The mobile number check first removes everything except digits, then drops a 91 country code or a leading 0, and finally insists on ten digits starting with 6, 7, 8 or 9. Any callable works here, including the closures from Closures & Callables.
Checking a whole form at once
filter_var_array() checks many fields in one call. You describe each field with a filter, or with an array holding 'filter', 'options' and 'flags'. Its sister filter_input_array(INPUT_POST, $rules) reads straight from the request. Fields that are missing come back as null, and fields you did not list are dropped, which protects you from extra fields such as a sneaky is_admin:
<?php
declare(strict_types=1);
// Pretend this came from $_POST
$post = [
'name' => ' Kabir Mehta ',
'email' => 'kabir@example.in',
'age' => '17',
'newsletter' => 'on',
'website' => 'htps:/kabir',
'courses' => ['3', '8', 'x'],
'is_admin' => '1', // a field we never asked for
];
$rules = [
'name' => ['filter' => FILTER_CALLBACK, 'options' => 'trim'],
'email' => FILTER_VALIDATE_EMAIL,
'age' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 18, 'max_range' => 60]],
'newsletter' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'website' => FILTER_VALIDATE_URL,
'courses' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY],
'city' => FILTER_DEFAULT,
];
$data = filter_var_array($post, $rules);
foreach ($data as $field => $value) {
printf("%-10s %s\n", $field, json_encode($value));
}name "Kabir Mehta" email "kabir@example.in" age false newsletter true website false courses [3,8,false] city null
Now each field has three possible states: a clean value, false (sent but invalid) or null (not sent). FILTER_REQUIRE_ARRAY says the field must be an array, such as checkboxes named courses[], and checks every item. Its opposite, FILTER_REQUIRE_SCALAR, rejects arrays where you expect one value, and FILTER_FORCE_ARRAY wraps a single value in an array. From here, building error messages is a simple loop: for each false or null, add a message for that field.
Common mistakes
- Checking the result with
!.if (!$age)also rejects a valid0orfalse. Compare with=== falseand=== null. - Trusting
FILTER_VALIDATE_URLfor links. It acceptsjavascript:andftp:. Check the scheme too. - Sanitizing instead of validating.
FILTER_SANITIZE_NUMBER_INTturns'12abc34'into'1234'. Validate, and show an error. - Validating PIN codes or phone numbers as integers. Leading zeros are rejected and large numbers lose meaning. Treat them as text with a pattern.
- Encoding HTML when saving. Store the clean value; escape with
htmlspecialchars()when you print it.
Key points
- Validate filters return the converted value or
false; sanitize filters always return a changed string. - The third argument is a flag, or an array with
'options'(such asmin_range,default,regexp) and'flags'. FILTER_VALIDATE_BOOLwithFILTER_NULL_ON_FAILUREseparates "no" from "not understood".FILTER_VALIDATE_URLaccepts any scheme; allow onlyhttpandhttpsfor links.FILTER_CALLBACKruns your own function;filter_var_array()andfilter_input_array()check whole forms and drop unknown fields.FILTER_SANITIZE_STRINGis deprecated; PHP 8.5 addsFILTER_THROW_ON_FAILURE.
Exercise
A delivery form sends pin, qty, express and email. Write $rules for filter_var_array() so that pin must match a six-digit PIN code not starting with 0, qty must be an integer from 1 to 20, express is a boolean that gives null when not understood, and email must be a valid email. Run it on ['pin' => '302001', 'qty' => '25', 'express' => 'yes', 'email' => 'isha@example'] and print one line per field showing OK with the value, or invalid.
Show solution
<?php
declare(strict_types=1);
$rules = [
'pin' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[1-9][0-9]{5}$/']],
'qty' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 20]],
'express' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'email' => FILTER_VALIDATE_EMAIL,
];
$input = ['pin' => '302001', 'qty' => '25', 'express' => 'yes', 'email' => 'isha@example'];
foreach (filter_var_array($input, $rules) as $field => $value) {
$ok = $value !== false && $value !== null;
echo $field, ': ', $ok ? 'OK ' . var_export($value, true) : 'invalid', "\n";
}pin: OK '302001' qty: invalid express: OK true email: invalid
qty fails the range check and isha@example has no domain ending. For express, a false result would mean "no", so in a real form you would treat only null as invalid for that field.
Quick Quiz
1. What does filter_var('12abc34', FILTER_SANITIZE_NUMBER_INT) return?
Sanitize filters remove characters and never report an error, so the letters simply disappear.
2. Which value does filter_var('maybe', FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) return?
With FILTER_NULL_ON_FAILURE, values that are not a recognised yes or no become null.
3. Why check the scheme after FILTER_VALIDATE_URL?
The URL filter checks the general shape; you must allow only http and https for links yourself.
4. In filter_var_array(), what happens to an input field that is not in your rules?
Only the fields you describe are returned, which keeps unexpected fields out of your data.
Learn PHP with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
