first php projects
Contact Form Handler
This increment moves from terminal input to a browser form. Build a contact page that renders on GET, validates on POST, rejects missing CSRF tokens, preserves safe input after validation errors, and redirects after success.
Use A Small File Shape
public/contact.php
src/ContactForm.php
templates/contact.php
Keep validation in src/ContactForm.php so the web entry point only translates the request into application work.
Validate Into A Predictable Shape
<?php
declare(strict_types=1);
function validateContact(array $input): array
{
$values = [
'name' => trim((string) ($input['name'] ?? '')),
'email' => trim((string) ($input['email'] ?? '')),
'message' => trim((string) ($input['message'] ?? '')),
];
$errors = [];
if ($values['name'] === '') {
$errors['name'] = 'Enter your name.';
}
if (filter_var($values['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'Enter a valid email address.';
}
if ($values['message'] === '') {
$errors['message'] = 'Enter a message.';
}
return [$values, $errors];
}
The handler should start a session, create a random token when one is absent, and compare the POST token with hash_equals() before accepting the message.
Escape At Render Time
The template must escape old values:
<?php
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
Use e($values['email']) in an attribute and e($values['message']) inside the textarea. Validation and escaping solve different problems: validation decides whether a value is acceptable; escaping prevents a value becoming HTML.
Verify The Browser Flow
Check:
- GET renders an empty form and CSRF token.
- Empty POST renders field errors and preserves safe values.
- Missing or wrong CSRF token is rejected.
- Valid POST stores or sends the message and redirects.
- Refreshing the success page does not submit again.
Practice
Practice: Build A Contact Form Handler
Implement the contact form flow from the lesson. Use a session-backed CSRF token and preserve escaped values after validation errors.
Requirements
- Render name, email, and message fields.
- Validate required fields and email format server-side.
- Verify a session-backed CSRF token before processing.
- Redirect after success to avoid duplicate submissions on refresh.
- Client-side validation is helpful but not a security control.
- Escape values when redisplaying them.
- Do not log full message bodies without a retention reason.
Verify GET, rejected POST, missing-token POST, accepted POST, and refresh-after-success behavior.
Show solution
<?php
session_start();
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submittedToken = (string) ($_POST['_csrf'] ?? '');
if (!hash_equals($_SESSION['csrf'], $submittedToken)) {
http_response_code(403);
exit('Invalid request token.');
}
[$values, $errors] = validateContact($_POST);
if ($errors === []) {
$_SESSION['flash'] = 'Message accepted.';
header('Location: /contact.php?sent=1', true, 303);
exit;
}
}
Render the form with escaped old values and a hidden _csrf field. A successful POST redirects with 303 See Other; rejected input renders safely without processing the message.