TL;DR

Learn safe PHP form processing. Read input data, sanitize and validate user submissions, and return meaningful error messages.

Key concepts

  • PHP form handling
  • PHP form validation
  • PHP sanitize input
  • PHP POST GET

Form Processing

Forms are the primary way users send data to a PHP application — login credentials, contact messages, search queries, registration details. Getting form processing right means more than just reading a value from $_POST. It means validating what arrived, sanitizing it before use, and giving users clear feedback when something is wrong. Done poorly, form handling is one of the most common sources of security vulnerabilities in PHP applications.

This lesson covers the full lifecycle of a form submission: reading data, sanitizing it, validating it, and responding appropriately.

Reading Form Data

PHP exposes submitted form data through three superglobals:

  • $_GET — data sent via URL query string (?name=Alice)
  • $_POST — data sent in the HTTP request body (standard HTML form submissions)
  • $_REQUEST — a merge of both (avoid this in production; prefer explicit access)

Access is straightforward, but you should always check whether a key exists before reading it. Using $_POST['email'] on a page loaded with a GET request will trigger an undefined index notice.

<?php

// Simulate a POST submission
$_POST = [
    'name'  => 'Alice',
    'email' => 'alice@example.com',
    'age'   => '28',
];

$name  = $_POST['name']  ?? '';
$email = $_POST['email'] ?? '';
$age   = $_POST['age']   ?? '';

echo "Name: $name\n";
echo "Email: $email\n";
echo "Age: $age\n";

The null coalescing operator (??) is your best friend here — it returns the left side if the key exists and is not null, or the right side otherwise. This keeps your code clean without needing isset() everywhere.

Sanitizing Input

Sanitization means cleaning up the raw value before you use or store it. The key rule: never trust user input. Values may contain extra whitespace, HTML tags, or characters designed to break your SQL queries or HTML output.

PHP's filter_var() function combined with FILTER_SANITIZE_* constants handles many common cases. For HTML output, htmlspecialchars() is essential to prevent cross-site scripting (XSS).

<?php

// Raw input that could cause problems
$_POST = [
    'username' => '  Alice<script>alert(1)</script>  ',
    'website'  => 'javascript:alert(1)',
    'bio'      => "Line one\nLine two",
];

// Trim whitespace and strip HTML tags
$username = trim(strip_tags($_POST['username'] ?? ''));

// Sanitize a URL — returns false if the URL is invalid
$rawUrl  = $_POST['website'] ?? '';
$website = filter_var($rawUrl, FILTER_SANITIZE_URL);
$website = filter_var($website, FILTER_VALIDATE_URL) ? $website : '';

// Safe output — convert < > & " ' to HTML entities
$bio = htmlspecialchars($_POST['bio'] ?? '', ENT_QUOTES, 'UTF-8');

echo "Username: $username\n";
echo "Website: " . ($website ?: '(invalid)') . "\n";
echo "Bio (safe for HTML): $bio\n";

Notice the layered approach on $website: sanitize first to strip illegal characters, then validate to confirm the result is actually a URL. If validation fails, default to an empty string rather than storing a potentially malicious value.

Validating Input

Validation is different from sanitization. Sanitization transforms the value; validation checks whether it meets your requirements. A validated field either passes or fails, and failures generate error messages the user can act on.

Build a reusable pattern: collect errors in an array, then check whether that array is empty before proceeding.

<?php

$_POST = [
    'email'    => 'not-an-email',
    'password' => 'abc',
    'age'      => '15',
];

$errors = [];

// Email must be a valid address
$email = trim($_POST['email'] ?? '');
if ($email === '') {
    $errors[] = 'Email is required.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'Email address is not valid.';
}

// Password must be at least 8 characters
$password = $_POST['password'] ?? '';
if (strlen($password) < 8) {
    $errors[] = 'Password must be at least 8 characters.';
}

// Age must be an integer between 18 and 120
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 18, 'max_range' => 120],
]);
if ($age === false) {
    $errors[] = 'You must be at least 18 years old.';
}

if (empty($errors)) {
    echo "All fields are valid. Processing submission...\n";
} else {
    echo "Validation failed:\n";
    foreach ($errors as $error) {
        echo "  - $error\n";
    }
}

filter_var() with FILTER_VALIDATE_INT and an options array is much cleaner than writing manual range checks. It returns false on failure, so a strict === false comparison is important — do not use a loose == false check, because 0 would also match.

Handling Multiple Field Types

Real forms mix text, numbers, checkboxes, selects, and optional fields. Each type needs a slightly different treatment.

Checkboxes are only present in $_POST when checked. Selects must be validated against an allowlist of acceptable values. Optional fields need defaults but not "required" errors.

<?php

$_POST = [
    'country'       => 'DE',
    'subscribe'     => 'on',
    // 'phone' intentionally missing — optional field
];

$allowedCountries = ['US', 'CA', 'GB', 'DE', 'FR', 'AU'];

$errors = [];

