19. PHP Form Validation
What you will learn
- Why the server must validate
- How to check required, length, number, email, and choice fields
- How to collect and show error messages
- How to keep what the visitor typed
Why validate on the server?
HTML attributes such as required and JavaScript checks are a kindness to visitors, but anyone can bypass them. Only validation in PHP protects your site. Always validate on the server, and treat browser checks as a bonus.
Validate, then sanitise, then escape
| Step | Meaning | Example |
|---|---|---|
| Validate | Is it acceptable? If not, reject it with a message | filter_var($email, FILTER_VALIDATE_EMAIL) |
| Clean | Tidy it: remove stray spaces | trim($name) |
| Escape | Make it safe for where it goes: HTML here | htmlspecialchars() when printing |
Prefer rejecting bad input with a clear message over silently “fixing” it. Escape at the moment of output, and use the right method for each destination (HTML, database, URL).
Useful checks
PHP code
<?php
var_dump(filter_var("sam@example.com", FILTER_VALIDATE_EMAIL) !== false);
var_dump(filter_var("not an email", FILTER_VALIDATE_EMAIL) !== false);
var_dump(filter_var("25", FILTER_VALIDATE_INT, ["options" => ["min_range" => 13, "max_range" => 120]]));
var_dump(filter_var("5", FILTER_VALIDATE_INT, ["options" => ["min_range" => 13, "max_range" => 120]]));
var_dump(filter_var("https://example.com", FILTER_VALIDATE_URL) !== false);
var_dump(in_array("php", ["html", "css", "php"], true));
var_dump(preg_match("/^[A-Za-z0-9_]{4,20}$/", "sam_99"));
What you should see (inside <pre>)
bool(true) bool(false) int(25) bool(false) bool(true) bool(true) int(1)
filter_var()returns the value when valid andfalsewhen not.in_array(..., true)checks a value against an allowed list, the safest way to validate a drop-down or radio choice.preg_match()tests text against a pattern and returns 1 for a match, 0 for none.
FILTER_VALIDATE_EMAIL is a good practical check, but the only real proof that an address works is sending a message to it. Do not reject unusual but valid names or addresses with over-strict patterns.
The standard pattern
- Create an empty
$errorsarray. - Read each value with a fallback, and trim it.
- Test each field. On failure, store a message under the field’s name.
- If
$errorsis still empty, process the form. Otherwise show the form again with the messages and the values the visitor typed.
A complete example
PHP code: signup.php
<?php
function e(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}
$allowed_topics = ["html", "css", "php"];
$errors = [];
$name = $email = $age = $topic = "";
$success = false;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$age = trim($_POST["age"] ?? "");
$topic = $_POST["topic"] ?? "";
if ($name === "") {
$errors["name"] = "Please enter your name.";
} elseif (mb_strlen($name, "UTF-8") > 50) {
$errors["name"] = "Your name must be 50 characters or fewer.";
}
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors["email"] = "Please enter a valid email address.";
}
$age_ok = filter_var($age, FILTER_VALIDATE_INT, [
"options" => ["min_range" => 13, "max_range" => 120],
]);
if ($age_ok === false) {
$errors["age"] = "Age must be a whole number from 13 to 120.";
}
if (!in_array($topic, $allowed_topics, true)) {
$errors["topic"] = "Please choose one of the topics.";
}
if (!$errors) {
// Here you would save the data or send an email.
$success = true;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sign up</title>
</head>
<body>
<?php if ($success): ?>
<p>Thank you, <?= e($name) ?>! You are signed up.</p>
<?php else: ?>
<?php if ($errors): ?>
<p role="alert">Please fix the problems below.</p>
<?php endif; ?>
<form method="post" novalidate>
<p>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" value="<?= e($name) ?>">
<?php if (isset($errors["name"])): ?><br><strong><?= e($errors["name"]) ?></strong><?php endif; ?>
</p>
<p>
<label for="email">Email</label><br>
<input type="email" id="email" name="email" value="<?= e($email) ?>">
<?php if (isset($errors["email"])): ?><br><strong><?= e($errors["email"]) ?></strong><?php endif; ?>
</p>
<p>
<label for="age">Age (13 to 120)</label><br>
<input type="text" id="age" name="age" value="<?= e($age) ?>">
<?php if (isset($errors["age"])): ?><br><strong><?= e($errors["age"]) ?></strong><?php endif; ?>
</p>
<p>
<label for="topic">Favourite topic</label><br>
<select id="topic" name="topic">
<option value="">Choose...</option>
<?php foreach ($allowed_topics as $t): ?>
<option value="<?= e($t) ?>"<?= $topic === $t ? " selected" : "" ?>><?= e(strtoupper($t)) ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($errors["topic"])): ?><br><strong><?= e($errors["topic"]) ?></strong><?php endif; ?>
</p>
<button type="submit">Sign up</button>
</form>
<?php endif; ?>
</body>
</html>
What happens when you submit the form with nothing filled in
The text “Please fix the problems below.” appears above the form, and four messages appear, one under each field:
- Please enter your name.
- Please enter a valid email address.
- Age must be a whole number from 13 to 120.
- Please choose one of the topics.
When every field is valid, the form is replaced by: Thank you, Sam! You are signed up.
What to notice
novalidateswitches off the browser’s pop-ups so you can see your own PHP messages. In a real form you could keep the browser checks too.- Every value goes back into the form through
e(), so the visitor does not retype it, and it is escaped. - The topic is checked against an allowed list, so a forged value is rejected.
- The helper
e()is a short name forhtmlspecialchars(), a common habit. - Messages are linked to their fields by position. For better accessibility, also connect each message with
aria-describedby.
Common mistakes
- Relying only on
requiredor JavaScript. - Rejecting valid input with an over-strict pattern.
- Showing a vague “something went wrong” instead of saying which field and how to fix it.
- Clearing the form when there are errors, so the visitor must start again.
- Escaping on input instead of on output, which stores damaged text.
- Forgetting to validate a drop-down. Its values can be forged.
Exercise
Write the validation code for a field username: it is required, 4 to 20 characters, and may contain only letters, numbers, and underscores. Store a clear message in $errors["username"] when it fails.
Show solution
<?php
$errors = [];
$username = trim($_POST["username"] ?? "");
if ($username === "") {
$errors["username"] = "Please choose a username.";
} elseif (!preg_match("/^[A-Za-z0-9_]{4,20}$/", $username)) {
$errors["username"] = "Use 4 to 20 letters, numbers, or underscores.";
}
Knowledge check
1. Why must validation happen on the server?
Browser checks can be bypassed, so only server checks protect the site.
2. How should you validate a choice from a drop-down?
Check it against an allowed list with in_array(..., true).
3. Why put the visitor’s values back into the form?
So they do not have to retype everything after an error.
Summary
- Validate every field in PHP, collect messages in an
$errorsarray, and show them clearly. - Use
filter_var, allowed lists, and length checks. Reject rather than silently fix. - Keep the typed values, and escape them whenever you print them.