Let us put the chapter together in one working contact form. It has labels, validation, error messages, sticky values, safe output and the Post/Redirect/Get pattern.
This version does not send email yet. Sending mail needs a mail server or service, which differs on every host. We save the message to a text file instead, and the projects chapter takes it further.
The complete file: contact.php
<?php
declare(strict_types=1);
session_start();
function clean(mixed $value): string
{
return is_string($value) ? trim($value) : "";
}
function h(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}
function validateMessage(array $data): array
{
$errors = [];
if (mb_strlen($data["name"]) < 2 || mb_strlen($data["name"]) > 60) {
$errors["name"] = "Please enter your name (2 to 60 characters).";
}
if (filter_var($data["email"], FILTER_VALIDATE_EMAIL) === false) {
$errors["email"] = "Please enter a valid email address.";
}
if (mb_strlen($data["message"]) < 10 || mb_strlen($data["message"]) > 2000) {
$errors["message"] = "Your message needs 10 to 2000 characters.";
}
return $errors;
}
$old = ["name" => "", "email" => "", "message" => ""];
$errors = [];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$old = [
"name" => clean($_POST["name"] ?? ""),
"email" => clean($_POST["email"] ?? ""),
"message" => clean($_POST["message"] ?? ""),
];
$errors = validateMessage($old);
if ($errors === []) {
$line = date("c") . " | " . str_replace(["\r", "\n"], " ", $old["name"] . " <" . $old["email"] . "> " . $old["message"]) . PHP_EOL;
file_put_contents(__DIR__ . "/messages.log", $line, FILE_APPEND | LOCK_EX);
$_SESSION["flash"] = "Thank you, your message was received.";
header("Location: contact.php");
exit;
}
}
$flash = $_SESSION["flash"] ?? null;
unset($_SESSION["flash"]);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
<?php if ($flash): ?>
<p role="status"><?= h($flash) ?></p>
<?php endif; ?>
<?php if ($errors): ?>
<div role="alert">
<p>Please fix these problems:</p>
<ul>
<?php foreach ($errors as $field => $message): ?>
<li><a href="#<?= h($field) ?>"><?= h($message) ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="contact.php" method="post" novalidate>
<p>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" required value="<?= h($old["name"]) ?>">
</p>
<p>
<label for="email">Email</label><br>
<input type="email" id="email" name="email" required value="<?= h($old["email"]) ?>">
</p>
<p>
<label for="message">Message</label><br>
<textarea id="message" name="message" rows="6" required><?= h($old["message"]) ?></textarea>
</p>
<button type="submit">Send message</button>
</form>
</body>
</html>
How it works
clean()trims every value and ignores anything that is not a string, so a hostilename[]=xcannot break the code.validateMessage()collects all errors by field name.- On a failed submit, the form is shown again with
$oldvalues (escaped byh()) and the error list. - On success, the message is appended to
messages.logwith a lock. New lines inside the text are replaced, so one message always stays on one line. - The script saves a flash message and redirects, so a refresh does not send the message twice.
What it still needs
- Spam protection. Bots will find any public form. Add a hidden honeypot field, a rate limit or a captcha.
- CSRF protection, so other sites cannot make a visitor’s browser post to your form. See CSRF.
- A safe place for the log.
messages.login the web folder could be downloaded by anyone who guesses the address. Keep it outside the public folder, or block access to it. - Real delivery, for example through a mail library and an SMTP service.
Why this matters in WordPress
Exercise: add a subject field
Add a “Subject” field that must be 3 to 100 characters, with its own error message and a sticky value. Which three places in the file change?
Show the answer
$oldgets a"subject"key, filled withclean($_POST["subject"] ?? "").validateMessage()gets a length check for$data["subject"]that adds$errors["subject"].- The HTML form gets a
<label for="subject">and an<input id="subject" name="subject" value="<?= h($old["subject"]) ?>">.