Skip to main content

๐Ÿ“‹ Lesson 11: Working with Forms ($_GET & $_POST)

Forms are the primary way users interact with web applications โ€” logging in, searching, registering, submitting orders. In this lesson, you'll learn how PHP receives and processes form data, the difference between GET and POST requests, and how to build forms that remember what the user typed.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Build HTML forms that submit data to PHP scripts
  • Retrieve form data using the $_GET and $_POST superglobals
  • Explain when to use GET vs. POST and the implications of each
  • Process various input types including text, checkboxes, radio buttons, selects, and textareas
  • Handle multi-value inputs (checkboxes and multi-selects)
  • Build sticky forms that preserve user input after submission

Estimated Time: 50 minutes

Prerequisites: Lessons 1โ€“10 (core PHP), basic HTML knowledge

๐Ÿ“‘ In This Lesson

How Forms Work with PHP

Until now, all the data in our PHP scripts has been hardcoded. Real web applications need to receive input from users โ€” and HTML forms are the standard way to do that. Here's the basic flow:

sequenceDiagram participant U as User (Browser) participant S as Server (PHP) U->>S: 1. Requests page with form S->>U: 2. Sends HTML form U->>U: 3. User fills out form U->>S: 4. Submits form (GET or POST) S->>S: 5. PHP reads $_GET or $_POST S->>S: 6. Processes data S->>U: 7. Sends response (result page)

The Basics: An HTML Form That Talks to PHP

Here's the simplest possible form-to-PHP example. Two files working together:

File: greet_form.html โ€” the form page:

<!DOCTYPE html>
<html>
<head><title>Greeting Form</title></head>
<body>
    <h1>Enter Your Name</h1>
    <form action="greet.php" method="post">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
        <button type="submit">Greet Me</button>
    </form>
</body>
</html>

File: greet.php โ€” the processing script:

<?php
// Read the "name" field from the submitted form
$name = $_POST["name"];
echo "<h1>Hello, $name!</h1>";
echo "<p>Welcome to our site.</p>";

Key Form Attributes

Every HTML form has two essential attributes that control where and how data is sent:

Attribute Purpose Example
action The URL of the PHP script that will process the form action="process.php"
method How data is sent: get or post method="post"

โš ๏ธ The name Attribute Is Required!

Every input that should send data to PHP must have a name attribute. The id attribute is for CSS/JavaScript; PHP only sees name. If an input has no name, its value won't appear in $_GET or $_POST.

<!-- โœ… PHP receives this โ€” has name="email" -->
<input type="email" name="email" id="email">

<!-- โŒ PHP does NOT receive this โ€” no name attribute -->
<input type="email" id="email">

GET vs. POST

The two HTTP methods you'll use for forms โ€” GET and POST โ€” behave very differently. Choosing the right one matters for security, usability, and correctness.

Feature GET POST
Data location URL query string (?name=value) Request body (hidden from URL)
Visible in URL? Yes โ€” visible in address bar, browser history, server logs No โ€” not visible in the URL
Bookmarkable? Yes โ€” the full URL with data can be saved/shared No โ€” bookmarking only saves the URL, not the data
Data size limit ~2,000 characters (browser-dependent) No practical limit (configurable on server)
Caching Can be cached by browser Never cached
Back button Safe โ€” re-displays the page Browser may warn "resubmit form data"
PHP superglobal $_GET $_POST
Best for Search, filtering, non-sensitive queries Login, registration, data changes, sensitive data

๐Ÿ“– Rule of Thumb

GET is for reading or looking up data โ€” search queries, filters, page navigation. The user should be able to bookmark the result.

POST is for writing or changing data โ€” creating accounts, submitting orders, updating profiles, anything with passwords or personal info.

GET in the URL

When a form uses method="get", the browser appends the data to the URL as a query string:

https://example.com/search.php?q=php+tutorials&category=web&page=1
                                               โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ query string โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each field becomes a key=value pair, separated by &. Spaces become + (or %20), and special characters are URL-encoded.

POST in the Body

When a form uses method="post", the same data is sent in the HTTP request body โ€” invisible to the user and not stored in browser history:

POST /register.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

username=alice&email=alice%40example.com&password=secret123

โš ๏ธ POST Is Not Encryption!

