Building a PHP Web Form

HTML forms, GET versus POST, superglobals, and the self-submitting page pattern that every PHP app is built on.

PHP & the Web Beginner 13 min read Watch the video (4:26)

A page that only prints things is a brochure. A page that reads what someone typed is an application. That step is smaller than you think.

#The anatomy of a form

html
<form action="handle.php" method="post">
  <label for="name">Your name</label>
  <input type="text" id="name" name="name">

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

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

Three attributes carry all the meaning:

  • action — the URL that receives the data. Leave it empty (action="") and it posts back to the current page.
  • methodget or post. More on this in a second.
  • name — on each input. This is the one that matters. PHP receives your data keyed by name. An input without a name is invisible to the server. id is for the <label> and your CSS; it is never sent anywhere.

#GET versus POST

GETPOST
Where the data goesIn the URL: ?name=DomIn the request body
Visible in the address barYesNo
Bookmarkable / shareableYesNo
Size limit~2000 charactersEffectively none
Safe to repeatYesNo — resubmits the action
PHP reads it from$_GET$_POST

The rule that has held up for thirty years: GET to read, POST to change.

A search box is GET — you want that URL to be shareable. A login form, a comment box, a "delete my account" button are POST — you do not want the password in the browser history, and you do not want a page refresh to run it a second time.

#Superglobals

PHP hands you the incoming request through a set of arrays that are available in every scope, no import needed:

php
$_GET      // query string parameters
$_POST     // form body fields
$_REQUEST  // both, merged — avoid it, the ambiguity causes bugs
$_SERVER   // request metadata: method, IP, user agent, paths
$_FILES    // uploaded files
$_SESSION  // per-user data that survives page loads

They are plain associative arrays. $_POST["name"] is the value of the input named name.

#The self-submitting page

You can split the form and its handler across two files. Almost nobody does, because then the form has no way to redisplay what the user typed when something is wrong. One file that both shows the form and handles the submission is the standard shape:

php
<?php
$name = "";
$email = "";
$message = "";
$sent = false;

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $name = trim($_POST["name"] ?? "");
    $email = trim($_POST["email"] ?? "");
    $message = trim($_POST["message"] ?? "");
    $sent = true;
}
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Contact — Coding with Dom</title>
</head>
<body>
  <h1>Get in touch</h1>

  <?php if ($sent): ?>
    <p>Thanks, <?= htmlspecialchars($name) ?>. We will reply to
       <?= htmlspecialchars($email) ?>.</p>
  <?php endif; ?>

  <form method="post" action="">
    <p>
      <label for="name">Name</label><br>
      <input type="text" id="name" name="name" value="<?= htmlspecialchars($name) ?>">
    </p>
    <p>
      <label for="email">Email</label><br>
      <input type="email" id="email" name="email" value="<?= htmlspecialchars($email) ?>">
    </p>
    <p>
      <label for="message">Message</label><br>
      <textarea id="message" name="message" rows="5"><?= htmlspecialchars($message) ?></textarea>
    </p>
    <button type="submit">Send message</button>
  </form>
</body>
</html>

Four techniques in there are worth calling out individually.

#1. Check the method, not the field

