🛡️ Lesson 12: Input Validation & Sanitization
Never trust user input. Every piece of data that comes from a form, URL, cookie, or API must be validated (is it what we expect?) and sanitized (is it safe to use?). This lesson teaches you PHP's built-in tools for both — so your applications reject bad data before it causes problems.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between validation and sanitization
- Use
filter_var()withFILTER_VALIDATE_*constants to validate emails, URLs, integers, and more - Use
filter_var()withFILTER_SANITIZE_*constants to clean user input - Use
filter_input()to read and filter GET/POST data in one step - Build custom validation rules with regular expressions
- Implement a reusable validation pattern with user-friendly error messages
Estimated Time: 45 minutes
Prerequisites: Lesson 11 (forms, $_GET, $_POST)
📑 In This Lesson
Why Validate? The Trust Boundary
In Lesson 11, we learned to read form data with $_GET and $_POST. But we only added basic checks like "is the field empty?" Real applications face a much bigger problem: users (and attackers) can send absolutely anything.
Consider this: your form has a number field for "age" with min="18" max="120". HTML5 prevents the browser from submitting invalid values, right? Wrong. Anyone can:
- Edit the HTML with browser developer tools and remove the constraints
- Submit the form using
curlor Postman without any browser at all - Send
age=-9999orage=<script>alert('hacked')</script>
⚠️ The Golden Rule of Web Security
Client-side validation is a convenience for the user. Server-side validation is a requirement for security. HTML5 validation attributes, JavaScript checks, and browser constraints can all be bypassed. PHP must be the final gatekeeper.
What Can Go Wrong?
Without proper validation and sanitization, your application is vulnerable to:
| Attack | What Happens | Defense |
|---|---|---|
| SQL Injection | Attacker manipulates database queries | Prepared statements (Lesson 19) |
| XSS (Cross-Site Scripting) | Attacker injects JavaScript into your pages | htmlspecialchars() on all output |
| Data corruption | Invalid data breaks your logic or database | Type validation, range checks |
| Email header injection | Attacker sends spam through your contact form | Email validation, newline filtering |
| Path traversal | Attacker accesses files outside allowed directories | Filename validation, basename() |
Validation and sanitization are your first line of defense. We'll revisit security in depth in Lesson 23, but learning these habits now prevents problems later.
Validation vs. Sanitization
These two concepts are related but distinct, and you'll often use both together.
📖 Definitions
Validation: Checking whether input meets your requirements. Does this look like an email? Is this number in the allowed range? Is this field filled in? Validation answers yes or no — it doesn't change the data.
Sanitization: Cleaning input to make it safe. Stripping HTML tags, removing illegal characters, encoding special characters. Sanitization modifies the data to remove anything dangerous.
<?php
$email = " Alice@Example.COM ";
// Sanitization — clean it up
$email = trim($email); // "Alice@Example.COM"
$email = filter_var($email, FILTER_SANITIZE_EMAIL); // "Alice@Example.COM"
$email = strtolower($email); // "alice@example.com"
// Validation — is it actually valid?
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo "Valid email: $email";
} else {
echo "Invalid email!";
}
The Recommended Order
- Trim — Remove leading/trailing whitespace with
trim() - Sanitize — Remove or encode unwanted characters
- Validate — Check that the cleaned data meets your rules
✅ Think of It Like This
Sanitization is like washing vegetables — remove the dirt. Validation is like checking the expiration date — is it still good to use? You wash first, then check.
filter_var() — Validation Filters
filter_var() is PHP's Swiss Army knife for input filtering. It takes a value and a filter constant, and either returns the filtered value or false on failure.
<?php
// Basic syntax
$result = filter_var($value, FILTER_CONSTANT);
// Returns the value if valid, or false if invalid
FILTER_VALIDATE_EMAIL
<?php
// Validates email format (follows RFC 822)
var_dump(filter_var("alice@example.com", FILTER_VALIDATE_EMAIL));
// string "alice@example.com"
var_dump(filter_var("not-an-email", FILTER_VALIDATE_EMAIL));
// false
var_dump(filter_var("alice@.com", FILTER_VALIDATE_EMAIL));
// false
var_dump(filter_var("alice+work@example.co.uk", FILTER_VALIDATE_EMAIL));
// string "alice+work@example.co.uk" (valid!)
// Practical usage
$email = trim($_POST["email"] ?? "");
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Please enter a valid email address.";
}
✅ Always Use FILTER_VALIDATE_EMAIL
Don't write your own email regex — it's nearly impossible to cover the full email spec. PHP's built-in filter handles edge cases like plus addressing (user+tag@domain.com), subdomains, and international TLDs correctly.
FILTER_VALIDATE_INT
<?php
// Validates that a value is an integer
var_dump(filter_var("42", FILTER_VALIDATE_INT)); // int 42
var_dump(filter_var("0", FILTER_VALIDATE_INT)); // int 0
var_dump(filter_var("-7", FILTER_VALIDATE_INT)); // int -7
var_dump(filter_var("3.14", FILTER_VALIDATE_INT)); // false (float!)
var_dump(filter_var("42abc", FILTER_VALIDATE_INT)); // false
var_dump(filter_var("", FILTER_VALIDATE_INT)); // false
// With range options
$age = filter_var($_POST["age"] ?? "", FILTER_VALIDATE_INT, [
"options" => [
"min_range" => 13,
"max_range" => 120,
]
]);
if ($age === false) {
$errors[] = "Age must be a whole number between 13 and 120.";
}
⚠️ Zero Is Valid! Use Strict Comparison
filter_var("0", FILTER_VALIDATE_INT) returns 0, which is falsy. Always compare with === false:
// BAD — thinks 0 is invalid!
if (!filter_var("0", FILTER_VALIDATE_INT)) { ... }
// GOOD — strict check for false
if (filter_var("0", FILTER_VALIDATE_INT) === false) { ... }
FILTER_VALIDATE_FLOAT
<?php
var_dump(filter_var("3.14", FILTER_VALIDATE_FLOAT)); // float 3.14
var_dump(filter_var("42", FILTER_VALIDATE_FLOAT)); // float 42.0
var_dump(filter_var("-0.5", FILTER_VALIDATE_FLOAT)); // float -0.5
var_dump(filter_var("1,234.56", FILTER_VALIDATE_FLOAT)); // false (comma!)
// Allow thousand separator
$price = filter_var("1,234.56", FILTER_VALIDATE_FLOAT, [
"flags" => FILTER_FLAG_ALLOW_THOUSAND
]);
// float 1234.56
FILTER_VALIDATE_URL
<?php
var_dump(filter_var("https://example.com", FILTER_VALIDATE_URL));
// string "https://example.com"
var_dump(filter_var("ftp://files.example.com/doc", FILTER_VALIDATE_URL));
// string "ftp://files.example.com/doc"
var_dump(filter_var("not a url", FILTER_VALIDATE_URL));
// false
var_dump(filter_var("example.com", FILTER_VALIDATE_URL));
// false (needs scheme: https://example.com)
// Require specific components
$url = filter_var($input, FILTER_VALIDATE_URL);
if ($url === false) {
$errors[] = "Please enter a valid URL (include https://).";
}
FILTER_VALIDATE_IP
<?php
var_dump(filter_var("192.168.1.1", FILTER_VALIDATE_IP)); // valid IPv4
var_dump(filter_var("::1", FILTER_VALIDATE_IP)); // valid IPv6
var_dump(filter_var("999.999.999.999", FILTER_VALIDATE_IP)); // false
// Validate only IPv4
filter_var($ip, FILTER_VALIDATE_IP, ["flags" => FILTER_FLAG_IPV4]);
// Validate only IPv6
filter_var($ip, FILTER_VALIDATE_IP, ["flags" => FILTER_FLAG_IPV6]);
// Reject private/reserved ranges
filter_var($ip, FILTER_VALIDATE_IP, ["flags" => FILTER_FLAG_NO_PRIV_RANGE]);
FILTER_VALIDATE_BOOLEAN
<?php
// Returns true, false, or null (if not a boolean-like value)
var_dump(filter_var("true", FILTER_VALIDATE_BOOLEAN)); // true
var_dump(filter_var("1", FILTER_VALIDATE_BOOLEAN)); // true
var_dump(filter_var("yes", FILTER_VALIDATE_BOOLEAN)); // true
var_dump(filter_var("on", FILTER_VALIDATE_BOOLEAN)); // true
var_dump(filter_var("false", FILTER_VALIDATE_BOOLEAN)); // false
var_dump(filter_var("0", FILTER_VALIDATE_BOOLEAN)); // false
var_dump(filter_var("no", FILTER_VALIDATE_BOOLEAN)); // false
var_dump(filter_var("off", FILTER_VALIDATE_BOOLEAN)); // false
// With FILTER_NULL_ON_FAILURE — returns null instead of false for non-booleans
var_dump(filter_var("maybe", FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
// null
FILTER_VALIDATE_DOMAIN
<?php
// PHP 7+ — validates domain names
var_dump(filter_var("example.com", FILTER_VALIDATE_DOMAIN)); // "example.com"
var_dump(filter_var("sub.example.co.uk", FILTER_VALIDATE_DOMAIN)); // valid
var_dump(filter_var("-invalid.com", FILTER_VALIDATE_DOMAIN)); // false
var_dump(filter_var("no spaces.com", FILTER_VALIDATE_DOMAIN)); // false
Quick Reference: Validation Filters
| Filter | Validates | Returns on Success |
|---|---|---|
FILTER_VALIDATE_EMAIL |
Email address format | The email string |
FILTER_VALIDATE_INT |
Integer (optional min/max range) | The integer value |
FILTER_VALIDATE_FLOAT |
Floating-point number | The float value |
FILTER_VALIDATE_URL |
URL with scheme | The URL string |
FILTER_VALIDATE_IP |
IPv4 or IPv6 address | The IP string |
FILTER_VALIDATE_BOOLEAN |
Boolean-like values | true or false |
FILTER_VALIDATE_DOMAIN |
Domain name | The domain string |
FILTER_VALIDATE_MAC |
MAC address | The MAC string |
filter_var() — Sanitization Filters
Sanitization filters modify the input, stripping or encoding characters that could cause problems. Unlike validation, they always return a string — they don't return false.
FILTER_SANITIZE_EMAIL
<?php
// Removes characters not allowed in emails
echo filter_var("alice@example.com", FILTER_SANITIZE_EMAIL);
// "alice@example.com" (unchanged)
echo filter_var("alice @example .com", FILTER_SANITIZE_EMAIL);
// "alice@example.com" (spaces removed)
echo filter_var("alice', FILTER_SANITIZE_SPECIAL_CHARS);
// "<script>alert("xss")</script>"
FILTER_SANITIZE_ADD_SLASHES
<?php
// Adds backslashes before quotes and backslash (like addslashes())
echo filter_var('He said "hello"', FILTER_SANITIZE_ADD_SLASHES);
// "He said \"hello\""
htmlspecialchars() — The Essential Sanitizer
While filter_var has sanitization filters, htmlspecialchars() remains the most important function for output sanitization. Use it every time you display user data in HTML:
<?php
$userInput = '<script>alert("hacked")</script>';
// What htmlspecialchars converts:
// < → <
// > → >
// " → "
// ' → ' (with ENT_QUOTES)
// & → &
echo htmlspecialchars($userInput, ENT_QUOTES, "UTF-8");
// <script>alert("hacked")</script>
// Browser displays it as text, not as executable code
📖 Two Contexts, Two Functions
Input sanitization — Use filter_var() with FILTER_SANITIZE_* to clean data when you receive it.
Output sanitization — Use htmlspecialchars() when displaying data in HTML. This is your primary XSS defense.
Many developers do both: sanitize on input AND escape on output. That's the safest approach.
Sanitize + Validate Together
The typical pattern is to sanitize first, then validate the cleaned result:
<?php
// Step 1: Get and trim the input
$email = trim($_POST["email"] ?? "");
// Step 2: Sanitize — remove illegal email characters
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
// Step 3: Validate — is it a properly formatted email?
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Please enter a valid email address.";
}
// Step 4: When displaying, always escape
echo "Email: " . htmlspecialchars($email);
filter_input() — Direct from GET/POST
filter_input() reads a value directly from $_GET, $_POST, or other input sources and applies a filter in one step. It returns null if the key doesn't exist, false if validation fails, or the filtered value on success.
<?php
// Syntax: filter_input(INPUT_TYPE, "key", FILTER, options)
// Read and validate an email from POST
$email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);
if ($email === null) {
echo "Email field was not submitted.";
} elseif ($email === false) {
echo "Invalid email format.";
} else {
echo "Valid email: $email";
}
// Read and validate an integer from GET
$page = filter_input(INPUT_GET, "page", FILTER_VALIDATE_INT, [
"options" => ["min_range" => 1, "default" => 1]
]);
// Read and sanitize a string from POST
$name = filter_input(INPUT_POST, "name", FILTER_SANITIZE_SPECIAL_CHARS);
Input Type Constants
| Constant | Reads From |
|---|---|
INPUT_GET |
$_GET |
INPUT_POST |
$_POST |
INPUT_COOKIE |
$_COOKIE |
INPUT_SERVER |
$_SERVER |
INPUT_ENV |
$_ENV |
filter_input vs. filter_var
<?php
// These two approaches are equivalent:
// Approach 1: filter_var (two steps)
$email = $_POST["email"] ?? "";
$email = filter_var($email, FILTER_VALIDATE_EMAIL);
// Approach 2: filter_input (one step)
$email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);
// filter_input is slightly cleaner for simple cases.
// filter_var is more flexible — you can sanitize THEN validate.
✅ When to Use Which
Use filter_input() for quick, single-step validation of GET/POST data — especially for simple fields like page numbers, IDs, and email addresses.
Use filter_var() when you need to sanitize first and then validate, or when you're working with data that's already in a variable (not directly from a superglobal).
filter_input_array() — Validate Multiple Fields at Once
<?php
// Define filters for multiple POST fields at once
$filters = [
"name" => FILTER_SANITIZE_SPECIAL_CHARS,
"email" => FILTER_VALIDATE_EMAIL,
"age" => [
"filter" => FILTER_VALIDATE_INT,
"options" => ["min_range" => 13, "max_range" => 120],
],
"url" => FILTER_VALIDATE_URL,
];
// Apply all filters in one call
$result = filter_input_array(INPUT_POST, $filters);
// $result is an associative array:
// [
// "name" => "Alice" (sanitized),
// "email" => "alice@example.com" or false,
// "age" => 25 or false,
// "url" => "https://..." or false,
// ]
// Check each result
if ($result["email"] === false) {
$errors[] = "Invalid email address.";
}
if ($result["age"] === false) {
$errors[] = "Age must be between 13 and 120.";
}
Custom Validation with Regex
PHP's built-in filters cover common cases, but sometimes you need custom patterns. That's where regular expressions from Lesson 10 come back into play.
Common Custom Validations
Username (alphanumeric + underscore, 3–20 chars)
<?php
function validate_username(string $username): bool {
return (bool) preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username);
}
// Test
var_dump(validate_username("alice_123")); // true
var_dump(validate_username("ab")); // false (too short)
var_dump(validate_username("alice smith")); // false (space)
var_dump(validate_username("alice@bob")); // false (@ not allowed)
Phone Number (US format)
<?php
function validate_phone(string $phone): bool {
// Accept: 555-123-4567, (555) 123-4567, 5551234567, 555.123.4567
$cleaned = preg_replace('/[\s\-\.\(\)]/', '', $phone);
return (bool) preg_match('/^1?\d{10}$/', $cleaned);
}
function format_phone(string $phone): string {
$digits = preg_replace('/\D/', '', $phone);
if (strlen($digits) === 11 && $digits[0] === "1") {
$digits = substr($digits, 1); // Remove leading 1
}
return sprintf("(%s) %s-%s",
substr($digits, 0, 3),
substr($digits, 3, 3),
substr($digits, 6, 4)
);
}
echo format_phone("2025551234"); // "(202) 555-1234"
US ZIP Code
<?php
function validate_zip(string $zip): bool {
return (bool) preg_match('/^\d{5}(-\d{4})?$/', $zip);
}
var_dump(validate_zip("89012")); // true
var_dump(validate_zip("89012-1234")); // true
var_dump(validate_zip("8901")); // false
var_dump(validate_zip("ABCDE")); // false
Strong Password
<?php
function validate_password(string $password): array {
$errors = [];
if (strlen($password) < 8) {
$errors[] = "Must be at least 8 characters.";
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = "Must contain at least one uppercase letter.";
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = "Must contain at least one lowercase letter.";
}
if (!preg_match('/\d/', $password)) {
$errors[] = "Must contain at least one digit.";
}
if (!preg_match('/[^a-zA-Z\d]/', $password)) {
$errors[] = "Must contain at least one special character.";
}
return $errors;
}
$pwErrors = validate_password("abc");
// ["Must be at least 8 characters.", "Must contain at least one uppercase letter.",
// "Must contain at least one digit.", "Must contain at least one special character."]
$pwErrors = validate_password("Str0ng!Pass");
// [] (empty — all checks passed)
Hex Color Code
<?php
function validate_hex_color(string $color): bool {
return (bool) preg_match('/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color);
}
var_dump(validate_hex_color("#FF00AA")); // true
var_dump(validate_hex_color("#F0A")); // true (shorthand)
var_dump(validate_hex_color("FF00AA")); // true (without #)
var_dump(validate_hex_color("#GGHHII")); // false
Using FILTER_VALIDATE_REGEXP
You can also use custom regex directly with filter_var():
<?php
// Validate a product code like "PRD-12345"
$code = "PRD-12345";
$result = filter_var($code, FILTER_VALIDATE_REGEXP, [
"options" => ["regexp" => '/^[A-Z]{3}-\d{5}$/']
]);
if ($result === false) {
echo "Invalid product code format.";
} else {
echo "Valid code: $result";
}
✅ Write Specific Error Messages
Instead of "Invalid input," tell the user exactly what's wrong: "Username must be 3–20 characters using only letters, numbers, and underscores." Specific messages help users fix their input without guessing.
Building a Validation Pattern
Let's put everything together into a reusable pattern for validating forms. This is a structure you'll use in virtually every PHP project.
Complete Self-Processing Form with Validation
<?php
// ============================================
// 1. Initialize variables and error array
// ============================================
$name = "";
$email = "";
$age = "";
$website = "";
$password = "";
$errors = [];
$success = false;
// ============================================
// 2. Process form submission
// ============================================
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// --- Name ---
$name = trim($_POST["name"] ?? "");
if ($name === "") {
$errors["name"] = "Name is required.";
} elseif (strlen($name) < 2 || strlen($name) > 50) {
$errors["name"] = "Name must be 2–50 characters.";
} elseif (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) {
$errors["name"] = "Name can only contain letters, spaces, hyphens, and apostrophes.";
}
// --- Email ---
$email = trim($_POST["email"] ?? "");
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
if ($email === "") {
$errors["email"] = "Email is required.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "Please enter a valid email address.";
}
// --- Age ---
$age = $_POST["age"] ?? "";
$ageValid = filter_var($age, FILTER_VALIDATE_INT, [
"options" => ["min_range" => 13, "max_range" => 120]
]);
if ($age === "") {
$errors["age"] = "Age is required.";
} elseif ($ageValid === false) {
$errors["age"] = "Age must be a number between 13 and 120.";
} else {
$age = $ageValid; // Use the validated integer
}
// --- Website (optional) ---
$website = trim($_POST["website"] ?? "");
if ($website !== "") {
// Add https:// if no scheme provided
if (!preg_match('/^https?:\/\//', $website)) {
$website = "https://" . $website;
}
if (!filter_var($website, FILTER_VALIDATE_URL)) {
$errors["website"] = "Please enter a valid URL.";
}
}
// --- Password ---
$password = $_POST["password"] ?? "";
$pwErrors = validate_password($password); // Our function from above
if (!empty($pwErrors)) {
$errors["password"] = "Password: " . implode(" ", $pwErrors);
}
// ============================================
// 3. All valid? Process the data!
// ============================================
if (empty($errors)) {
// Hash the password, save to database, etc.
$success = true;
}
}
// ============================================
// Helper: Check if a field has an error
// ============================================
function field_error(string $field, array $errors): string {
if (isset($errors[$field])) {
return '<span class="error">' . htmlspecialchars($errors[$field]) . '</span>';
}
return "";
}
function field_class(string $field, array $errors): string {
return isset($errors[$field]) ? 'class="input-error"' : '';
}
?>
And the HTML portion of the same file:
<!-- In the HTML section of the same file -->
<?php if ($success): ?>
<div class="alert success">
<p>Account created successfully! Welcome, <?= htmlspecialchars($name) ?>.</p>
</div>
<?php else: ?>
<form method="post" action="" novalidate>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" id="name" name="name"
value="<?= htmlspecialchars($name) ?>"
<?= field_class("name", $errors) ?>>
<?= field_error("name", $errors) ?>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email"
value="<?= htmlspecialchars($email) ?>"
<?= field_class("email", $errors) ?>>
<?= field_error("email", $errors) ?>
</div>
<div class="form-group">
<label for="age">Age *</label>
<input type="number" id="age" name="age"
value="<?= htmlspecialchars($age) ?>"
<?= field_class("age", $errors) ?>>
<?= field_error("age", $errors) ?>
</div>
<div class="form-group">
<label for="website">Website (optional)</label>
<input type="url" id="website" name="website"
value="<?= htmlspecialchars($website) ?>"
<?= field_class("website", $errors) ?>>
<?= field_error("website", $errors) ?>
</div>
<div class="form-group">
<label for="password">Password *</label>
<input type="password" id="password" name="password"
<?= field_class("password", $errors) ?>>
<?= field_error("password", $errors) ?>
<small>Min 8 chars, must include uppercase, lowercase, digit, and special character.</small>
</div>
<button type="submit">Create Account</button>
</form>
<?php endif; ?>
✅ Key Patterns to Notice
- Errors are stored in an associative array keyed by field name — so each field shows its own error
- Helper functions
field_error()andfield_class()keep the HTML clean novalidateon the form disables browser validation for testing (remove in production)- Passwords are never made sticky — don't echo passwords back into the form
- Optional fields validate only when they have a value
Best Practices for Error Messages
| Bad Message | Good Message | Why |
|---|---|---|
| "Invalid input" | "Name must be 2–50 characters" | Tells the user exactly what to fix |
| "Error in field 3" | "Email: Please include an @ sign" | Identifies the field by name |
| "Bad email" | "Please enter a valid email (e.g., name@example.com)" | Shows the expected format |
| "Password wrong" | "Password must include at least one uppercase letter" | Specifies which rule failed |
| "Validation failed: FILTER_VALIDATE_INT" | "Age must be a whole number between 13 and 120" | Human language, not code constants |
💡 Tip: Never reveal technical details in error messages shown to users. "SQL syntax error near 'DROP TABLE'" tells attackers what you're vulnerable to. Log technical errors; show friendly messages.
Hands-On Exercises
🏋️ Exercise 1: Validation Function Library
Objective: Build a set of reusable validation functions.
Instructions:
- Create the following functions, each returning
trueon success or an error message string on failure:
validate_required($value, $fieldName) // Non-empty after trim
validate_email($value) // Valid email
validate_min_length($value, $min, $name) // Minimum string length
validate_max_length($value, $max, $name) // Maximum string length
validate_int_range($value, $min, $max, $name) // Integer in range
validate_in_list($value, $allowed, $name) // Value is in allowed list
💡 Hint
Return true for valid, a string for invalid. The calling code checks: if ($result !== true) { $errors[] = $result; }
✅ Solution
<?php
function validate_required(string $value, string $fieldName): string|true {
return trim($value) !== "" ? true : "$fieldName is required.";
}
function validate_email(string $value): string|true {
$value = trim($value);
if ($value === "") return "Email is required.";
$sanitized = filter_var($value, FILTER_SANITIZE_EMAIL);
return filter_var($sanitized, FILTER_VALIDATE_EMAIL)
? true
: "Please enter a valid email address.";
}
function validate_min_length(string $value, int $min, string $name): string|true {
return strlen(trim($value)) >= $min
? true
: "$name must be at least $min characters.";
}
function validate_max_length(string $value, int $max, string $name): string|true {
return strlen(trim($value)) <= $max
? true
: "$name must be no more than $max characters.";
}
function validate_int_range(string $value, int $min, int $max, string $name): string|true {
$result = filter_var($value, FILTER_VALIDATE_INT, [
"options" => ["min_range" => $min, "max_range" => $max]
]);
return $result !== false ? true : "$name must be a number between $min and $max.";
}
function validate_in_list(string $value, array $allowed, string $name): string|true {
return in_array($value, $allowed, true)
? true
: "$name must be one of: " . implode(", ", $allowed) . ".";
}
// Usage
$errors = [];
$checks = [
validate_required($_POST["name"] ?? "", "Name"),
validate_email($_POST["email"] ?? ""),
validate_min_length($_POST["name"] ?? "", 2, "Name"),
validate_int_range($_POST["age"] ?? "", 13, 120, "Age"),
validate_in_list($_POST["role"] ?? "", ["user", "editor", "admin"], "Role"),
];
foreach ($checks as $result) {
if ($result !== true) {
$errors[] = $result;
}
}
🏋️ Exercise 2: Contact Form with Full Validation
Objective: Build a fully validated, sticky, self-processing contact form.
Requirements:
- Fields: Name (required, 2–50 chars, letters/spaces only), Email (required, valid format), Phone (optional, US format), Subject (required, select dropdown), Message (required, 10–1000 chars)
- Per-field error messages displayed next to each input
- All fields are sticky (except password if you add one)
- Success message on valid submission
- Use both
filter_var()and regex validation
💡 Hint
Use the associative error array pattern: $errors["name"] = "...". Create helper functions for displaying errors next to fields. Validate the phone only if it's not empty.
✅ Solution
<?php
$name = "";
$email = "";
$phone = "";
$subject = "";
$message = "";
$errors = [];
$success = false;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Sanitize
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
$phone = trim($_POST["phone"] ?? "");
$subject = $_POST["subject"] ?? "";
$message = trim($_POST["message"] ?? "");
// Validate name
if ($name === "") {
$errors["name"] = "Name is required.";
} elseif (strlen($name) < 2 || strlen($name) > 50) {
$errors["name"] = "Name must be 2–50 characters.";
} elseif (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) {
$errors["name"] = "Name can only contain letters, spaces, hyphens, and apostrophes.";
}
// Validate email
if ($email === "") {
$errors["email"] = "Email is required.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "Please enter a valid email (e.g., name@example.com).";
}
// Validate phone (optional)
if ($phone !== "") {
$phoneDigits = preg_replace('/\D/', '', $phone);
if (strlen($phoneDigits) < 10 || strlen($phoneDigits) > 11) {
$errors["phone"] = "Please enter a valid US phone number.";
}
}
// Validate subject
$validSubjects = ["general", "support", "billing", "feedback"];
if (!in_array($subject, $validSubjects)) {
$errors["subject"] = "Please select a subject.";
}
// Validate message
if ($message === "") {
$errors["message"] = "Message is required.";
} elseif (strlen($message) < 10) {
$errors["message"] = "Message must be at least 10 characters.";
} elseif (strlen($message) > 1000) {
$errors["message"] = "Message must be under 1000 characters.";
}
if (empty($errors)) {
$success = true;
}
}
function err(string $field): string {
global $errors;
return isset($errors[$field])
? "<span class='error'>" . htmlspecialchars($errors[$field]) . "</span>" : "";
}
?>
<?php if ($success): ?>
<p class="success">Thanks, <?= htmlspecialchars($name) ?>! Your message has been received.</p>
<?php else: ?>
<form method="post" action="">
<label>Name *<br>
<input type="text" name="name" value="<?= htmlspecialchars($name) ?>">
</label><?= err("name") ?><br>
<label>Email *<br>
<input type="email" name="email" value="<?= htmlspecialchars($email) ?>">
</label><?= err("email") ?><br>
<label>Phone (optional)<br>
<input type="tel" name="phone" value="<?= htmlspecialchars($phone) ?>">
</label><?= err("phone") ?><br>
<label>Subject *<br>
<select name="subject">
<option value="">-- Select --</option>
<?php foreach (["general"=>"General","support"=>"Support","billing"=>"Billing","feedback"=>"Feedback"] as $v=>$l): ?>
<option value="<?=$v?>" <?=$subject===$v?"selected":""?>><?=$l?></option>
<?php endforeach; ?>
</select>
</label><?= err("subject") ?><br>
<label>Message * (10–1000 chars)<br>
<textarea name="message" rows="5"><?= htmlspecialchars($message) ?></textarea>
</label><?= err("message") ?><br>
<button type="submit">Send Message</button>
</form>
<?php endif; ?>
🎯 Quick Quiz
Question 1: What's the difference between validation and sanitization?
Question 2: What does filter_var("0", FILTER_VALIDATE_INT) return?
Question 3: Why shouldn't you rely on HTML5 form validation alone?
Question 4: What does filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL) return if the key doesn't exist?
Question 5: When should you use htmlspecialchars()?
Summary
🎉 Key Takeaways
- Never trust user input — client-side validation is a UX convenience, server-side validation is a security requirement
- Sanitize then validate — clean the data first, then check if it meets your rules
filter_var()handles both validation (FILTER_VALIDATE_*) and sanitization (FILTER_SANITIZE_*)- Key validation filters:
FILTER_VALIDATE_EMAIL,FILTER_VALIDATE_INT(with min/max range),FILTER_VALIDATE_URL,FILTER_VALIDATE_FLOAT,FILTER_VALIDATE_IP filter_input()reads and filters GET/POST data in one step; returnsnull(missing),false(invalid), or the value (valid)- Use
=== falsefor filter comparisons —0and""are valid results that evaluate as falsy - Custom regex for patterns PHP doesn't cover: usernames, phone numbers, ZIP codes, product codes
htmlspecialchars()is your primary XSS defense — use it on all user data displayed in HTML- Specific error messages help users fix problems; never expose technical details to end users
- Store errors by field name (
$errors["email"] = "...") so each field can show its own error
📚 Additional Resources
- PHP Manual: filter_var()
- PHP Manual: filter_input()
- PHP Manual: Validation Filters List
- PHP Manual: Sanitization Filters List
- PHP Manual: htmlspecialchars()
- OWASP Input Validation Cheat Sheet
🚀 What's Next?
You can now receive, validate, and sanitize user input safely. In Lesson 13: File Handling & Includes, you'll learn to read and write files with PHP, organize your code with include/require, and build reusable components like headers and footers.
🎉 Congratulations!
You now know how to build forms that reject bad data before it ever reaches your application. This is a skill you'll use in every PHP project.