POST hides data from the URL bar, but the data is not encrypted. Anyone monitoring the network can read it. For truly secure transmission, you need HTTPS (which we cover in Lesson 23). POST just prevents the data from showing up in browser history and server access logs.

$_GET โ€” Query String Data

$_GET is a superglobal associative array that PHP automatically fills with data from the URL's query string. You don't need to parse the URL yourself โ€” PHP does it for you.

A Search Form Example

File: search_form.html

<form action="search.php" method="get">
    <label for="q">Search:</label>
    <input type="text" id="q" name="q" placeholder="Enter search term...">

    <label for="category">Category:</label>
    <select id="category" name="category">
        <option value="all">All</option>
        <option value="tutorials">Tutorials</option>
        <option value="articles">Articles</option>
        <option value="videos">Videos</option>
    </select>

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

File: search.php

<?php
// When the user submits: search.php?q=php+forms&category=tutorials

// Read individual values from $_GET
$query    = $_GET["q"] ?? "";         // "php forms"
$category = $_GET["category"] ?? "all"; // "tutorials"

echo "<h1>Search Results</h1>";
echo "<p>Searching for: <strong>" . htmlspecialchars($query) . "</strong></p>";
echo "<p>Category: <strong>" . htmlspecialchars($category) . "</strong></p>";

// Check if any search was actually submitted
if ($query === "") {
    echo "<p>Please enter a search term.</p>";
} else {
    // In a real app, you'd query a database here
    echo "<p>Showing results for <em>" . htmlspecialchars($query) . "</em>...</p>";
}

โœ… Always Use htmlspecialchars() for Output!

Notice we wrap user input in htmlspecialchars() before echoing it. This prevents Cross-Site Scripting (XSS) attacks where malicious users inject HTML or JavaScript through form inputs. We'll cover this in depth in Lesson 23, but start the habit now:

// BAD โ€” vulnerable to XSS!
echo "Hello, " . $_GET["name"];

// GOOD โ€” safe output
echo "Hello, " . htmlspecialchars($_GET["name"] ?? "");

Checking if Data Exists

Users might visit your page directly (without submitting a form), so always check if the expected data exists:

<?php
// Method 1: Null coalescing operator (preferred)
$query = $_GET["q"] ?? "";

// Method 2: isset()
if (isset($_GET["q"])) {
    $query = $_GET["q"];
} else {
    $query = "";
}

// Method 3: empty() โ€” checks both existence AND emptiness
if (!empty($_GET["q"])) {
    // Process the search
    echo "Searching for: " . htmlspecialchars($_GET["q"]);
} else {
    echo "No search term provided.";
}

// Method 4: array_key_exists() โ€” for when "" (empty string) is valid
if (array_key_exists("q", $_GET)) {
    // Key exists, even if the value is ""
}

$_GET with Pagination Links

$_GET isn't only for forms โ€” any URL with a query string populates it. This is how pagination, filters, and sorting work:

<?php
// URL: products.php?page=3&sort=price&order=asc
$page  = (int) ($_GET["page"] ?? 1);
$sort  = $_GET["sort"] ?? "name";
$order = $_GET["order"] ?? "asc";

// Validate the values
$validSorts  = ["name", "price", "date"];
$validOrders = ["asc", "desc"];

if (!in_array($sort, $validSorts)) {
    $sort = "name";
}
if (!in_array($order, $validOrders)) {
    $order = "asc";
}

echo "<p>Page $page, sorted by $sort ($order)</p>";

// Build pagination links
$prevPage = max(1, $page - 1);
$nextPage = $page + 1;

echo "<a href=\"products.php?page=$prevPage&sort=$sort&order=$order\">Previous</a> | ";
echo "<a href=\"products.php?page=$nextPage&sort=$sort&order=$order\">Next</a>";

$_POST โ€” Form Body Data

$_POST works exactly like $_GET, but it reads data sent in the request body instead of the URL. Use it for forms with method="post".

A Registration Form Example

File: register_form.html

<form action="register.php" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" required>

    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required>

    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required>

    <label for="confirm">Confirm Password:</label>
    <input type="password" id="confirm" name="confirm" required>

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

File: register.php

<?php
// Read POST data
$username = trim($_POST["username"] ?? "");
$email    = trim($_POST["email"] ?? "");
$password = $_POST["password"] ?? "";
$confirm  = $_POST["confirm"] ?? "";

// Basic validation
$errors = [];