php
if ($_SERVER["REQUEST_METHOD"] === "POST") {

This is the reliable way to tell a fresh page load from a submission. Checking if (isset($_POST["name"])) seems equivalent but fails on an unchecked checkbox — browsers do not send unchecked boxes at all, so the field is simply absent.

#2. ?? stops the undefined-key warnings

php
$name = trim($_POST["name"] ?? "");

The null coalescing operator means "this, or that if it does not exist." Without it, a missing key emits a warning that prints straight into your HTML. Use ?? on every superglobal read, every time.

trim() strips leading and trailing whitespace, so a name of " " becomes "" and your emptiness check actually works.

#3. Sticky fields

php
<input type="text" name="name" value="<?= htmlspecialchars($name) ?>">

Echoing the submitted value back into value means a failed submission does not wipe the form. Nothing makes users abandon a form faster than retyping a long message because one field was wrong. For <textarea>, the value goes between the tags, not in a value attribute.

#4. htmlspecialchars() on every echo

This is not optional, and it is the most important line in the file.

If a user types <script>alert('pwned')</script> into the name field and you echo it raw, that script runs in the browser of everyone who sees the page. That is cross-site scripting — XSS — and it is one of the most common vulnerabilities on the web.

htmlspecialchars() converts < into &lt;, > into &gt;, " into &quot; and & into &amp;. The browser then displays those characters instead of interpreting them as markup.

A shorter alias is worth defining at the top of any project:

php
function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

Then it is just <?= e($name) ?> everywhere — short enough that you will never be tempted to skip it. ENT_QUOTES also escapes single quotes, which matters when your value lands inside a single-quoted attribute.

#Checkboxes, radios and selects

These behave differently enough to catch people out:

html
<input type="checkbox" name="topics[]" value="python"> Python
<input type="checkbox" name="topics[]" value="php"> PHP
<input type="checkbox" name="topics[]" value="ruby"> Ruby

<input type="radio" name="level" value="beginner" checked> Beginner
<input type="radio" name="level" value="advanced"> Advanced

<select name="track">
  <option value="">Choose one…</option>
  <option value="python">Python</option>
</select>
php
$topics = $_POST["topics"] ?? [];   // an array, or empty if none ticked
$level  = $_POST["level"] ?? "";    // a single value
$track  = $_POST["track"] ?? "";

echo "You picked: " . e(implode(", ", $topics));

The [] on the checkbox name is what makes PHP collect them into an array. Without it, each checkbox overwrites the last and you only ever see one.

Remember: unchecked boxes are not submitted at all. Never assume the key exists.

#Post/Redirect/Get

There is one remaining flaw in the page above. After a successful submission, hitting refresh re-posts the form, and the browser helpfully asks "Confirm form resubmission?" — which is how people end up sending the same message five times.

The fix is a redirect after a successful POST:

php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // ... validate and save ...
    if (empty($errors)) {
        header("Location: contact.php?sent=1");
        exit;
    }
}

$justSent = isset($_GET["sent"]);

Now the browser lands on a plain GET request. Refreshing does nothing but reload a harmless page. This is the Post/Redirect/Get pattern and it is used in essentially every serious web application.

Check yourself

4 questions on what you just read. No score is kept — it is only here to catch the bits that did not stick.

  1. 1An input renders fine but its value never reaches PHP. What is missing?

    Show the answer

    A. The name attribute.

    PHP keys $_POST by name. id exists for the <label> and your CSS, and is never sent anywhere.

  2. 2Which check reliably tells a fresh page load from a submission?

    Show the answer

    A. $_SERVER["REQUEST_METHOD"] === "POST"

    Browsers do not send unchecked checkboxes at all, so testing a single field breaks the moment a box is left unticked. Test the method.

  3. 3When should htmlspecialchars() be applied to user data?

    Show the answer

    A. On output — every single time the value is printed.

    Escaping on input mangles your stored data and leaves you guessing which fields were already processed. Store what they typed; escape when you print it.

  4. 4Why redirect after a successful POST?

    Show the answer

    A. So a refresh re-runs a harmless GET instead of resubmitting the form.

    This is the Post/Redirect/Get pattern, and it is why you are not on your fifth identical support ticket. Remember exit; right after header().

#Key takeaways

  • The name attribute is what reaches the server. No name, no data.
  • GET to read, POST to change. Never send credentials via GET.
  • Detect submissions with $_SERVER["REQUEST_METHOD"], not isset() on a field.
  • Read superglobals with ?? to avoid undefined-key warnings.
  • htmlspecialchars() on every echo of user data — escape on output, always.
  • Checkbox groups need name="thing[]"; unchecked boxes are never sent.
  • Redirect after a successful POST so refresh cannot resubmit.

Next, we make the form actually compute something.

Finished this one?

The challenge above has no posted solution — that is deliberate. Get it working, then come back for the next lesson.

Next lesson Subscribe