Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 34 of 54

Validating and escaping form input

Validate form input on the server with filter_var, ctype and simple rules, collect error messages per field, keep entered values, and escape output correctly.

14 minPHP on the webUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonValidate every field of a form on the server, show clear error messages, and escape output safely.

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
<?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));
result
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
<?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:

http
POST /signup.php
Content-Type: application/x-www-form-urlencoded

name=Asha+Verma&email=asha%40&age=9
result
<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:

http
POST /signup.php
Content-Type: application/x-www-form-urlencoded

name=Asha+Verma&email=asha%40example.com&age=29
result
<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 old FILTER_SANITIZE_STRING filter 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;amp; and does not protect SQL or JSON anyway.

Common mistakes

  • Relying on HTML attributes such as required instead of checking on the server.
  • Comparing filter_var() results loosely, so a valid 0 looks 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
<?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";
}
result
'98100 12345': ok
'12345': Phone must be a 10-digit mobile number.
'': ok

Key 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.

Check yourself

1. Why is HTML5 validation (required, type="email") not enough?
2. What does filter_var('abc', FILTER_VALIDATE_INT) return?
3. How do you require an integer between 1 and 10 with filter_var?
4. When should you apply htmlspecialchars()?