Building a PHP Web Form
HTML forms, GET versus POST, superglobals, and the self-submitting page pattern that every PHP app is built on.
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
<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.method—getorpost. More on this in a second.name— on each input. This is the one that matters. PHP receives your data keyed byname. An input without anameis invisible to the server.idis for the<label>and your CSS; it is never sent anywhere.
#GET versus POST
| GET | POST | |
|---|---|---|
| Where the data goes | In the URL: ?name=Dom | In the request body |
| Visible in the address bar | Yes | No |
| Bookmarkable / shareable | Yes | No |
| Size limit | ~2000 characters | Effectively none |
| Safe to repeat | Yes | No — 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:
$_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 loadsThey 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
$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
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
$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
<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 <, > into >, " into " and & into &. The browser then displays those characters instead of interpreting them as markup.
A shorter alias is worth defining at the top of any project:
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:
<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>$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:
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.
-
1An input renders fine but its value never reaches PHP. What is missing?
Show the answer
A. The
nameattribute.PHP keys
$_POSTbyname.idexists for the<label>and your CSS, and is never sent anywhere. -
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.
-
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.
-
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 afterheader().
#Key takeaways
- The
nameattribute 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"], notisset()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.