PHP Form Complete

sa sahaya jerish Updated 17 Oct 2026
9 min read ·Lesson 99 of 248
In this post

This lesson combines everything from the PHP Forms section into one complete, production-style form: a course enquiry form with validation, normalization, sticky values, error messages, CSRF protectio

This lesson combines everything from the PHP Forms section into one complete, production-style form: a course enquiry form with validation, normalization, sticky values, error messages, CSRF protection, a spam honeypot, the Post/Redirect/Get pattern, flash messages, safe output, and storage.


What the Finished Form Does

RequirementTechniqueLesson
Show the form on GET, process on POST$_SERVER['REQUEST_METHOD']PHP Form Handling
Reject forged submissionsCSRF token in the sessionPHP Form Handling
Catch simple spam botsHidden "honeypot" field(this lesson)
Check required fieldsTrimmed, type-safe checksPHP Form Required
Validate email and phonefilter_var(), regex, normalizationPHP Form URL/E-mail, Form Input Sanitizing
Validate choicesAllow-lists from option arraysCheckbox, Radio and Select Inputs
Keep the user's input after errorsFlashed old input + old() helperSticky Forms
Prevent duplicate submissionsRedirect after every POST (PRG)$_POST
Prevent XSShtmlspecialchars() via e()Escaping Output

Project Structure

enquiry/
├── public/
│   └── enquiry.php          ← the form page (controller + template)
├── src/
│   └── enquiry-functions.php ← reusable functions
└── storage/
    └── enquiries.jsonl      ← saved enquiries (outside the web root)

Functions live in src/, data in storage/, and only public/ is exposed by the web server. Enquiries are stored one JSON object per line (JSON Lines), which is easy to append and to import later; a real application would usually insert them into a database instead.


Step 1: Reusable Functions

src/enquiry-functions.php

<?php
declare(strict_types=1);

const COURSES = ['php' => 'PHP Full Course', 'mysql' => 'MySQL Essentials', 'laravel' => 'Laravel Framework'];
const MODES = ['online' => 'Online', 'offline' => 'Classroom (Chennai)'];

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function old(array $old, string $key, string $default = ''): string
{
    $value = $old[$key] ?? $default;
    return is_string($value) ? $value : $default;
}

function inputString(array $input, string $key): string
{
    $value = $input[$key] ?? '';
    return is_string($value) ? trim($value) : '';
}

function csrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function isValidCsrfToken(mixed $token): bool
{
    return is_string($token)
        && isset($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

function normalizeMobile(string $input): ?string
{
    $digits = preg_replace('/\D+/', '', $input) ?? '';

    if (strlen($digits) === 12 && str_starts_with($digits, '91')) {
        $digits = substr($digits, 2);
    }

    return preg_match('/^[6-9]\d{9}$/', $digits) === 1 ? $digits : null;
}

function validateEnquiry(array $input): array
{
    $data = [
        'name' => preg_replace('/\s+/u', ' ', inputString($input, 'name')) ?? '',
        'email' => inputString($input, 'email'),
        'phone' => inputString($input, 'phone'),
        'course' => inputString($input, 'course'),
        'mode' => inputString($input, 'mode'),
        'message' => inputString($input, 'message'),
        'consent' => ($input['consent'] ?? null) === 'yes',
    ];

    $errors = [];

    if ($data['name'] === '' || mb_strlen($data['name']) > 60) {
        $errors['name'] = 'Please enter your name.';
    }

    if (filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) {
        $errors['email'] = 'Please enter a valid email address.';
    }

    if ($data['phone'] !== '') {
        $phone = normalizeMobile($data['phone']);
        if ($phone === null) {
            $errors['phone'] = 'Enter a valid 10-digit mobile number or leave it empty.';
        } else {
            $data['phone'] = $phone;
        }
    }

    if (!array_key_exists($data['course'], COURSES)) {
        $errors['course'] = 'Please choose a course.';
    }

    if (!array_key_exists($data['mode'], MODES)) {
        $errors['mode'] = 'Please choose online or offline.';
    }

    $length = mb_strlen($data['message']);
    if ($length < 10 || $length > 1000) {
        $errors['message'] = 'Message must be between 10 and 1000 characters.';
    }

    if (!$data['consent']) {
        $errors['consent'] = 'Please agree to be contacted about your enquiry.';
    }

    return [$data, $errors];
}

function saveEnquiry(array $data, string $file): void
{
    $record = $data + ['received_at' => date(DATE_ATOM)];
    $line = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL;

    if (file_put_contents($file, $line, FILE_APPEND | LOCK_EX) === false) {
        throw new RuntimeException('Could not save the enquiry.');
    }
}

Testing the Validation Without a Browser

Because validateEnquiry() receives an array instead of reading $_POST, it can be tested directly:

<?php
require __DIR__ . '/src/enquiry-functions.php';

[$data, $errors] = validateEnquiry([
    'name' => '  Karthik   R ',
    'email' => 'karthik@example.com',
    'phone' => '+91 98765 43210',
    'course' => 'php',
    'mode' => 'online',
    'message' => 'I would like to join the weekend batch.',
    'consent' => 'yes',
]);

var_dump($errors === []);
echo "{$data['name']} | {$data['phone']} | {$data['course']}\n";

[, $errors] = validateEnquiry([
    'name' => '',
    'email' => 'karthik@',
    'phone' => '12345',
    'course' => 'java',
    'message' => 'Hi',
]);

print_r($errors);

Output

bool(true)
Karthik R | 9876543210 | php
Array
(
    [name] => Please enter your name.
    [email] => Please enter a valid email address.
    [phone] => Enter a valid 10-digit mobile number or leave it empty.
    [course] => Please choose a course.
    [mode] => Please choose online or offline.
    [message] => Message must be between 10 and 1000 characters.
    [consent] => Please agree to be contacted about your enquiry.
)
  • Valid input comes back normalized: extra spaces in the name are collapsed, and the phone number is reduced to 10 digits.
  • Invalid input produces one clear message per field, all reported together.
  • The optional phone field is validated only when something was entered.

Step 2: The Page — Controller and Template

public/enquiry.php

<?php
declare(strict_types=1);

require __DIR__ . '/../src/enquiry-functions.php';

session_start();

$storageFile = __DIR__ . '/../storage/enquiries.jsonl';

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
    if (!isValidCsrfToken($_POST['csrf_token'] ?? null)) {
        http_response_code(419);
        exit('Your session has expired. Please reload the page and try again.');
    }

    // Honeypot: real users never see or fill this field
    if (inputString($_POST, 'website') !== '') {
        header('Location: /enquiry.php?sent=1', true, 303);
        exit;
    }

    [$data, $errors] = validateEnquiry($_POST);

    if ($errors !== []) {
        $_SESSION['_old'] = $data;
        $_SESSION['_errors'] = $errors;
        header('Location: /enquiry.php', true, 303);
        exit;
    }

    try {
        saveEnquiry($data, $storageFile);
    } catch (Throwable $e) {
        error_log('Enquiry not saved: ' . $e->getMessage());
        $_SESSION['_old'] = $data;
        $_SESSION['_errors'] = ['form' => 'Sorry, something went wrong. Please try again later.'];
        header('Location: /enquiry.php', true, 303);
        exit;
    }

    $_SESSION['_success'] = 'Thank you, ' . $data['name'] . '! Our team will contact you within one working day.';
    header('Location: /enquiry.php', true, 303);
    exit;
}

$old = $_SESSION['_old'] ?? [];
$errors = $_SESSION['_errors'] ?? [];
$success = $_SESSION['_success'] ?? null;
unset($_SESSION['_old'], $_SESSION['_errors'], $_SESSION['_success']);

$error = fn(string $field): string => isset($errors[$field])
    ? '<p class="error" id="' . e($field) . '-error">' . e($errors[$field]) . '</p>'
    : '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Course Enquiry</title>
</head>
<body>
<h1>Course Enquiry</h1>

<?php if ($success !== null): ?>
    <p class="success"><?= e($success) ?></p>
<?php endif; ?>
<?= $error('form') ?>

<form method="post" action="/enquiry.php" novalidate>
    <input type="hidden" name="csrf_token" value="<?= e(csrfToken()) ?>">

    <div style="position:absolute; left:-9999px" aria-hidden="true">
        <label>Website <input name="website" tabindex="-1" autocomplete="off"></label>
    </div>

    <label>Name *
        <input name="name" required maxlength="60" value="<?= e(old($old, 'name')) ?>">
    </label>
    <?= $error('name') ?>

    <label>Email *
        <input type="email" name="email" required value="<?= e(old($old, 'email')) ?>">
    </label>
    <?= $error('email') ?>

    <label>Mobile (optional)
        <input type="tel" name="phone" value="<?= e(old($old, 'phone')) ?>">
    </label>
    <?= $error('phone') ?>

    <label>Course *
        <select name="course" required>
            <option value="">-- Choose a course --</option>
            <?php foreach (COURSES as $value => $label): ?>
                <option value="<?= e($value) ?>"<?= old($old, 'course') === $value ? ' selected' : '' ?>><?= e($label) ?></option>
            <?php endforeach; ?>
        </select>
    </label>
    <?= $error('course') ?>

    <fieldset>
        <legend>Mode *</legend>
        <?php foreach (MODES as $value => $label): ?>
            <label><input type="radio" name="mode" value="<?= e($value) ?>"<?= old($old, 'mode') === $value ? ' checked' : '' ?>> <?= e($label) ?></label>
        <?php endforeach; ?>
    </fieldset>
    <?= $error('mode') ?>

    <label>Message *
        <textarea name="message" required minlength="10" maxlength="1000"><?= e(old($old, 'message')) ?></textarea>
    </label>
    <?= $error('message') ?>

    <label>
        <input type="checkbox" name="consent" value="yes"<?= ($old['consent'] ?? false) === true ? ' checked' : '' ?>>
        I agree to be contacted about this enquiry *
    </label>
    <?= $error('consent') ?>

    <button type="submit">Send enquiry</button>
</form>
</body>
</html>

How the Pieces Work Together

The Request Flow

GET  /enquiry.php          → empty form (+ CSRF token)
POST /enquiry.php (errors) → flash old input + errors → 303 → GET → form with values and messages
POST /enquiry.php (valid)  → save → flash success     → 303 → GET → thank-you message, empty form

Every POST ends with a redirect, so refreshing the page never resubmits the form, and the back button behaves predictably.

Step-by-Step Explanation

  1. Bootstrap: strict types, shared functions, and the session are loaded before any output.
  2. CSRF check: a POST without the correct token is rejected before anything else happens.
  3. Honeypot: the website field is moved off-screen and hidden from screen readers. Humans leave it empty; many simple bots fill every field. Spam submissions receive a normal-looking redirect, so bots do not learn that they were detected.
  4. Validation: validateEnquiry($_POST) returns normalized data and field-specific errors.
  5. Errors: the normalized input and the errors are flashed to the session; after the redirect, the template refills every field with old() and shows each message beside its field.
  6. Saving: the enquiry is appended to a JSON Lines file with an exclusive lock (LOCK_EX), so simultaneous submissions cannot corrupt the file. Failures are logged with error_log() — the visitor sees a friendly message, never technical details.
  7. Success: a flash message thanks the user by name. It is escaped by e() in the template, like every other dynamic value.
  8. Template: HTML attributes such as required and maxlength give instant feedback in the browser; novalidate is included here so you can observe the server-side validation while testing — remove it in production for the best user experience.

Security Checklist

ThreatProtection in This Form
Cross-site scriptingEvery output passes through e()
Cross-site request forgerySession-bound CSRF token checked with hash_equals()
Manipulated choicesCourse and mode validated against allow-lists
Unexpected input typesinputString() ignores arrays
Duplicate submissionsPost/Redirect/Get
Spam botsHoneypot field (add rate limiting or a CAPTCHA for high-traffic forms)
Data exposureStorage outside the public folder; errors logged, not displayed

Extending the Form

  • Database storage: replace saveEnquiry() with an INSERT using a PDO prepared statement (see PDO Prepared Statements).
  • Email notification: send a confirmation to the user and a notification to your team, using a mail library such as PHPMailer or Symfony Mailer rather than raw mail().
  • Rate limiting: count submissions per IP address or session within a time window and reject excessive requests.
  • Reusable form classes: in frameworks, form requests or form components encapsulate the same steps — the concepts in this section are exactly what they automate.

Common Mistakes in Complete Forms

MistakeConsequenceFix
Validating only in the browserInvalid and malicious data is savedValidate in PHP
Missing CSRF protectionAttackers submit forms on users' behalfToken in every POST form
Rendering a page directly after POSTDuplicate submissions on refreshRedirect with 303
Losing input after errorsFrustrated usersFlash old input; use old()
Showing exception messages to usersInformation leakageLog details, show a generic message
Storing data inside the web rootFiles downloadable by URLStore outside public/ or in a database

Best Practices

  • Keep validation in a reusable, testable function that returns data and errors.
  • Process POST requests before output, and always redirect afterwards.
  • Escape every value in the template, including flash messages and error texts.
  • Protect forms with CSRF tokens and basic spam defenses.
  • Log technical errors and show users clear, friendly messages.

Official reference: https://www.php.net/manual/en/tutorial.forms.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about PHP Form Complete and get answers drawn from it.

Signed-in readers only.