if ($username === "") {
    $errors[] = "Username is required.";
}
if ($email === "") {
    $errors[] = "Email is required.";
}
if (strlen($password) < 8) {
    $errors[] = "Password must be at least 8 characters.";
}
if ($password !== $confirm) {
    $errors[] = "Passwords do not match.";
}

// Display results
if (count($errors) > 0) {
    echo "<h2>Registration Failed</h2>";
    echo "<ul>";
    foreach ($errors as $error) {
        echo "<li>" . htmlspecialchars($error) . "</li>";
    }
    echo "</ul>";
    echo "<p><a href='register_form.html'>Go back and try again</a></p>";
} else {
    echo "<h2>Registration Successful!</h2>";
    echo "<p>Welcome, " . htmlspecialchars($username) . "!</p>";
    // In a real app: hash password, save to database, start session
}

Detecting Form Submission

Sometimes you need to know whether the user actually submitted a form, or just arrived at the page. A common technique is to check the request method:

<?php
// Method 1: Check $_SERVER["REQUEST_METHOD"]
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // Form was submitted
    $name = trim($_POST["name"] ?? "");
    // ... process form
} else {
    // Page loaded normally (GET request) โ€” show the form
}

// Method 2: Check for a specific form field
if (isset($_POST["username"])) {
    // The registration form was submitted
}

// Method 3: Use a hidden field as a "form ID"
// In the HTML: <input type="hidden" name="form_action" value="register">
if (($_POST["form_action"] ?? "") === "register") {
    // The registration form was submitted
}

โœ… Prefer $_SERVER["REQUEST_METHOD"]

Checking the request method is the most reliable approach. It works even if the form has no fields filled in, and it clearly communicates intent in your code.

Processing Different Input Types

HTML offers many input types. Here's how each one sends data to PHP:

Text, Email, Password, Number

These all arrive as strings in $_POST or $_GET:

<?php
// HTML:
// <input type="text" name="city" value="Portland">
// <input type="email" name="email" value="a@b.com">
// <input type="number" name="age" value="30">
// <input type="password" name="pass" value="secret">

$city  = $_POST["city"] ?? "";     // "Portland" (string)
$email = $_POST["email"] ?? "";    // "a@b.com" (string)
$age   = (int) ($_POST["age"] ?? 0); // 30 (cast to int!)
$pass  = $_POST["pass"] ?? "";     // "secret" (string)
โš ๏ธ Important: Even type="number" sends a string to PHP. Always cast with (int) or (float) if you need a numeric type.

Textarea

<label for="bio">Bio:</label>
<textarea id="bio" name="bio" rows="5" cols="40"></textarea>
<?php
$bio = trim($_POST["bio"] ?? "");
// Contains the full text, including newlines (\n)

// To display in HTML, convert newlines to <br> tags
echo nl2br(htmlspecialchars($bio));

Radio Buttons

Radio buttons in a group share the same name. Only the selected one's value is sent:

<p>Preferred contact method:</p>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
<label><input type="radio" name="contact" value="text"> Text</label>
<?php
$contact = $_POST["contact"] ?? "";
// "email", "phone", or "text" โ€” whichever was selected
// "" if nothing was selected (no default checked)

// Validate against known values
$validMethods = ["email", "phone", "text"];
if (!in_array($contact, $validMethods)) {
    $contact = "email"; // Default fallback
}

Single Checkbox

A single checkbox is either sent or not. If checked, PHP receives its value. If unchecked, the key doesn't exist at all:

<label>
    <input type="checkbox" name="agree" value="yes">
    I agree to the terms and conditions
</label>
<?php
// If checked:  $_POST["agree"] === "yes"
// If unchecked: "agree" key does NOT exist in $_POST

$agreed = isset($_POST["agree"]); // true or false
// OR
$agreed = ($_POST["agree"] ?? "") === "yes";

โš ๏ธ Unchecked Checkboxes Send Nothing

This is a common source of bugs. An unchecked checkbox doesn't send value="no" โ€” it sends nothing at all. The key won't exist in $_POST. Always use isset() or null coalescing to handle this.

Select (Dropdown)

<label for="country">Country:</label>
<select id="country" name="country">
    <option value="">-- Select --</option>
    <option value="us">United States</option>
    <option value="ca">Canada</option>
    <option value="uk">United Kingdom</option>
    <option value="ph">Philippines</option>
