Forms are how visitors talk to your website: search boxes, contact forms, sign-up pages and checkout screens. In the HTML Forms lesson you built the front end. Now you will write the PHP that receives the data, checks it and responds. Handling PHP forms is also where most security mistakes happen, so this lesson treats safety as part of the basics, not an extra.
How form data reaches PHP
When a form is submitted, the browser sends each field as a name/value pair. The key is the field’s name attribute, so a field without a name is never sent. The form’s method attribute decides how the data travels, and its action attribute decides which PHP file receives it.
PHP collects the data into two built-in associative arrays (called superglobals, because they are available everywhere): $_GET for data in the URL and $_POST for data in the request body. Everything in them arrives as strings (or arrays of strings), even when the user typed a number.
GET or POST?
| GET | POST | |
|---|---|---|
| Where the data goes | In the URL: search.php?q=dosa&page=2 | In the body of the request, not visible in the URL |
| Bookmarkable and shareable | Yes | No |
| Stored in history and server logs | Yes | No (the URL only) |
| Typical use | Searching, filtering, sorting, pagination | Logging in, sign-ups, sending messages, anything that changes data |
The rule of thumb for PHP forms: use GET when the request only reads something and the result could be bookmarked. Use POST when it changes something or contains private data such as passwords. Remember that POST is not encryption; only HTTPS protects the data on its way to the server.
Reading GET data safely
Never assume a field exists: a visitor can open your page without submitting the form, or edit the URL by hand. The null coalescing operator ?? from PHP Operators gives a default without a warning. Then, whenever you print user input into HTML, pass it through htmlspecialchars(). It converts characters such as <, >, & and quotes into HTML entities, so the browser displays them as text instead of running them. Skipping this step opens the door to cross-site scripting (XSS).
<?php
declare(strict_types=1);
// PHP fills $_GET from the URL. Here we set it by hand to show
// what PHP would receive for: search.php?q=<b>Dosa</b>&page=2
$_GET = ['q' => '<b>Dosa</b>', 'page' => '2'];
$query = trim($_GET['q'] ?? '');
$page = filter_var($_GET['page'] ?? 1, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'default' => 1],
]);
if ($query === '') {
echo "<p>Please type something to search.</p>";
} else {
echo "<p>Results for " . htmlspecialchars($query) . " (page $page)</p>";
}<p>Results for <b>Dosa</b> (page 2)</p>
The visitor sees the text Results for <b>Dosa</b> rather than bold HTML, which proves the input was treated as plain text. filter_var() with FILTER_VALIDATE_INT turns the page number into a real integer and falls back to 1 if someone types page=abc or page=-5.
Handling a POST form with validation
A typical form page does three jobs: show the form, check the submitted data, and either show errors or act on the data. Check $_SERVER['REQUEST_METHOD'] to know whether the form was submitted. Validation means checking every field against clear rules on the server, even if the HTML form already has required or type="email". Browser checks are a convenience for honest users; anyone can bypass them.
<?php
declare(strict_types=1);
// Simulate a submitted form. On a real server PHP fills these for you.
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST = ['name' => ' Priya ', 'email' => 'priya@example', 'age' => '17', 'message' => ''];
/** @return array<string, string> field => error message */
function validateContact(array $input): array
{
$errors = [];
if (trim($input['name'] ?? '') === '') {
$errors['name'] = 'Please enter your name.';
}
if (filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Please enter a valid email address.';
}
$age = filter_var($input['age'] ?? '', FILTER_VALIDATE_INT,
['options' => ['min_range' => 18, 'max_range' => 120]]);
if ($age === false) {
$errors['age'] = 'You must be 18 or older.';
}
if (mb_strlen(trim($input['message'] ?? '')) < 10) {
$errors['message'] = 'Message must be at least 10 characters.';
}
return $errors;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors = validateContact($_POST);
if ($errors === []) {
echo "Thank you, " . htmlspecialchars(trim($_POST['name'])) . "!";
} else {
foreach ($errors as $field => $error) {
echo "$field: $error\n";
}
}
}email: Please enter a valid email address. age: You must be 18 or older. message: Message must be at least 10 characters.
Collecting errors in an array keyed by field name lets you show each message next to the right input. When you redisplay the form, fill each field with the submitted value, escaped, so users do not have to retype everything: value="<?= htmlspecialchars($_POST['name'] ?? '') ?>". This is called a sticky form.
Tip: After a successful POST, redirect with header('Location: /thank-you.php'); exit; instead of printing a page directly. This Post/Redirect/Get pattern stops the browser from resubmitting the form if the user refreshes.
Protecting PHP forms against CSRF
Cross-site request forgery (CSRF) tricks a logged-in user’s browser into submitting one of your forms from another website. The standard defence is a secret token: generate a random value, store it in the user’s session, put it in a hidden field, and on submission check that the two match.
<?php
declare(strict_types=1);
session_start();
// 1. Create a secret token once per session
$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));
// 2. On submit, check the token before doing anything else
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sent = $_POST['csrf_token'] ?? '';
if (!is_string($sent) || !hash_equals($_SESSION['csrf_token'], $sent)) {
http_response_code(403);
exit('Invalid form submission.');
}
// token is valid: continue with validation as in contact.php
}
?>
<form method="post" action="contact.php">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<!-- other fields -->
<button type="submit">Send</button>
</form>Example output (first visit, a GET request): the form HTML, including a hidden field such as <input type="hidden" name="csrf_token" value="9f2c…e41a"> that holds a 64-character random hex token. A POST request without the matching token stops with HTTP status 403 and the text: Invalid form submission.
random_bytes() produces cryptographically secure randomness and hash_equals() compares strings in constant time, so attackers cannot guess the token by measuring response times. Frameworks such as Laravel and Symfony, and WordPress with its nonces, handle this for you, but it helps to understand what they do.
Watch out: In PHP forms, validation, escaping and CSRF tokens solve different problems. Validation checks that data makes sense, htmlspecialchars() protects HTML output, and CSRF tokens prove the request came from your own form. When the data goes into a database you also need prepared statements, covered in PHP MySQL with PDO.
