PHP Forms Handling

PHP forms handling is the process of receiving data that a user types or selects in an HTML <form>, reading that data safely in PHP, validating and cleaning it, and then deciding what to do next — save it, email it, or show the user an error. Nearly every interactive PHP application, from a simple contact page to a full login or checkout flow, is built on this exact pattern. Handle it correctly and your app is secure, predictable, and forgiving of bad input; handle it carelessly and you open the door to cross-site scripting (XSS), broken pages, and corrupted data.

Overview: How Forms Work

An HTML form is just a set of input fields wrapped in a <form> tag. Two attributes matter most: method, which controls how the browser packages the data, and action, which controls where the browser sends it. When the user clicks submit, the browser gathers every named field (<input>, <select>, <textarea>) and sends an HTTP request to the action URL.

With method="get", the browser appends the fields to the URL as a query string (?name=Ada&age=30). This is visible, bookmarkable, cached, and length-limited by the browser/server — it’s meant for retrieving data, like search boxes or filters, never for anything that changes server state or carries sensitive data. With method="post", the fields travel in the HTTP request body instead. They’re invisible in the URL, not length-limited in the same way, and are the correct choice for logins, purchases, and anything that creates, updates, or deletes data.

PHP automatically parses whichever the browser sends and fills two superglobal arrays before your script even starts running: $_GET for query-string data and $_POST for body data submitted with method="post" using the standard application/x-www-form-urlencoded or multipart/form-data encodings. A third array, $_REQUEST, merges $_GET, $_POST, and $_COOKIE together — it’s convenient but imprecise, so most experienced developers avoid it and read from $_GET/$_POST explicitly so it’s always clear where a value came from.

A very common PHP idiom is the self-processing form: a single .php file both renders the HTML form and handles its own submission, by leaving action empty (or pointing it at itself) and checking $_SERVER['REQUEST_METHOD'] to decide whether this request is a fresh page view (GET) or a submission (POST). This keeps the form and its validation logic together, and lets you redisplay the form with error messages and the user’s previously typed values if validation fails.

Syntax

The general shape of a form and the PHP that reads it:

<form method="post" action="process.php">
    <input type="text" name="username">
    <button type="submit">Send</button>
</form>

<?php
$username = $_POST['username'] ?? '';
Piece Purpose
method get for retrieving/filtering data, post for anything that changes state or is sensitive
action Where the request is sent; empty or omitted means “submit to this same page”
name attribute Becomes the array key in $_GET/$_POST, e.g. name="email"$_POST['email']
name="tags[]" Square brackets collect multiple values (checkboxes, multi-selects) into a PHP array
enctype="multipart/form-data" Required on the form when it includes <input type="file">, populates $_FILES

Key superglobals used when handling forms:

Superglobal Contains
$_GET Query-string parameters
$_POST Body fields from a method="post" submission
$_REQUEST Merged $_GET + $_POST + $_COOKIE (generally avoid — ambiguous source)
$_FILES Uploaded file metadata from multipart/form-data forms
$_SERVER['REQUEST_METHOD'] The HTTP method of the current request, e.g. 'GET' or 'POST'

Examples

Example 1: A self-processing form

<?php
$name = '';
$email = '';
$errors = [];

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

    if ($name === '') {
        $errors[] = 'Name is required.';
    }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'A valid email address is required.';
    }

    if (empty($errors)) {
        echo "Thanks, " . htmlspecialchars($name) . "! We'll email you at " . htmlspecialchars($email) . ".";
    }
}
?>
<!DOCTYPE html>
<html>
<body>
<form method="post" action="">
    <input type="text" name="name" value="<?= htmlspecialchars($name) ?>">
    <input type="email" name="email" value="<?= htmlspecialchars($email) ?>">
    <button type="submit">Submit</button>
</form>
<?php foreach ($errors as $error): ?>
    <p><?= htmlspecialchars($error) ?></p>