</select>
<?php
$country = $_POST["country"] ?? "";
// "us", "ca", "uk", "ph", or "" (if "-- Select --" was left)

if ($country === "") {
    echo "Please select a country.";
}

Hidden Inputs

Hidden inputs pass data that the user doesn't see but PHP needs:

<!-- Pass a product ID with the form -->
<input type="hidden" name="product_id" value="42">

<!-- Pass the page they came from -->
<input type="hidden" name="redirect_to" value="/dashboard">
<?php
$productId  = (int) ($_POST["product_id"] ?? 0);
$redirectTo = $_POST["redirect_to"] ?? "/";

// โš ๏ธ Hidden fields can be edited by users (via browser dev tools)!
// Always validate hidden field values server-side.

Multi-Value Inputs

Some form controls let users select multiple values. To receive these as an array in PHP, you need to add [] to the name attribute.

Checkbox Groups

<p>Select your favorite languages:</p>
<label><input type="checkbox" name="languages[]" value="php"> PHP</label>
<label><input type="checkbox" name="languages[]" value="js"> JavaScript</label>
<label><input type="checkbox" name="languages[]" value="python"> Python</label>
<label><input type="checkbox" name="languages[]" value="csharp"> C#</label>
<label><input type="checkbox" name="languages[]" value="rust"> Rust</label>
<?php
// The [] in name="languages[]" tells PHP to expect an array
$languages = $_POST["languages"] ?? [];

// If user checked PHP, JavaScript, and Rust:
// $languages = ["php", "js", "rust"]

// If nothing was checked:
// "languages" key won't exist in $_POST, so we default to []

echo "You selected " . count($languages) . " language(s):\n";
foreach ($languages as $lang) {
    echo "  - " . htmlspecialchars($lang) . "\n";
}

// Validate each value
$validLanguages = ["php", "js", "python", "csharp", "rust"];
$languages = array_filter($languages, fn($l) => in_array($l, $validLanguages));

โš ๏ธ Don't Forget the [] Brackets!

Without [] in the name, PHP only receives the last checked value. With name="languages" (no brackets), checking PHP, JS, and Rust would only give you "rust".

<!-- BAD โ€” only sends last checked value -->
<input type="checkbox" name="languages" value="php">
<input type="checkbox" name="languages" value="js">

<!-- GOOD โ€” sends all checked values as an array -->
<input type="checkbox" name="languages[]" value="php">
<input type="checkbox" name="languages[]" value="js">

Multi-Select Dropdown

<label for="skills">Skills (hold Ctrl/Cmd to select multiple):</label>
<select id="skills" name="skills[]" multiple size="5">
    <option value="html">HTML/CSS</option>
    <option value="js">JavaScript</option>
    <option value="php">PHP</option>
    <option value="mysql">MySQL</option>
    <option value="react">React</option>
    <option value="node">Node.js</option>
</select>
<?php
$skills = $_POST["skills"] ?? [];
// Same as checkboxes โ€” an array of selected values
echo "Selected skills: " . implode(", ", array_map("htmlspecialchars", $skills));

Putting It All Together

Here's a complete example showing multiple input types in one form:

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $name      = trim($_POST["name"] ?? "");
    $email     = trim($_POST["email"] ?? "");
    $age       = (int) ($_POST["age"] ?? 0);
    $gender    = $_POST["gender"] ?? "";
    $languages = $_POST["languages"] ?? [];
    $bio       = trim($_POST["bio"] ?? "");
    $newsletter = isset($_POST["newsletter"]);

    echo "<h2>Profile Summary</h2>";
    echo "<p><strong>Name:</strong> " . htmlspecialchars($name) . "</p>";
    echo "<p><strong>Email:</strong> " . htmlspecialchars($email) . "</p>";
    echo "<p><strong>Age:</strong> $age</p>";
    echo "<p><strong>Gender:</strong> " . htmlspecialchars($gender) . "</p>";
    echo "<p><strong>Languages:</strong> " . implode(", ", array_map("htmlspecialchars", $languages)) . "</p>";
    echo "<p><strong>Bio:</strong> " . nl2br(htmlspecialchars($bio)) . "</p>";
    echo "<p><strong>Newsletter:</strong> " . ($newsletter ? "Yes" : "No") . "</p>";
}

Sticky Forms

