Handling Forms and Requests in PHP

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

PHP 8.4

Handling user input is the riskiest part of web development: anything arriving in a request is untrusted. This chapter covers receiving, validating and re-emitting form data safely.

Superglobals

PHP puts request data into superglobal arrays, available inside functions without any declaration:

Variable Contents
$_GET Query string parameters (?id=1)
$_POST Request body of a method="post" form
$_SERVER Request and server info: REQUEST_METHOD, REQUEST_URI
$_FILES Uploaded file metadata
$_COOKIE Cookies sent by the browser
$_SESSION Session data (after session_start())

Reading a missing key warns, so always fall back:

$id = $_GET["id"] ?? null;

GET versus POST

  • GET for queries, filters, pagination — operations that do not change server state. Parameters live in the URL, so the page can be bookmarked and shared.
  • POST for submissions that create or modify data. Parameters travel in the body, not the URL.

Check the method like this:

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // handle submission
}

Do not test whether $_POST is empty — a POST with no fields defeats it. REQUEST_METHOD is the accurate signal.

A minimal form handler

Save as form.php, run php -S localhost:8000, and open http://localhost:8000/form.php:

<?php
declare(strict_types=1);

$errors = [];
$values = ["name" => "", "email" => ""];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $values["name"] = trim($_POST["name"] ?? "");
    $values["email"] = trim($_POST["email"] ?? "");

    if (mb_strlen($values["name"], "UTF-8") < 2) {
        $errors["name"] = "Please enter at least 2 characters";
    }

    if (!filter_var($values["email"], FILTER_VALIDATE_EMAIL)) {
        $errors["email"] = "That email address is not valid";
    }

    if ($errors === []) {
        // validated: persist, then redirect
        header("Location: /thanks.php", true, 303);
        exit;
    }
}
?>
<!doctype html>
<meta charset="utf-8">
<title>Sign up</title>

<?php if ($errors !== []): ?>
    <p>Please fix the problems below and resubmit.</p>
<?php endif; ?>

<form method="post" action="/form.php">
    <label>Name
        <input name="name" value="<?= htmlspecialchars($values["name"], ENT_QUOTES, "UTF-8") ?>">
    </label>
    <?php if (isset($errors["name"])): ?>
        <em><?= htmlspecialchars($errors["name"], ENT_QUOTES, "UTF-8") ?></em>
    <?php endif; ?>

    <label>Email
        <input name="email" type="email" value="<?= htmlspecialchars($values["email"], ENT_QUOTES, "UTF-8") ?>">
    </label>
    <?php if (isset($errors["email"])): ?>
        <em><?= htmlspecialchars($errors["email"], ENT_QUOTES, "UTF-8") ?></em>
    <?php endif; ?>

    <button type="submit">Submit</button>
</form>

The three decisions that matter:

  1. Redirect after success (Post/Redirect/Get) so refreshing the browser cannot resubmit. Use status 303 and always exit after header().
  2. On failure, re-render the form instead of redirecting, refilling what the user typed.
  3. Escape every value printed into HTML.

Escaping output: preventing XSS

echo htmlspecialchars($userInput, ENT_QUOTES, "UTF-8");

htmlspecialchars turns <, >, &, " and ' into entities so the browser treats them as text. Pass all three arguments:

  • ENT_QUOTES — escapes both quote types; escaping only double quotes still leaves single-quoted attributes exploitable.
  • "UTF-8" — states the charset instead of letting PHP guess.

Without it:

// the user typed: <script>alert(1)</script>
echo $values["name"];   // the browser executes it

Escape on output, not on storage. Keep the original in the database and escape when rendering to HTML, so the same data stays clean for JSON, CSV or plain text.

Validating input with filter_var

$email = filter_var($_POST["email"] ?? null, FILTER_VALIDATE_EMAIL);
$age = filter_var($_POST["age"] ?? null, FILTER_VALIDATE_INT, [
    "options" => ["min_range" => 0, "max_range" => 150],
]);
$url = filter_var($_POST["site"] ?? null, FILTER_VALIDATE_URL);

Failure returns false, and 0 is a valid result, so compare with === false:

if ($age === false) {
    $errors["age"] = "Age must be an integer of 150 or less";
}

You can also read straight from a superglobal:

$page = filter_input(INPUT_GET, "page", FILTER_VALIDATE_INT, [
    "options" => ["default" => 1, "min_range" => 1],
]);

filter_input has the advantage of not warning when the key is absent.

Common mistakes

1. Echoing $_POST["name"] directly

That is an XSS hole. Wrap it in htmlspecialchars.

2. Using if ($_POST) to detect a POST request

An empty submission breaks it. Read $_SERVER["REQUEST_METHOD"].

3. Not redirecting after a successful submission

Refreshing resubmits the form.

4. Forgetting exit after header()

The rest of the script still runs and may leak output or override the redirect.

5. Comparing filter_var with == false

0 and "0" are treated as failure. Use === false.

6. Trusting client-side validation

Browser validation is bypassable; server-side checks are the actual boundary.

Summary

  • $_GET queries, $_POST submissions; branch on REQUEST_METHOD.
  • Read request values with ??, never raw.
  • Escape with htmlspecialchars($v, ENT_QUOTES, "UTF-8") at render time, not at save time.
  • Validate with filter_var and compare failures against === false.
  • On success: 303 redirect plus exit.

Next: reading and writing files, from one-liners to streaming large logs.