Step-by-step guide: text-based contact form with validation
Contact forms look simple but collect untrusted input. A production-ready form validates on the server, preserves user entries when validation fails, and escapes output when redisplaying messages. This tutorial uses plain PHP without frameworks so you see each step clearly.
Define validation rules in one place
Create a function that returns an associative array of field errors. An empty array means success. Keeping rules centralized makes unit testing easier later.
function validate_contact(array $data): array {
$errors = [];
$name = trim($data['name'] ?? '');
$message = trim($data['message'] ?? '');
$email = trim($data['email'] ?? '');
if ($name === '' || strlen($name) > 120) {
$errors['name'] = 'Please enter your name (max 120 characters).';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Please enter a valid email address.';
}
if ($message === '' || strlen($message) < 20) {
$errors['message'] = 'Message must be at least 20 characters.';
}
return $errors;
}Below is how the output typically looks in a browser, terminal, or API client:
Request handled successfully.
Handler pattern with repopulated form
On GET, show an empty form. On POST, validate; if errors exist, render the same page with error text next to fields.
Use htmlspecialchars($value, ENT_QUOTES, 'UTF-8') when echoing user input back into HTML attributes.
$errors = [];
$input = ['name' => '', 'email' => '', 'message' => ''];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = [
'name' => $_POST['name'] ?? '',
'email' => $_POST['email'] ?? '',
'message' => $_POST['message'] ?? '',
];
$errors = validate_contact($input);
if (!$errors) {
// Store in DB or send via mail — sanitize headers if using mail()
header('Location: contact-thanks.php');
exit;
}
}Below is how the output typically looks in a browser, terminal, or API client:
HTTP 302 Redirect Location: contact-thanks.php (After a valid form submit)
Displaying errors accessibly
Pair each input with an error paragraph and aria-invalid="true" when validation fails.
Screen reader users then hear which fields need attention. Color alone is not sufficient feedback.
Optional: honeypot for basic bot reduction
Add a hidden field humans should leave empty. Bots often fill every field; if the honeypot is non-empty, silently discard the submission. This is not a substitute for rate limiting or CAPTCHA on high-traffic sites.
mail() for contact forms often fails on shared hosting. Prefer SMTP libraries or form backends with SPF-aligned domains.