Imagine filling out a long registration form, making one mistake, and having every field reset to blank. Frustrating! Sticky forms preserve the user's input so they only need to fix what was wrong.

๐Ÿ“– Definition

Sticky Form: A form that remembers and redisplays the values the user previously entered after a submission that resulted in errors or needed corrections.

Making Text Inputs Sticky

Set the value attribute to the previously submitted value:

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

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

    if ($name === "") {
        $errors[] = "Name is required.";
    }
    if ($email === "" || !str_contains($email, "@")) {
        $errors[] = "Valid email is required.";
    }

    if (empty($errors)) {
        // Success! Process the data and redirect
        echo "<p>Thank you, " . htmlspecialchars($name) . "!</p>";
        exit;
    }
}
?>

<!-- Display errors if any -->
<?php if (!empty($errors)): ?>
    <div class="errors">
        <ul>
        <?php foreach ($errors as $error): ?>
            <li><?= htmlspecialchars($error) ?></li>
        <?php endforeach; ?>
        </ul>
    </div>
<?php endif; ?>

<!-- The form โ€” values are "sticky" -->
<form method="post">
    <label for="name">Name:</label>
    <input type="text" id="name" name="name"
           value="<?= htmlspecialchars($name) ?>">

    <label for="email">Email:</label>
    <input type="email" id="email" name="email"
           value="<?= htmlspecialchars($email) ?>">

    <button type="submit">Submit</button>
</form>
๐Ÿ’ก Key Insight: Notice how htmlspecialchars() is used in the value attribute. This isn't just for display โ€” it prevents a user from injecting HTML into your form by entering something like "><script>alert('hacked')</script> as their name.

Making Textareas Sticky

Textareas don't use a value attribute โ€” the content goes between the tags:

<label for="bio">Bio:</label>
<textarea id="bio" name="bio" rows="5"><?= htmlspecialchars($bio) ?></textarea>
<!-- โš ๏ธ No space between > and <?= or you'll get extra whitespace! -->

Making Radio Buttons Sticky

Add the checked attribute to the previously selected option:

<?php $contact = $_POST["contact"] ?? "email"; ?>

<label>
    <input type="radio" name="contact" value="email"
        <?= $contact === "email" ? "checked" : "" ?>>
    Email
</label>
<label>
    <input type="radio" name="contact" value="phone"
        <?= $contact === "phone" ? "checked" : "" ?>>
    Phone
</label>
<label>
    <input type="radio" name="contact" value="text"
        <?= $contact === "text" ? "checked" : "" ?>>
    Text
</label>

Making Checkboxes Sticky

<?php $languages = $_POST["languages"] ?? []; ?>

<label>
    <input type="checkbox" name="languages[]" value="php"
        <?= in_array("php", $languages) ? "checked" : "" ?>>
    PHP
</label>
<label>
    <input type="checkbox" name="languages[]" value="js"
        <?= in_array("js", $languages) ? "checked" : "" ?>>
    JavaScript
</label>
<label>
    <input type="checkbox" name="languages[]" value="python"
        <?= in_array("python", $languages) ? "checked" : "" ?>>
    Python
</label>

Making Select Dropdowns Sticky

<?php $country = $_POST["country"] ?? ""; ?>

<select name="country">
    <option value="">-- Select --</option>
    <option value="us" <?= $country === "us" ? "selected" : "" ?>>United States</option>
    <option value="ca" <?= $country === "ca" ? "selected" : "" ?>>Canada</option>
    <option value="uk" <?= $country === "uk" ? "selected" : "" ?>>United Kingdom</option>
    <option value="ph" <?= $country === "ph" ? "selected" : "" ?>>Philippines</option>
</select>

โœ… Sticky Form Cheat Sheet

Input Type How to Make Sticky
Text / Email / Password / Number value="<?= htmlspecialchars($var) ?>"
Textarea Content between tags: ><?= htmlspecialchars($var) ?></textarea>
Radio <?= $var === "value" ? "checked" : "" ?>
Checkbox (single) <?= isset($_POST["key"]) ? "checked" : "" ?>
Checkbox (group) <?= in_array("value", $arr) ? "checked" : "" ?>
Select <?= $var === "value" ? "selected" : "" ?>

Self-Processing Forms

In the examples so far, the form and the processing script were separate files. In practice, it's common to combine them into a single PHP file that both displays the form and processes it. This is called a self-processing (or self-submitting) form.

