Never trust input
Everything in $_GET, $_POST, $_COOKIE and $_FILES comes from the visitor's side, and the visitor controls it. Most people will use your form honestly, but a request can be sent by anything: a script, a bot, a tool such as curl, or a modified copy of your form. The browser checks you add with HTML attributes such as required, maxlength and type="email" are helpful for honest users, and worthless as protection. The server must check every value itself, every time.
Validation answers two questions for each field: is it present, and is it acceptable? If not, the visitor needs a clear message and the form again, with what they typed still filled in.
filter_var for common formats
PHP's filter_var() validates the formats you meet most. It returns the cleaned value on success and false on failure:
<?php
var_dump(filter_var("asha@example.com", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("asha@", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("42", FILTER_VALIDATE_INT));
var_dump(filter_var("4.5", FILTER_VALIDATE_INT));
var_dump(filter_var("12", FILTER_VALIDATE_INT, ["options" => ["min_range" => 1, "max_range" => 10]]));
var_dump(filter_var("24.99", FILTER_VALIDATE_FLOAT));
var_dump(filter_var("https://mrhimanshu.com", FILTER_VALIDATE_URL));
var_dump(filter_var("yes", FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));string(16) "asha@example.com"
bool(false)
int(42)
bool(false)
bool(false)
float(24.99)
string(22) "https://mrhimanshu.com"
bool(true)Notice that a valid integer comes back as a real int, not the string "42": validation and conversion in one step. Because 0 is a valid integer, always compare the result with === false. FILTER_VALIDATE_URL accepts any scheme, including javascript:, so for links that will be clicked also check that the URL starts with https:// or http://.
Other quick checks: ctype_digit($v) (only digits), mb_strlen($v) for length limits, in_array($v, $allowed, true) for a choice from a fixed list, and preg_match() for patterns such as PIN codes, from Regular expressions with preg_ functions.
A complete form with validation
Here is a sign-up form that validates three fields, collects messages in an array keyed by field name, and shows them next to the form. On success it shows a confirmation instead:
<?php
function e(?string $v): string { return htmlspecialchars($v ?? '', ENT_QUOTES, 'UTF-8'); }
$values = ['name' => '', 'email' => '', 'age' => ''];
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($values as $field => $_) {
$values[$field] = trim((string) ($_POST[$field] ?? ''));
}
if ($values['name'] === '') {
$errors['name'] = 'Please enter your name.';
} elseif (mb_strlen($values['name']) > 60) {
$errors['name'] = 'Name must be 60 characters or fewer.';
}
if (filter_var($values['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Please enter a valid email address.';
}
$age = filter_var($values['age'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 13, 'max_range' => 120]]);
if ($age === false) {
$errors['age'] = 'Age must be a whole number from 13 to 120.';
}
if (!$errors) {
// Save the account here (see the PDO lessons), then redirect.
echo '<p>Welcome, ' . e($values['name']) . '!</p>';
exit;
}
}
?>
<form method="post" action="/signup.php" novalidate>
<?php foreach (['name' => 'Name', 'email' => 'Email', 'age' => 'Age'] as $field => $label): ?>
<label><?= $label ?> <input name="<?= $field ?>" value="<?= e($values[$field]) ?>"></label>
<?php if (isset($errors[$field])): ?>
<p class="error"><?= e($errors[$field]) ?></p>
<?php endif; ?>
<?php endforeach; ?>
<button>Sign up</button>
</form>A submission with a bad email and an age that is too low:
POST /signup.php
Content-Type: application/x-www-form-urlencoded
name=Asha+Verma&email=asha%40&age=9<form method="post" action="/signup.php" novalidate>
<label>Name <input name="name" value="Asha Verma"></label>
<label>Email <input name="email" value="asha@"></label>
<p class="error">Please enter a valid email address.</p>
<label>Age <input name="age" value="9"></label>
<p class="error">Age must be a whole number from 13 to 120.</p>
<button>Sign up</button>
</form>The name passed and the visitor's typing was kept, so they only fix the two fields that are wrong. Fixing them:
POST /signup.php
Content-Type: application/x-www-form-urlencoded
name=Asha+Verma&email=asha%40example.com&age=29<p>Welcome, Asha Verma!</p>The novalidate attribute switches off the browser's own checks, which is handy while testing your server-side rules. Remove it in production so honest users get instant feedback too.
Validation, sanitising and escaping
Three words often get mixed up:
- Validating checks whether input is acceptable and rejects it if not. This is your main tool.
- Sanitising changes input to make it acceptable, such as
trim()or removing non-digits from a phone number. Use it for harmless clean-up, not to "fix" dangerous input. The oldFILTER_SANITIZE_STRINGfilter was deprecated in PHP 8.1 for exactly that reason. - Escaping prepares a value for the place you are about to put it:
htmlspecialchars()for HTML, prepared statements for SQL (CRUD with prepared statements),urlencode()for URLs,json_encode()for JavaScript.
The rule: validate on the way in, escape on the way out. Store the validated original text (say, Salt & Pepper) in the database, and escape it each time you print it, for the context you print it into. Escaping before saving leads to double-escaped text such as &amp; and does not protect SQL or JSON anyway.
Common mistakes
- Relying on HTML attributes such as
requiredinstead of checking on the server. - Comparing filter_var() results loosely, so a valid
0looks like a failure. - Clearing the form after an error, so visitors retype everything.
- Escaping input before saving it, instead of escaping output.
Exercise
Add a phone field to the rules above: optional, but if given it must be a 10-digit Indian mobile number that starts with 6, 7, 8 or 9, after removing spaces and dashes. Write the check as a function and test it with "98100 12345", "12345" and an empty string.
Show one possible answer
<?php
function phoneError(string $phone): ?string
{
$digits = str_replace([' ', '-'], '', $phone);
if ($digits === '') {
return null; // optional
}
if (preg_match('/^[6-9]\d{9}$/', $digits) !== 1) {
return 'Phone must be a 10-digit mobile number.';
}
return null;
}
foreach (["98100 12345", "12345", ""] as $phone) {
echo "'$phone': " . (phoneError($phone) ?? "ok") . "\n";
}'98100 12345': ok
'12345': Phone must be a 10-digit mobile number.
'': okKey points
- Always validate on the server. Browser checks such as required and type="email" are a convenience, and anyone can bypass them.
- filter_var() validates emails, integers, floats, URLs and booleans: filter_var($v, FILTER_VALIDATE_EMAIL).
- Collect errors in an array keyed by field name, and redisplay the form with the visitor's values and the messages.
- Validate input when it arrives; escape output when you print it. They are two separate jobs.