LearnAI ToolsCareerPractice BuildsPlayContact
PHP ProgrammingBeginner~1.5 hours

Contact Form Handler

Validate and process form submissions with email notifications.

SuperglobalsFormsValidation

Overview

A contact form is the smallest possible example of a much bigger idea: every value inside `$_POST` came from a browser, and a browser is controlled entirely by whoever is using it — which means it can never be trusted. PHP hands you the submitted fields through the `$_POST` superglobal exactly as the visitor typed them, with no validation of any kind applied automatically. Everything from checking that a required field was actually filled in, to confirming an email address looks like an email address, is work your script has to do itself before that data is safe to act on.

By the end of this tutorial you will have a single self-submitting file, `contact.php`, that shows a form, and on submission validates the name, email, and message fields, redisplays specific error messages next to whichever fields failed, and — once everything passes — sends a notification using PHP's built-in `mail()` function. `mail()` depends on a mail transfer agent being configured on the server it runs on (like `sendmail` on Linux, or an SMTP relay set in `php.ini`), so a real production deployment would typically use a library like PHPMailer or a transactional email API such as SendGrid, Mailgun, or Resend for reliable delivery and logging. `mail()` is still the right tool for this tutorial, though, because it demonstrates the exact same request-validate-notify flow those tools build on top of, without needing an external account to follow along.

What You'll Build
  • A self-submitting HTML form with name, email, and message fields.
  • Server-side request handling using the `$_SERVER['REQUEST_METHOD']` and `$_POST` superglobals.
  • Required-field checks plus an email-format check via `filter_var(..., FILTER_VALIDATE_EMAIL)`.
  • A notification email sent with `mail()` whenever a submission passes validation.
  • Inline error messages shown next to each invalid field.
  • A success confirmation shown after a valid submission, with the form reset for a new message.

Prerequisites

  • PHP superglobals — what `$_POST` and `$_SERVER` are and how they differ from ordinary variables.
  • Forms in HTML — `<form method="POST">`, input `name` attributes, and how a browser packages submitted fields.
  • Conditionals and functions — `if`/`else`, and writing a small function that returns a value.
  • Arrays — associative arrays, used here to collect one error message per invalid field.
  • Basic PHP syntax — `<?php ?>` tags, `echo`/short-echo `<?= ?>`, and string concatenation with `.`.

Project Structure

Everything lives in one file, `contact.php`. That is a deliberate choice, not a shortcut: the form's `action=""` attribute means the browser posts back to the very file it was served from, so there is no separate "handler" URL that has to be kept in sync with the form's field names. A block of PHP at the top of the file checks `$_SERVER['REQUEST_METHOD'] === 'POST'` to decide whether this request is a first visit (show a blank form) or a submission (validate `$_POST` and either show errors or send the email).

Two arrays carry state from the top of the file down to the HTML at the bottom: `$errors`, an associative array mapping a field name to a human-readable message, and `$old`, holding whatever the visitor actually typed. Redisplaying `$old` in each `value="..."` attribute is what lets a visitor fix one mistake — say, a malformed email — without retyping their entire message.

Step 1: Build the Contact Form

Start with the plain HTML skeleton and the form itself. `$errors` and `$old` do not have real validation logic behind them yet — that arrives in Steps 2 and 3 — but they are declared up front as empty so the form below always has something safe to read, whether this is the first visit or a page that failed validation.

<?php
// $errors and $old are filled in by the validation logic added in Steps 2-3.
// Declaring them empty here means the HTML below always has something safe
// to read, whether this is a fresh page load or a resubmission with mistakes.
$errors = [];
$old = ['name' => '', 'email' => '', 'message' => ''];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Us</title>
</head>
<body>
<h1>Contact Us</h1>
<!-- action="" resubmits to this same file, so the PHP above can read $_POST next request -->
<form action="" method="POST" novalidate>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($old['name']) ?>">
<br>
<label for="email">Email</label><br>
<input type="text" id="email" name="email" value="<?= htmlspecialchars($old['email']) ?>">
<br>
<label for="message">Message</label><br>
<textarea id="message" name="message" rows="5"><?= htmlspecialchars($old['message']) ?></textarea>
<br>
<button type="submit">Send Message</button>
</form>
</body>
</html>

`htmlspecialchars()` converts characters like `<` and `"` into their HTML-entity equivalents, which is what stops a visitor who types `<script>` into the message field from having that markup actually run when it is echoed back. `novalidate` on the `<form>` tag turns off the browser's own built-in validation popups, since Step 3 builds PHP-driven error messages that should be the only ones the visitor sees.

Step 2: Read the Submitted Data