The Pattern

<?php
// --- Step 1: Initialize variables ---
$name    = "";
$email   = "";
$message = "";
$errors  = [];
$success = false;

// --- Step 2: Process form if submitted ---
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // Read and trim input
    $name    = trim($_POST["name"] ?? "");
    $email   = trim($_POST["email"] ?? "");
    $message = trim($_POST["message"] ?? "");

    // Validate
    if ($name === "") {
        $errors[] = "Name is required.";
    }
    if ($email === "" || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "A valid email is required.";
    }
    if ($message === "") {
        $errors[] = "Message is required.";
    }

    // If no errors, process the data
    if (empty($errors)) {
        // Save to database, send email, etc.
        $success = true;
    }
}
?>
<!DOCTYPE html>
<html>
<head><title>Contact Us</title></head>
<body>
    <h1>Contact Us</h1>

    <?php if ($success): ?>
        <div class="success">
            <p>Thank you, <?= htmlspecialchars($name) ?>! Your message has been sent.</p>
        </div>
    <?php else: ?>

        <?php if (!empty($errors)): ?>
            <div class="errors">
                <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?= htmlspecialchars($error) ?></li>
                <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>

        <form method="post" action="">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name"
                   value="<?= htmlspecialchars($name) ?>">

            <label for="email">Email:</label>
            <input type="email" id="email" name="email"
                   value="<?= htmlspecialchars($email) ?>">

            <label for="message">Message:</label>
            <textarea id="message" name="message" rows="5"><?= htmlspecialchars($message) ?></textarea>

            <button type="submit">Send Message</button>
        </form>
    <?php endif; ?>
</body>
</html>
flowchart TD A[User visits contact.php] --> B{Is it a POST request?} B -->|No - first visit| C[Show empty form] B -->|Yes - form submitted| D[Read & validate input] D --> E{Errors?} E -->|Yes| F[Show form with errors + sticky values] E -->|No| G[Process data & show success message] F --> H[User fixes errors & resubmits] H --> D

Why action=""?

Setting action="" (empty string) makes the form submit to the same URL โ€” the current page. You can also use action="<?= htmlspecialchars($_SERVER["PHP_SELF"]) ?>", but the empty string is simpler and just as effective.

โš ๏ธ Don't Use $_SERVER["PHP_SELF"] Unescaped!

If you do use PHP_SELF, always wrap it in htmlspecialchars(). An attacker could craft a URL like /contact.php/"><script>alert('xss')</script> to inject code into your form's action attribute.

<!-- BAD โ€” XSS vulnerability -->
<form action="<?= $_SERVER["PHP_SELF"] ?>" method="post">

<!-- GOOD โ€” escaped -->
<form action="<?= htmlspecialchars($_SERVER["PHP_SELF"]) ?>" method="post">

<!-- BEST โ€” just use empty action -->
<form action="" method="post">

Post/Redirect/Get (PRG) Pattern

After a successful POST form submission, if the user refreshes the page, the browser will ask "Resubmit form data?" and may duplicate the action (double order, double message, etc.). The PRG pattern prevents this:

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $name = trim($_POST["name"] ?? "");
    // ... validate ...

    if (empty($errors)) {
        // Process the data (save to DB, send email, etc.)

        // Redirect to prevent resubmission on refresh
        header("Location: contact.php?success=1");
        exit; // Always exit after header redirect!
    }
}

// Check for success message from redirect
if (isset($_GET["success"])) {
    echo "<p class='success'>Your message has been sent!</p>";
}
// ... show form ...

โœ… PRG is a Best Practice

The Post/Redirect/Get pattern is used by virtually every professional web application. After processing a POST request successfully, redirect the user with header("Location: ...") followed by exit. The redirect converts the POST into a GET, so refreshing the page won't resubmit the form.

Hands-On Exercises

๐Ÿ‹๏ธ Exercise 1: Calculator Form

Objective: Build a self-processing calculator that takes two numbers and an operation.

Instructions:

  1. Create a form with two number inputs and a select dropdown for the operation (+, -, *, /)
  2. Use method="post" and make the form self-processing
  3. Display the result below the form after submission
  4. Make the form sticky โ€” preserve the inputs after submission
  5. Handle division by zero with an error message
๐Ÿ’ก Hint

Use a match expression for the operation. Cast inputs to (float). Check for $num2 == 0 before dividing.

