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 and false when 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

  1. Create an empty $errors array.
  2. Read each value with a fallback, and trim it.
  3. Test each field. On failure, store a message under the field’s name.
  4. If $errors is 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

  • novalidate switches 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 for htmlspecialchars(), 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 required or 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 $errors array, 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.
Scroll to Top