// Select: validate against an allowlist
$country = $_POST['country'] ?? '';
if (!in_array($country, $allowedCountries, strict: true)) {
    $errors[] = 'Please select a valid country.';
}

// Checkbox: present means true, absent means false
$subscribe = isset($_POST['subscribe']);

// Optional text field: trim and default to empty string
$phone = trim($_POST['phone'] ?? '');
if ($phone !== '' && !preg_match('/^\+?[\d\s\-()]{7,15}$/', $phone)) {
    $errors[] = 'Phone number format is not recognised.';
}

if (empty($errors)) {
    echo "Country: $country\n";
    echo "Subscribe to newsletter: " . ($subscribe ? 'Yes' : 'No') . "\n";
    echo "Phone: " . ($phone !== '' ? $phone : '(not provided)') . "\n";
} else {
    foreach ($errors as $error) {
        echo "- $error\n";
    }
}

The named argument strict: true on in_array() prevents PHP from doing type coercion during the comparison — always use it when checking allowlists.

Try It Yourself

Combine everything into a complete contact form processor. It reads five fields, sanitizes each one, validates required fields and formats, and either reports errors or prints a success summary.

<?php

// Simulate a form submission — edit these values to test different scenarios
$_POST = [
    'name'    => '  Bob Smith  ',
    'email'   => 'bob@example.com',
    'subject' => 'Question about pricing',
    'message' => 'Hello, I would like to know more about your plans.',
    'rating'  => '4',
];

function processContactForm(array $post): array
{
    $errors = [];
    $data   = [];

    // Name
    $name = trim(strip_tags($post['name'] ?? ''));
    if ($name === '') {
        $errors[] = 'Name is required.';
    } elseif (strlen($name) > 100) {
        $errors[] = 'Name must be 100 characters or fewer.';
    } else {
        $data['name'] = $name;
    }

    // Email
    $email = trim($post['email'] ?? '');
    if ($email === '') {
        $errors[] = 'Email is required.';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'Please enter a valid email address.';
    } else {
        $data['email'] = $email;
    }

    // Subject (optional, max 200 chars)
    $subject = trim(strip_tags($post['subject'] ?? ''));
    $data['subject'] = substr($subject, 0, 200);

    // Message (required, max 2000 chars)
    $message = trim($post['message'] ?? '');
    if ($message === '') {
        $errors[] = 'Message is required.';
    } else {
        $data['message'] = htmlspecialchars(substr($message, 0, 2000), ENT_QUOTES, 'UTF-8');
    }

    // Rating (optional integer 1–5)
    $rawRating = $post['rating'] ?? '';
    if ($rawRating !== '') {
        $rating = filter_var($rawRating, FILTER_VALIDATE_INT, [
            'options' => ['min_range' => 1, 'max_range' => 5],
        ]);
        if ($rating === false) {
            $errors[] = 'Rating must be a number between 1 and 5.';
        } else {
            $data['rating'] = $rating;
        }
    }

    return ['errors' => $errors, 'data' => $data];
}

$result = processContactForm($_POST);

if (empty($result['errors'])) {
    echo "Form submitted successfully!\n\n";
    foreach ($result['data'] as $field => $value) {
        echo ucfirst($field) . ': ' . $value . "\n";
    }
} else {
    echo "Please fix the following errors:\n";
    foreach ($result['errors'] as $error) {
        echo "  - $error\n";
    }
}

Try changing the $_POST values — remove a required field, enter an invalid email, or set rating to 99 — and observe how the validator responds.

Key Takeaways

  • Use $_POST['key'] ?? '' to safely read form fields without triggering notices on missing keys.
  • Sanitize with trim(), strip_tags(), and htmlspecialchars() to clean raw input; validate with filter_var() and custom rules to confirm it meets your requirements.
  • Collect all errors in an array before deciding whether to proceed — this lets users fix multiple problems in a single round trip.
  • Always use in_array($value, $allowlist, strict: true) when validating against a fixed set of acceptable values.
  • FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL, and FILTER_VALIDATE_INT cover the most common validation scenarios without requiring custom regex.
  • Encode values with htmlspecialchars() at output time, not at input time, so you preserve the original value for storage or further processing.

Pro Tip: Build a small FormValidator class that accepts a rules array (like ['email' => ['required', 'email'], 'age' => ['required', 'integer', 'min:18']]) and returns a structured errors array. This pattern is the foundation of every major PHP framework's validation layer — understanding it from scratch makes Laravel's $request->validate() feel obvious rather than magical.

Next Steps

You can now read, sanitize, and validate form input safely, collect structured error messages, and protect against common input attacks. Your applications handle user data with confidence.

Validated data needs to be presented back to users as HTML, and mixing PHP logic with HTML markup quickly becomes unmanageable. Templating and views teaches you to separate data preparation from presentation using layouts, partials, output buffering, and proper escaping. This separation is the foundation of every PHP framework's view layer, and understanding it from scratch will make tools like Blade and Twig feel intuitive rather than magical.

Continue to Templating and Views -->