PHP Form Validation
What Is Form Validation? In simple terms: Validation checks whether submitted data is acceptable before your application uses it — for example, that an email address looks like an email address, an ag
What Is Form Validation?
In simple terms: Validation checks whether submitted data is acceptable before your application uses it — for example, that an email address looks like an email address, an age is a whole number in a sensible range, and a password is long enough.
Technically: Validation applies rules to each input value and either accepts it (often converting it to the right type) or rejects it with an error message. In PHP, validation is built from comparisons, string functions, regular expressions, and the filter extension (filter_var() and related functions).
Why Server-Side Validation Is Mandatory
HTML attributes such as required, type="email", min, and maxlength improve the user experience, but they run in the browser, which the user controls. Anyone can remove them in developer tools or send requests directly with tools such as curl. Only validation on the server protects your data:
| Client-Side (HTML/JavaScript) | Server-Side (PHP) |
|---|---|
| Instant feedback while typing | Runs after submission |
| Can be bypassed | Cannot be bypassed |
| Optional, for convenience | Required, for correctness and security |
Kinds of Validation Rules
| Rule Type | Example | Typical Tool |
|---|---|---|
| Presence | Name must not be empty | trim($value) !== '' (see PHP Form Required) |
| Type | Age must be an integer | filter_var(..., FILTER_VALIDATE_INT) |
| Format | Email, URL, PIN code | FILTER_VALIDATE_EMAIL, preg_match() |
| Length | Password at least 8 characters | mb_strlen() |
| Range | Rating from 1 to 5 | Filter options min_range/max_range |
| Allowed values | Plan is free or pro | in_array(..., true) |
| Consistency | Password confirmation matches | === comparison |
| Uniqueness | Email not already registered | Database query |
The Filter Extension: filter_var()
Syntax
filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed
Validation filters return the converted value on success and false on failure.
Validating Integers
<?php
var_dump(filter_var('42', FILTER_VALIDATE_INT));
var_dump(filter_var(' 42 ', FILTER_VALIDATE_INT));
var_dump(filter_var('42abc', FILTER_VALIDATE_INT));
var_dump(filter_var('4.2', FILTER_VALIDATE_INT));
var_dump(filter_var('150', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]));
Output
int(42)
int(42)
bool(false)
bool(false)
bool(false)
- A valid value is returned as a real
int, ready to use. - Leading and trailing whitespace is ignored.
- Partial numbers (
42abc) and decimals are rejected — unlike(int), which would silently convert'42abc'to42. min_rangeandmax_rangeenforce a range in the same step.
Watch out for
0: A valid input of'0'returnsint(0), which is falsy. Always compare with=== false, never withif (!$result).
Validating Floats and Booleans
<?php
var_dump(filter_var('3.14', FILTER_VALIDATE_FLOAT));
var_dump(filter_var('3,14', FILTER_VALIDATE_FLOAT, ['options' => ['decimal' => ',']]));
var_dump(filter_var('yes', FILTER_VALIDATE_BOOL));
var_dump(filter_var('off', FILTER_VALIDATE_BOOL));
var_dump(filter_var('maybe', FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE));
Output
float(3.14)
float(3.14)
bool(true)
bool(false)
NULL
- The
decimaloption accepts a comma as the decimal separator. FILTER_VALIDATE_BOOL(an alias ofFILTER_VALIDATE_BOOLEAN, PHP 8.0+) accepts1,true,on,yesastrueand0,false,off,no,''asfalse.- Because
falseis both a valid result and the failure value, addFILTER_NULL_ON_FAILUREto getnullfor invalid input such as'maybe'.
Other Validation Filters
| Filter | Validates |
|---|---|
FILTER_VALIDATE_EMAIL | Email addresses (see PHP Form URL/E-mail) |
FILTER_VALIDATE_URL | URLs |
FILTER_VALIDATE_IP | IPv4/IPv6 addresses (flags for private/reserved ranges) |
FILTER_VALIDATE_DOMAIN | Domain names (with FILTER_FLAG_HOSTNAME) |
FILTER_VALIDATE_MAC | MAC addresses |
FILTER_VALIDATE_REGEXP | A custom regular expression ('options' => ['regexp' => '/.../']) |
Validating Many Fields at Once: filter_var_array()
<?php
$input = ['age' => '25', 'email' => 'asha@example', 'newsletter' => 'yes', 'rating' => '7'];
$definition = [
'age' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 13, 'max_range' => 120]],
'email' => FILTER_VALIDATE_EMAIL,
'newsletter' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 5]],
];
var_dump(filter_var_array($input, $definition));
Output
array(4) {
["age"]=>
int(25)
["email"]=>
bool(false)
["newsletter"]=>
bool(true)
["rating"]=>
bool(false)
}
Each field gets its own filter and options. Fields listed in the definition but missing from the input are returned as null, which lets you distinguish "missing" (null) from "invalid" (false). filter_input_array(INPUT_POST, $definition) does the same directly on the request data.
Building a Reusable Validator
Real forms combine several rules per field and need readable error messages. A small rule-based validator keeps form code short and consistent:
<?php
function validate(array $input, array $rules): array
{
$errors = [];
foreach ($rules as $field => $fieldRules) {
$raw = $input[$field] ?? '';
$value = is_string($raw) ? trim($raw) : '';
$label = str_replace('_', ' ', $field);
foreach ($fieldRules as $rule) {
[$name, $a, $b] = array_pad(explode(':', $rule), 3, null);
$error = match ($name) {
'required' => $value === '' ? "The $label field is required." : null,
'email' => $value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) === false
? "The $label must be a valid email address." : null,
'min' => $value !== '' && mb_strlen($value) < (int) $a
? "The $label must be at least $a characters." : null,
'max' => mb_strlen($value) > (int) $a
? "The $label may not be longer than $a characters." : null,
'int' => $value !== '' && filter_var($value, FILTER_VALIDATE_INT, [
'options' => ['min_range' => (int) $a, 'max_range' => (int) $b],
]) === false
? "The $label must be a whole number between $a and $b." : null,
'same' => $value !== (is_string($input[$a] ?? null) ? trim($input[$a]) : '')
? "The $label must match the $a." : null,
default => throw new InvalidArgumentException("Unknown rule: $name"),
};
if ($error !== null) {
$errors[$field] = $error;
break;
}
}
}
return $errors;
}
$rules = [
'name' => ['required', 'max:50'],
'email' => ['required', 'email'],
'age' => ['required', 'int:18:100'],
'password' => ['required', 'min:8'],
'password_confirm' => ['required', 'same:password'],
];
print_r(validate([
'name' => '',
'email' => 'asha@',
'age' => '15',
'password' => 'short',
'password_confirm' => 'shorter',
], $rules));
$valid = validate([
'name' => 'Asha',
'email' => 'asha@example.com',
'age' => '24',
'password' => 'Secure#2026',
'password_confirm' => 'Secure#2026',
], $rules);
echo $valid === [] ? "No errors\n" : "Errors found\n";
Output
Array
(
[name] => The name field is required.
[email] => The email must be a valid email address.
[age] => The age must be a whole number between 18 and 100.
[password] => The password must be at least 8 characters.
[password_confirm] => The password confirm must match the password.
)
No errors
How It Works
- Rules are written as short strings (
'int:18:100');explode(':', ...)splits the rule name from its parameters, andarray_pad()guarantees three elements for destructuring. - Every value is read safely: non-string input (for example, an array sent as
name[]) is treated as empty. - A
matchexpression maps each rule to its check. Most rules skip empty values, sorequiredalone decides whether a field may be empty — optional fields can still have format rules. - Only the first failing rule per field is reported (
break), giving one clear message per field. - Unknown rule names throw an exception, so typos in rule definitions are noticed immediately.
Note: This simplified validator trims every value. In a real application, do not trim passwords — leading or trailing spaces may be intentional. Frameworks provide complete validators (Laravel's
$request->validate(), Symfony Validator) built on the same ideas.
Where Validation Fits
read input → validate → (if errors) re-display form with messages
→ (if valid) process with the validated, typed values
- Validate all fields and show all errors together.
- Use the converted values from validation (for example, the integer returned by
filter_var()) for further processing. - Validation does not replace output escaping (
htmlspecialchars()) or prepared statements for SQL; each protects a different step.
Common Mistakes
Relying on HTML Validation
Effect: Invalid or malicious data reaches the server.
Fix: Validate every field in PHP.
Using Casts Instead of Validation
Effect: (int) 'abc' silently becomes 0; (int) '42abc' becomes 42.
Fix: filter_var(..., FILTER_VALIDATE_INT) and check for false.
Checking Filter Results Loosely
Effect: Valid values 0 or false are treated as failures.
Fix: Compare with === false (or use FILTER_NULL_ON_FAILURE for booleans).
Validating Without Allow-Lists
Effect: Unexpected values for plans, roles, or sort orders are accepted.
Fix: in_array($value, $allowed, true).
Stopping at the First Error
Effect: Users fix one problem, resubmit, and discover the next.
Fix: Collect errors for all fields.
Best Practices
- Validate on the server, using HTML validation only for convenience.
- Use
filter_var()for types and standard formats, and regular expressions for custom formats. - Keep validation rules in one place and return field-specific error messages.
- Work with the converted, validated values afterwards.
- Combine validation with output escaping, prepared statements, and CSRF protection.
Official reference: https://www.php.net/manual/en/book.filter.php