Validating Input
Check every value in PHP with filter_var and an allowlist, and collect all the errors instead of stopping at the first.
filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixedValidation is deciding whether input is acceptable before you use it. Everything in $_GET, $_POST and the headers was written by whoever sent the request. The required and type="email" attributes in your HTML help a person using a browser, but a request can be sent with any content at all, so the check that counts is the one in PHP.
filter_var
filter_var() takes a value and a filter constant, and returns the filtered value or false when it does not pass. FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL, FILTER_VALIDATE_INT and FILTER_VALIDATE_FLOAT cover most form fields. FILTER_VALIDATE_INT takes min_range and max_range options, which turns "a whole number from 13 to 120" into one call that also converts the string to an int.
Collect every error
Returning at the first problem makes the visitor submit again once per mistake. Build an array keyed by field name, add a message as each check fails, and treat an empty array as success. That same array then supplies the message printed beside each input when the form is redrawn.
What trips people up
For a field with a fixed set of answers - a plan, a country, a sort order - compare against an allowlist using in_array($value, $allowed, true). A pattern that rejects known-bad values always misses the case nobody thought of.
Example
When to use it
- Reject a sign-up form and show a message under each bad field at once.
- Convert a page number from the query string into a bounded integer.
- Restrict a sort order or plan name to a list your code defines.
- Test a form's rules by calling the validator with a fixed array.
More examples
Filtering a whole form in one call
filter_var_array(array $array, array|int $options = FILTER_DEFAULT, bool $add_empty = true) applies a filter per key in one pass and returns a new array. A field that fails gives false; a field that was never submitted gives null, because $add_empty defaults to true. Only the keys in the definition come back, so unexpected fields in $_POST are dropped rather than carried along. Reach for it when a form has many fields validated the same simple way.
<?php
declare(strict_types=1);
$definition = [
'email' => FILTER_VALIDATE_EMAIL,
'website' => FILTER_VALIDATE_URL,
'age' => [
'filter' => FILTER_VALIDATE_INT,
'options' => ['min_range' => 13, 'max_range' => 120],
],
];
$clean = filter_var_array($_POST, $definition) ?: [];
$errors = [];
foreach (array_keys($definition) as $field) {
$value = $clean[$field] ?? null;
if ($value === null || $value === false) {
$errors[$field] = ucfirst($field) . ' is not valid.';
}
}An allowlist for a fixed set of answers
A select element or a set of radio buttons restricts the choice in the browser, and not at all in the request. Checking membership of a list you control is the whole validation, and the third argument true makes the comparison strict so no type juggling can let something else through. Falling back to a known value means the code after the check always has a usable $plan.
<?php
declare(strict_types=1);
$allowedPlans = ['free', 'pro', 'team'];
$plan = $_POST['plan'] ?? '';
$errors = [];
if (!in_array($plan, $allowedPlans, true)) {
$errors['plan'] = 'Choose one of the listed plans.';
$plan = 'free';
}
$monthlyPrice = match ($plan) {
'free' => 0,
'pro' => 12,
'team' => 40,
};Length, emptiness and the redirect after success
Length limits use mb_strlen() rather than strlen(), because strlen() counts bytes and an accented or non-Latin character takes more than one. The validator is a plain function taking an array, which means it can be called with a hand-built array in a test without a web server anywhere. Error messages are escaped on the way out like any other text that came from a person.
<?php
declare(strict_types=1);
function validateComment(array $input): array
{
$errors = [];
$body = trim($input['body'] ?? '');
$length = mb_strlen($body);
if ($length === 0) {
$errors['body'] = 'Write something first.';
} elseif ($length < 10) {
$errors['body'] = 'At least 10 characters.';
} elseif ($length > 2000) {
$errors['body'] = 'At most 2000 characters.';
}
if (filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'That email address is not valid.';
}
return $errors;
}
$errors = validateComment($_POST);
if ($errors === []) {
// save the comment here
header('Location: /thanks', true, 303);
exit;
}
foreach ($errors as $message) {
echo '<p>' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '</p>';
}
Discussion