People make mistakes when they fill in forms: they skip a required field, mistype their email address or enter "abc" as their age. Some visitors, and plenty of automated bots, will also send deliberately strange or harmful data. Before your code saves anything or sends any email, it has to check that the input makes sense. That's validation.
HTML can do some checking in the browser, with attributes like required and type="email", and that's great for giving quick feedback. But anyone can bypass the browser and send a request directly, so server-side validation in PHP is essential. The browser's checks are for convenience; PHP's checks are for safety.
In this lesson you'll check required fields, emails, numbers and lengths, collect friendly error messages and show them next to the right fields, keep the form sticky, and protect it against cross-site request forgery with a token.
Collecting errors in an array
A simple, reliable pattern is to read every field, check each rule, and store a message in an $errors array keyed by field name:
$errors = [];
$name = trim($_POST['name'] ?? '');
if ($name === '') {
$errors['name'] = 'Please enter your name.';
} elseif (mb_strlen($name) > 50) {
$errors['name'] = 'Name must be 50 characters or fewer.';
}
if (!$errors) {
// everything is valid: save, send, redirect
}An empty array counts as false, so if (!$errors) means "no problems were found". Keying errors by field makes it easy to show each message beside the right input.
Validating with filter_var
PHP's filter_var checks common formats for you:
| Check | Code | Returns |
|---|---|---|
| Email address | filter_var($email, FILTER_VALIDATE_EMAIL) | The email, or false |
| Whole number | filter_var($age, FILTER_VALIDATE_INT) | The int, or false |
| Number in a range | filter_var($age, FILTER_VALIDATE_INT, ['options' => ['min_range' => 13, 'max_range' => 120]]) | The int, or false |
| Web address | filter_var($url, FILTER_VALIDATE_URL) | The URL, or false |
Because a valid value like 0 is also "falsy", always compare the result with === false rather than using it directly in an if.
Showing errors and keeping input
Show each error next to its field and keep what the visitor typed:
<input name="email" value="<?= htmlspecialchars($email) ?>">
<?php if (isset($errors['email'])): ?>
<p class="error"><?= $errors['email'] ?></p>
<?php endif; ?>Good error messages say what's wrong and how to fix it: "Please enter an email address like name@example.com" is far more helpful than "Invalid input". For select boxes and radio buttons, check the submitted value against the list of allowed options, never just trust it.
CSRF tokens
Cross-site request forgery (CSRF) tricks a logged-in visitor's browser into submitting one of your forms from another website. The standard defence is a secret token stored in the visitor's session and included as a hidden field:
session_start();
$_SESSION['token'] ??= bin2hex(random_bytes(32));<input type="hidden" name="token" value="<?= $_SESSION['token'] ?>">When the form is submitted, compare the two with hash_equals($_SESSION['token'], $_POST['token'] ?? ''). A forged request from another site doesn't know the token, so it fails. You'll learn more about sessions in the next lesson.
PHP Form Validation Syntax
Validation pattern
$errors = [];
$email = trim($_POST['email'] ?? '');
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Please enter a valid email address.';
}
if (!$errors) {
// safe to continue
}PHP Form Validation Summary
- Always validate on the server; browser checks can be bypassed.
- Collect messages in an
$errorsarray keyed by field, and only continue when it's empty. filter_varvalidates emails, integers, ranges and URLs; compare its result with=== false.- Show helpful messages next to fields and keep the visitor's input.
- Protect forms that change data with a CSRF token checked by
hash_equals.
PHP Form Validation Examples
Each example shows the code, the output it produces and what each part does.
Validating a sign-up form
The visitor submitted name "", email sam@example and age 9.
<?php
$errors = [];
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$age = $_POST['age'] ?? '';
if ($name === '') {
$errors['name'] = 'Please enter your name.';
}
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Please enter an email address like name@example.com.';
}
$ageNumber = filter_var($age, FILTER_VALIDATE_INT, ['options' => ['min_range' => 13, 'max_range' => 120]]);
if ($ageNumber === false) {
$errors['age'] = 'You must be between 13 and 120 to join.';
}
foreach ($errors as $field => $message) {
echo "$field: $message\n";
}
echo $errors ? count($errors) . " problem(s) to fix" : "All good!";name: Please enter your name.
email: Please enter an email address like name@example.com.
age: You must be between 13 and 120 to join.
3 problem(s) to fixLine-by-line explanation
- Lines 7 to 9An empty name fails the required check.
- Lines 10 to 12"sam@example" has no top-level domain such as .com, so filter_var rejects it.
- Lines 13 to 169 is a whole number but outside the allowed range, so the result is false.
- Line 21A non-empty array counts as true.
A sticky field with its error message
<?php
$email = 'sam@example';
$errors = ['email' => 'Please enter an email address like name@example.com.'];
?>
<label>Email
<input name="email" value="<?= htmlspecialchars($email) ?>"
<?= isset($errors['email']) ? 'aria-invalid="true"' : '' ?>>
</label>
<?php if (isset($errors['email'])): ?>
<p class="error"><?= htmlspecialchars($errors['email']) ?></p>
<?php endif; ?><label>Email
<input name="email" value="sam@example"
aria-invalid="true">
</label>
<p class="error">Please enter an email address like name@example.com.</p>Line-by-line explanation
- Line 6The visitor's text stays in the box, so they only need to fix it.
- Line 7aria-invalid tells screen readers the field has a problem.
- Lines 9 to 11The message appears only when there's an error for this field.
Checking a CSRF token
<?php
session_start();
$_SESSION['token'] ??= bin2hex(random_bytes(32));
function tokenIsValid(): bool {
return hash_equals($_SESSION['token'], $_POST['token'] ?? '');
}
$_POST['token'] = $_SESSION['token']; // a genuine submission
var_dump(tokenIsValid());
$_POST['token'] = 'forged-value'; // a forged one
var_dump(tokenIsValid());bool(true)
bool(false)Line-by-line explanation
- Line 3Create a random token once per session. ??= only assigns if it isn't set yet.
- Line 6hash_equals compares safely, without leaking timing information.
- Line 9Simulates a genuine submission that includes the real token.
- Line 12Simulates a forged request that doesn't know the token.
Common PHP Form Validation Mistakes
Relying only on HTML validation
<input name="age" type="number" min="13" required>
<!-- and no checks in PHP -->$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, ['options' => ['min_range' => 13]]);
if ($age === false) { $errors['age'] = 'You must be 13 or older.'; }Treating a valid 0 as a failure
if (!filter_var($qty, FILTER_VALIDATE_INT)) { ... } // "0" counts as invalidif (filter_var($qty, FILTER_VALIDATE_INT) === false) { ... }Vague error messages
$errors['password'] = 'Invalid.';$errors['password'] = 'Use at least 8 characters, including a number.';PHP Form Validation in Real Projects
Validation in practice
Frameworks provide validation rules such as required|email|max:255 in Laravel, but they do exactly what you did here: check each field, collect messages and send the visitor back with their input preserved. CSRF tokens are built into every major PHP framework and into WordPress, where they're called nonces.
PHP Form Validation FAQ
How do I validate an email address in PHP?
Use filter_var($email, FILTER_VALIDATE_EMAIL), which returns the address or false. It checks the format only; it can't tell whether the mailbox exists or belongs to the visitor. When ownership matters, such as for sign-ups, send a confirmation link to the address.
How do I check that a number is within a range?
Pass range options to the integer filter: filter_var($age, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]). It returns the number as an int when valid and false otherwise, so "17" fails a range of 1 to 12. Compare the result with === false, because 0 can be a valid value.
How do I validate a date typed into a form?
Parse it with DateTimeImmutable::createFromFormat('!d/m/Y', $text) and then check DateTimeImmutable::getLastErrors(). PHP quietly rolls an impossible date such as 31/02/2026 over to 3 March and only records a warning, so reject the input if any warnings or errors are reported.
Should I clean input before saving it to the database?
Normalise it, for example by trimming spaces and lowercasing email addresses, and reject values that break your rules. Don't strip or escape HTML at this point. Store the real value, use prepared statements to save it safely, and escape it with htmlspecialchars() when you display it.
Where should validation code live?
Keep each form's rules in one function or class that receives the submitted data and returns an array of errors. The page then only decides whether to show the errors or continue. That separation makes the rules easy to test and to reuse, for example in both a web form and an API.
PHP Form Validation Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the check for a valid email address.
if (filter_var($email, ) === ) {
$errors['email'] = 'Invalid email';
}
Why is server-side validation essential?
What does this output?
var_dump(filter_var("42", FILTER_VALIDATE_INT), filter_var("4.2", FILTER_VALIDATE_INT));
What does if (!$errors) mean when $errors is an array?
What does a CSRF token protect against?
PHP Form Validation Quiz
5 questions to check what you learned.
Where should validation happen for security?
- On the server, in PHP (browser checks are optional extras)
- Only in the browser
- Only in the database
- Nowhere
Why compare filter_var's result with
=== false?- filter_var always returns a string
- false is the success value
- It's faster
- A valid value like 0 is falsy, so a plain if would treat it as invalid
What makes a good error message?
- "Error 422"
- It's as short as possible
- It explains what's wrong and how to fix it
- It shows the PHP code
What's the difference between validating and escaping?
- They're the same
- Validation checks input when it arrives; escaping makes output safe when it's displayed
- Escaping happens first
- Validation is only for numbers
Which function compares a submitted CSRF token with the stored one safely?
- hash_equals
- ==
- strcmp
- password_verify