<?php endforeach; ?>
</body>
</html>

Output (initial page load, a plain GET request, before anything is submitted):

<!DOCTYPE html>
<html>
<body>
<form method="post" action="">
    <input type="text" name="name" value="">
    <input type="email" name="email" value="">
    <button type="submit">Submit</button>
</form>
</body>
</html>

Because the request method is GET on first load, the whole if block is skipped: $name, $email, and $errors stay at their defaults, so the form renders empty and no error paragraphs or thank-you message appear. Once the user submits with POST, the same script fills in $name/$email, validates them, and either prints a confirmation or redisplays the form with the entered values preserved (via value="<?= htmlspecialchars($name) ?>") and any errors listed underneath.

Example 2: Validating and sanitizing input in a reusable function

<?php
function validateForm(array $input): array
{
    $clean = [];
    $errors = [];

    $clean['username'] = trim($input['username'] ?? '');
    $clean['age'] = filter_var($input['age'] ?? '', FILTER_VALIDATE_INT);

    if ($clean['username'] === '' || strlen($clean['username']) < 3) {
        $errors[] = 'Username must be at least 3 characters.';
    }

    if ($clean['age'] === false || $clean['age'] < 13) {
        $errors[] = 'Age must be a number 13 or older.';
    }

    return [$clean, $errors];
}

$simulatedPost = ['username' => 'jo', 'age' => '9'];
[$data, $errors] = validateForm($simulatedPost);

foreach ($errors as $error) {
    echo $error . PHP_EOL;
}

echo 'Clean username: ' . var_export($data['username'], true) . PHP_EOL;
echo 'Clean age: ' . var_export($data['age'], true) . PHP_EOL;

Output:

Username must be at least 3 characters.
Age must be a number 13 or older.
Clean username: 'jo'
Clean age: 9

This pattern separates validation from the superglobals themselves — validateForm() accepts a plain array, so it works identically whether you feed it $_POST in production or a hand-built array in a test. filter_var(..., FILTER_VALIDATE_INT) converts '9' to the integer 9 (or returns false if the value isn’t a valid integer), which is far safer than comparing raw strings.

Example 3: Checkboxes and a select menu

<?php
$interests = [];
$country = '';
$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $interests = $_POST['interests'] ?? [];
    $country = $_POST['country'] ?? '';
    $message = empty($interests)
        ? 'Please select at least one interest.'
        : 'You selected: ' . implode(', ', array_map('htmlspecialchars', $interests));
}

$countries = ['us' => 'United States', 'ca' => 'Canada', 'uk' => 'United Kingdom'];
?>
<form method="post" action="">
<label><input type="checkbox" name="interests[]" value="php"> PHP</label>
<label><input type="checkbox" name="interests[]" value="js"> JavaScript</label>
<select name="country">
<?php foreach ($countries as $code => $label): ?>
<option value="<?= htmlspecialchars($code) ?>"><?= htmlspecialchars($label) ?></option>
<?php endforeach; ?>
</select>
<button type="submit">Send</button>
</form>
<?php if ($message !== ''): ?>
<p><?= htmlspecialchars($message) ?></p>
<?php endif; ?>

Output (plain GET request, before submission):

<form method="post" action="">
<label><input type="checkbox" name="interests[]" value="php"> PHP</label>
<label><input type="checkbox" name="interests[]" value="js"> JavaScript</label>
<select name="country">
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
</select>
<button type="submit">Send</button>
</form>

Using name="interests[]" with square brackets tells PHP to collect every checked box into a numerically-indexed array under $_POST['interests'], instead of overwriting a single value each time. If nothing is checked, the key may be missing entirely, which is why the code defaults it to an empty array with $_POST['interests'] ?? [] before calling empty() on it.

How It Works Step by Step