โœ… Solution
<?php
$num1   = "";
$num2   = "";
$op     = "+";
$result = null;
$error  = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $num1 = $_POST["num1"] ?? "";
    $num2 = $_POST["num2"] ?? "";
    $op   = $_POST["op"] ?? "+";

    if ($num1 === "" || $num2 === "" || !is_numeric($num1) || !is_numeric($num2)) {
        $error = "Please enter valid numbers.";
    } elseif ($op === "/" && (float)$num2 == 0) {
        $error = "Cannot divide by zero!";
    } else {
        $a = (float) $num1;
        $b = (float) $num2;
        $result = match ($op) {
            "+" => $a + $b,
            "-" => $a - $b,
            "*" => $a * $b,
            "/" => $a / $b,
            default => null,
        };
    }
}
?>
<h2>Calculator</h2>

<?php if ($error): ?>
    <p style="color: red;"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>

<form method="post" action="">
    <input type="number" name="num1" step="any"
           value="<?= htmlspecialchars($num1) ?>" placeholder="First number">

    <select name="op">
        <option value="+" <?= $op === "+" ? "selected" : "" ?>>+</option>
        <option value="-" <?= $op === "-" ? "selected" : "" ?>>-</option>
        <option value="*" <?= $op === "*" ? "selected" : "" ?>>ร—</option>
        <option value="/" <?= $op === "/" ? "selected" : "" ?>>รท</option>
    </select>

    <input type="number" name="num2" step="any"
           value="<?= htmlspecialchars($num2) ?>" placeholder="Second number">

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

<?php if ($result !== null): ?>
    <p><strong>Result:</strong>
    <?= htmlspecialchars($num1) ?> <?= htmlspecialchars($op) ?>
    <?= htmlspecialchars($num2) ?> = <strong><?= $result ?></strong></p>
<?php endif; ?>

๐Ÿ‹๏ธ Exercise 2: Survey Form with All Input Types

Objective: Build a complete survey form that uses every input type covered in this lesson.

Instructions:

  1. Create a self-processing survey with these fields:
    • Name (text)
    • Email (email)
    • Age (number)
    • Gender (radio buttons)
    • Favorite colors (checkbox group โ€” at least 5 options)
    • Country (select dropdown)
    • Comments (textarea)
    • Subscribe to newsletter (single checkbox)
  2. Validate: name and email are required, age must be 13โ€“120
  3. Make every field sticky
  4. On success, display a formatted summary of all responses
๐Ÿ’ก Hint

Initialize all variables at the top. Use in_array() for sticky checkboxes and radios. Remember the [] brackets for checkbox groups.

โœ… Solution
<?php
$name       = "";
$email      = "";
$age        = "";
$gender     = "";
$colors     = [];
$country    = "";
$comments   = "";
$newsletter = false;
$errors     = [];
$success    = false;

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $name       = trim($_POST["name"] ?? "");
    $email      = trim($_POST["email"] ?? "");
    $age        = $_POST["age"] ?? "";
    $gender     = $_POST["gender"] ?? "";
    $colors     = $_POST["colors"] ?? [];
    $country    = $_POST["country"] ?? "";
    $comments   = trim($_POST["comments"] ?? "");
    $newsletter = isset($_POST["newsletter"]);

    // Validate
    if ($name === "") { $errors[] = "Name is required."; }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = "Valid email is required."; }
    if ($age === "" || (int)$age < 13 || (int)$age > 120) { $errors[] = "Age must be 13โ€“120."; }

    // Validate multi-values
    $validColors = ["red", "blue", "green", "yellow", "purple", "orange"];
    $colors = array_filter($colors, fn($c) => in_array($c, $validColors));

    if (empty($errors)) { $success = true; }
}
?>

<?php if ($success): ?>
    <h2>Survey Summary</h2>
    <p><strong>Name:</strong> <?= htmlspecialchars($name) ?></p>
    <p><strong>Email:</strong> <?= htmlspecialchars($email) ?></p>
    <p><strong>Age:</strong> <?= (int)$age ?></p>
    <p><strong>Gender:</strong> <?= htmlspecialchars($gender ?: "Not specified") ?></p>
    <p><strong>Colors:</strong> <?= implode(", ", array_map("htmlspecialchars", $colors)) ?: "None" ?></p>
    <p><strong>Country:</strong> <?= htmlspecialchars($country ?: "Not specified") ?></p>
    <p><strong>Comments:</strong> <?= nl2br(htmlspecialchars($comments)) ?: "None" ?></p>
    <p><strong>Newsletter:</strong> <?= $newsletter ? "Yes" : "No" ?></p>
