Skip to content

PHP Tutorial Forms and User Input

Building a Contact Form

Lesson 145 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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

index.php
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

  1. clean() trims every value and ignores anything that is not a string, so a hostile name[]=x cannot break the code.
  2. validateMessage() collects all errors by field name.
  3. On a failed submit, the form is shown again with $old values (escaped by h()) and the error list.
  4. On success, the message is appended to messages.log with a lock. New lines inside the text are replaced, so one message always stays on one line.
  5. 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.log in 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.
info
Why this matters in WordPress
A WordPress contact form plugin does exactly this: it validates, escapes and stores or emails the message. Knowing the plain PHP version helps you read and fix those plugins.

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
  1. $old gets a "subject" key, filled with clean($_POST["subject"] ?? "").
  2. validateMessage() gets a length check for $data["subject"] that adds $errors["subject"].
  3. The HTML form gets a <label for="subject"> and an <input id="subject" name="subject" value="<?= h($old["subject"]) ?>">.