The `$_SERVER['REQUEST_METHOD']` check is what separates "someone just opened this page" from "someone just submitted the form" — a plain page load is a `GET` request, and there is nothing in `$_POST` to read yet. `trim()` strips accidental leading or trailing whitespace a visitor might have pasted in, and the `?? ''` null-coalescing operator guards against a field being missing from the request entirely, which can happen if the form is ever tampered with or a field gets renamed.

<?php
$errors = [];
$old = ['name' => '', 'email' => '', 'message' => ''];
// Only run the processing logic once the form has actually been submitted —
// a fresh page load is a GET request and should just show the blank form.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// trim() strips accidental leading/trailing whitespace a visitor might paste in.
// ?? '' guards against a missing key if a field was somehow omitted from the request.
$old['name'] = trim($_POST['name'] ?? '');
$old['email'] = trim($_POST['email'] ?? '');
$old['message'] = trim($_POST['message'] ?? '');
}
?>

Step 3: Validate the Input

Each field gets its own check, appended into `$errors` under that field's name so the HTML in Step 5 can display the right message next to the right input. The email check runs in two stages with `elseif` — first confirming something was typed at all, then confirming what was typed actually looks like an email address — because `filter_var()` with `FILTER_VALIDATE_EMAIL` only checks the *shape* of an address (`name@domain.tld`), not whether that mailbox genuinely exists; only actually sending mail to it could confirm that.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$old['name'] = trim($_POST['name'] ?? '');
$old['email'] = trim($_POST['email'] ?? '');
$old['message'] = trim($_POST['message'] ?? '');
// Required-field checks: an empty string after trim() means nothing meaningful was typed.
if ($old['name'] === '') {
$errors['name'] = 'Please enter your name.';
}
if ($old['email'] === '') {
$errors['email'] = 'Please enter your email address.';
} elseif (filter_var($old['email'], FILTER_VALIDATE_EMAIL) === false) {
// FILTER_VALIDATE_EMAIL checks the shape of the address, not whether the
// mailbox actually exists — that can only be confirmed by sending mail to it.
$errors['email'] = 'Please enter a valid email address.';
}
if ($old['message'] === '') {
$errors['message'] = 'Please enter a message.';
} elseif (strlen($old['message']) < 10) {
$errors['message'] = 'Your message should be at least 10 characters.';
}
}
?>

Step 4: Send the Notification Email

`empty($errors)` is only `true` once every check in Step 3 has passed with nothing to report, which is exactly the signal to actually send mail. PHP's `mail()` function takes four arguments — the recipient, the subject, the body, and a string of extra headers — and returns `true` or `false` depending on whether the underlying mail transport accepted the message for delivery (not whether it was actually read, or even successfully delivered end to end). The `From` header deliberately points at an address on your own domain rather than the visitor's address, because many mail providers reject or spam-flag a message that claims to be "From" a domain it does not control; the visitor's real address goes in `Reply-To` instead, so clicking "Reply" on the notification naturally routes back to them.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($errors)) {
// empty($errors) is only true once every check in Step 3 has passed.
$to = 'you@example.com'; // Where the notification should land
$subject = 'New contact form message from ' . $old['name'];
$body = "Name: {$old['name']}\nEmail: {$old['email']}\n\nMessage:\n{$old['message']}";
// mail() needs a From header that belongs to *your* domain — providers often reject
// or flag messages that claim to be "From" an address they don't control, so the
// visitor's own address goes in Reply-To instead, where a reply naturally reaches them.
$headers = "From: Website Contact Form <noreply@example.com>\r\n";
$headers .= 'Reply-To: ' . $old['email'] . "\r\n";
$sent = mail($to, $subject, $body, $headers);
if ($sent) {
$success = true;
} else {
// mail() depends on a working local MTA (sendmail on Linux, etc.) or an SMTP relay
// configured in php.ini — in production, swap this for PHPMailer or a transactional
// email API (SendGrid, Mailgun, Resend) so delivery does not silently fail.
$errors['send'] = 'Sorry, something went wrong sending your message. Please try again later.';
}
}
?>

Step 5: Show Feedback and Keep Entered Values

The final piece adds a success message, per-field error text, and resets `$old` after a successful send so the visitor is not shown their own message a second time. Because every `<input>` and `<textarea>` already reads its value from `$old`, and every error already lives in `$errors` under the matching field name, this step is mostly about placing small `<?php if (...) ?>` checks around the existing form from Step 1 rather than writing new logic.