<?php else: ?>

    <?php if (!empty($errors)): ?>
        <div style="color:red"><ul>
        <?php foreach ($errors as $e): ?>
            <li><?= htmlspecialchars($e) ?></li>
        <?php endforeach; ?>
        </ul></div>
    <?php endif; ?>

    <form method="post" action="">
        <label>Name: <input type="text" name="name" value="<?= htmlspecialchars($name) ?>"></label><br>
        <label>Email: <input type="email" name="email" value="<?= htmlspecialchars($email) ?>"></label><br>
        <label>Age: <input type="number" name="age" min="13" max="120" value="<?= htmlspecialchars($age) ?>"></label><br>

        <p>Gender:</p>
        <label><input type="radio" name="gender" value="male" <?= $gender === "male" ? "checked" : "" ?>> Male</label>
        <label><input type="radio" name="gender" value="female" <?= $gender === "female" ? "checked" : "" ?>> Female</label>
        <label><input type="radio" name="gender" value="other" <?= $gender === "other" ? "checked" : "" ?>> Other</label>
        <label><input type="radio" name="gender" value="prefer_not" <?= $gender === "prefer_not" ? "checked" : "" ?>> Prefer not to say</label><br>

        <p>Favorite Colors:</p>
        <?php foreach (["red","blue","green","yellow","purple","orange"] as $c): ?>
            <label><input type="checkbox" name="colors[]" value="<?= $c ?>"
                <?= in_array($c, $colors) ? "checked" : "" ?>> <?= ucfirst($c) ?></label>
        <?php endforeach; ?><br>

        <label>Country:
        <select name="country">
            <option value="">-- Select --</option>
            <?php foreach (["us"=>"United States","ca"=>"Canada","uk"=>"UK","ph"=>"Philippines"] as $code => $label): ?>
                <option value="<?= $code ?>" <?= $country === $code ? "selected" : "" ?>><?= $label ?></option>
            <?php endforeach; ?>
        </select></label><br>

        <label>Comments:<br><textarea name="comments" rows="4" cols="40"><?= htmlspecialchars($comments) ?></textarea></label><br>

        <label><input type="checkbox" name="newsletter" <?= $newsletter ? "checked" : "" ?>> Subscribe to newsletter</label><br>

        <button type="submit">Submit Survey</button>
    </form>
<?php endif; ?>

๐ŸŽฏ Quick Quiz

Question 1: Which HTTP method should you use for a login form?

Question 2: What happens when an unchecked checkbox is submitted?

Question 3: Why do checkbox groups need [] in the name attribute?

Question 4: What does htmlspecialchars() do when used in a form's value attribute?

Question 5: What is the Post/Redirect/Get (PRG) pattern?

Summary

๐ŸŽ‰ Key Takeaways

  • HTML forms send data to PHP via the action and method attributes; every input needs a name attribute
  • GET sends data in the URL (bookmarkable, visible) โ€” use for searches, filters, navigation
  • POST sends data in the request body (hidden from URL) โ€” use for sensitive data, creating/updating resources
  • $_GET and $_POST are superglobal arrays that PHP fills automatically with submitted data
  • Always check if data exists with ??, isset(), or !empty() before using it
  • Always use htmlspecialchars() when outputting user data to prevent XSS
  • Multi-value inputs (checkbox groups, multi-selects) need [] in the name: name="items[]"
  • Sticky forms preserve user input by echoing values back into form fields with proper escaping
  • Self-processing forms combine the form and handler in one file using $_SERVER["REQUEST_METHOD"]
  • PRG pattern โ€” redirect after successful POST to prevent duplicate submissions on refresh

๐Ÿ“š Additional Resources

๐Ÿš€ What's Next?

Now that you can receive user input, you need to make sure it's safe and valid. In Lesson 12: Input Validation & Sanitization, you'll learn to use filter_var(), filter_input(), PHP's built-in validation filters, regular expressions for custom rules, and how to display meaningful error messages.

๐ŸŽ‰ Congratulations!

You've unlocked PHP's connection to the browser! From here on, everything you build will involve user interaction.