Behind the scenes, a form submission goes through the same sequence every time:

  • The browser collects every field’s name and current value at the moment submit is clicked.
  • It encodes them — as a query string for GET, or as the request body (URL-encoded or multipart) for POST.
  • It sends an HTTP request to the action URL with that data attached.
  • The PHP engine, before your script’s first line executes, parses the request and populates $_GET and/or $_POST (and $_FILES for uploads) automatically — you never parse this by hand.
  • Your script reads the relevant superglobal, validates and sanitizes each value, and only then acts on it — storing it, emailing it, or rendering a response.
  • If validation fails, a well-built form redisplays itself with the user’s input preserved and clear error messages, rather than discarding everything they typed.

Common Mistakes

Mistake 1: Echoing form input without escaping it

<?php
$comment = $_POST['comment'] ?? '';
echo "<div class='comment'>$comment</div>";

If a visitor submits <script>alert('hacked')</script> as their comment, this code prints that script tag straight into the page, and the browser executes it — a classic stored/reflected XSS vulnerability. User input must never be trusted as safe HTML.

<?php
$comment = $_POST['comment'] ?? '';
echo "<div class='comment'>" . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . "</div>";

htmlspecialchars() converts <, >, &, and quotes into HTML entities, so the browser displays the text instead of running it. Escape on output, every single time you print user-supplied data into HTML.

Mistake 2: Reading fields without checking they exist

<?php
$age = $_POST['age'];
if ($age > 18) {
    echo "Welcome!";
}

If the form is loaded with GET, or the age field is missing or renamed, this throws an “Undefined array key” warning and the comparison silently misbehaves. Client-side required attributes don’t stop a request forged without them (via curl, a browser devtools edit, or a disabled-JS request), so server code must never assume a key is present.

<?php
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT);
if ($age !== null && $age !== false && $age > 18) {
    echo "Welcome!";
}

filter_input() reads directly from the request and returns null if the field is missing or false if it fails validation, so both cases are handled explicitly instead of triggering a warning.

Best Practices

  • Always check $_SERVER['REQUEST_METHOD'] in self-processing forms so validation only runs on an actual submission.
  • Validate and sanitize on the server for every field, even if you also validate in JavaScript — client-side checks can always be bypassed.
  • Use htmlspecialchars() (or an equivalent) on every piece of user input before echoing it back into HTML.
  • Prefer filter_var()/filter_input() over manual string checks for emails, integers, and URLs.
  • Use method="post" for anything that changes state (creating an account, placing an order, deleting data); reserve GET for reads and searches.
  • Add a CSRF token (a random, session-bound value hidden in the form and checked on submit) to protect state-changing forms from cross-site request forgery.
  • Repopulate the user’s previously entered values when validation fails, so they don’t have to retype everything.
  • For file uploads, set enctype="multipart/form-data" on the form and always re-check $_FILES size, type, and extension server-side — never trust the client-reported MIME type alone.

Practice Exercises

  • Build a self-processing feedback form with name, email, and message fields. Require all three, validate the email with filter_var, and redisplay the form with the previous values and an error list if validation fails.
  • Write a function sanitizeInput(string $value): string that trims whitespace and escapes HTML special characters, then use it to safely display a search query submitted via $_GET['q'].
  • Extend Example 3 so that if the user picks “United States” from the country <select>, a second required field, state, must also be filled in before the form is considered valid.

Summary

  • $_GET holds query-string data; $_POST holds body data from method="post" submissions; PHP populates both automatically before your script runs.
  • Use GET for retrieving/filtering data and POST for anything that changes server state or is sensitive.
  • Self-processing forms check $_SERVER['REQUEST_METHOD'] to decide whether to render a blank form or validate a submission.
  • Always validate and sanitize on the server, regardless of any client-side checks.
  • Escape all user input with htmlspecialchars() before echoing it into HTML to prevent XSS.
  • Use square-bracket field names like interests[] to collect multiple values such as checkboxes into a PHP array.
  • Protect state-changing forms with CSRF tokens and never trust $_FILES data without server-side re-validation.