<?php if (!empty($success)): ?>
<p style="color: green;">Thanks, <?= htmlspecialchars($old['name']) ?>! Your message has been sent.</p>
<?php
// Reset $old so the form below renders empty fields after a successful send,
// instead of showing the visitor's own message back to them a second time.
$old = ['name' => '', 'email' => '', 'message' => ''];
?>
<?php endif; ?>
<?php if (!empty($errors['send'])): ?>
<p style="color: red;"><?= htmlspecialchars($errors['send']) ?></p>
<?php endif; ?>
<form action="" method="POST" novalidate>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($old['name']) ?>">
<?php if (!empty($errors['name'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['name']) ?></small>
<?php endif; ?>
<br>
<label for="email">Email</label><br>
<input type="text" id="email" name="email" value="<?= htmlspecialchars($old['email']) ?>">
<?php if (!empty($errors['email'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['email']) ?></small>
<?php endif; ?>
<br>
<label for="message">Message</label><br>
<textarea id="message" name="message" rows="5"><?= htmlspecialchars($old['message']) ?></textarea>
<?php if (!empty($errors['message'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['message']) ?></small>
<?php endif; ?>
<br>
<button type="submit">Send Message</button>
</form>

Complete Code

Here is the full `contact.php`, with the processing logic from Steps 2-4 at the top and the display logic from Steps 1 and 5 combined into one form below it.

contact.php
<?php
$errors = [];
$old = ['name' => '', 'email' => '', 'message' => ''];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$old['name'] = trim($_POST['name'] ?? '');
$old['email'] = trim($_POST['email'] ?? '');
$old['message'] = trim($_POST['message'] ?? '');
if ($old['name'] === '') {
$errors['name'] = 'Please enter your name.';
}
if ($old['email'] === '') {
$errors['email'] = 'Please enter your email address.';
} elseif (filter_var($old['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Please enter a valid email address.';
}
if ($old['message'] === '') {
$errors['message'] = 'Please enter a message.';
} elseif (strlen($old['message']) < 10) {
$errors['message'] = 'Your message should be at least 10 characters.';
}
if (empty($errors)) {
$to = 'you@example.com';
$subject = 'New contact form message from ' . $old['name'];
$body = "Name: {$old['name']}\nEmail: {$old['email']}\n\nMessage:\n{$old['message']}";
$headers = "From: Website Contact Form <noreply@example.com>\r\n";
$headers .= 'Reply-To: ' . $old['email'] . "\r\n";
$sent = mail($to, $subject, $body, $headers);
if ($sent) {
$success = true;
} else {
$errors['send'] = 'Sorry, something went wrong sending your message. Please try again later.';
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Us</title>
</head>
<body>
<h1>Contact Us</h1>
<?php if (!empty($success)): ?>
<p style="color: green;">Thanks, <?= htmlspecialchars($old['name']) ?>! Your message has been sent.</p>
<?php $old = ['name' => '', 'email' => '', 'message' => '']; ?>
<?php endif; ?>
<?php if (!empty($errors['send'])): ?>
<p style="color: red;"><?= htmlspecialchars($errors['send']) ?></p>
<?php endif; ?>
<form action="" method="POST" novalidate>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($old['name']) ?>">
<?php if (!empty($errors['name'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['name']) ?></small>
<?php endif; ?>
<br>
<label for="email">Email</label><br>
<input type="text" id="email" name="email" value="<?= htmlspecialchars($old['email']) ?>">
<?php if (!empty($errors['email'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['email']) ?></small>
<?php endif; ?>
<br>
<label for="message">Message</label><br>
<textarea id="message" name="message" rows="5"><?= htmlspecialchars($old['message']) ?></textarea>
<?php if (!empty($errors['message'])): ?>
<br><small style="color: red;"><?= htmlspecialchars($errors['message']) ?></small>
<?php endif; ?>
<br>
<button type="submit">Send Message</button>
</form>
</body>
</html>

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a CSRF token stored in `$_SESSION` and checked on submit, so the form cannot be posted to from another site.
  • Add a honeypot field or a CAPTCHA service to cut down on automated spam submissions.
  • Log every submission to a database table so past messages can be reviewed even if a notification email is missed.
  • Swap `mail()` for PHPMailer configured against a real SMTP account so delivery is reliable and easier to debug.
  • Rate-limit submissions per IP address (for example, one every 60 seconds) to stop the form being used to spam an inbox.

Summary

You built a contact form that treats every submitted value as untrusted until proven otherwise: `$_POST` supplies the raw data, targeted checks turn that data into either a specific error message or a green light to proceed, and only a fully validated submission ever reaches `mail()`. That same request-read, validate, act pattern is the backbone of almost every PHP form you will ever write, whether it ends in an email, a database insert, or an API call.