PHP 完整表单实例
Wiring validation, re-rendering, escaping and redirect into a usable form handler.
The goal
A contact form with name, email, website, comment and gender. Requirements: required and format checks, sticky fields on error, PRG to prevent resubmission.
The form page
<?php
// contact.php —— 表单与处理逻辑在同一文件
declare(strict_types=1);
$errors = [];
$old = ["name" => "", "email" => "", "website" => "", "comment" => "", "gender" => ""];
if (($_SERVER["REQUEST_METHOD"] ?? "") === "POST") {
foreach ($old as $k => $_) {
$old[$k] = trim((string)($_POST[$k] ?? ""));
}
if ($old["name"] === "") {
$errors["name"] = "姓名必填";
} elseif (mb_strlen($old["name"]) > 20) {
$errors["name"] = "姓名最多 20 个字";
}
if ($old["email"] === "") {
$errors["email"] = "邮箱必填";
} elseif (filter_var($old["email"], FILTER_VALIDATE_EMAIL) === false) {
$errors["email"] = "邮箱格式不正确";
}
if ($old["website"] !== "" && filter_var($old["website"], FILTER_VALIDATE_URL) === false) {
$errors["website"] = "网址格式不正确";
}
if (!in_array($old["gender"], ["female", "male"], true)) {
$errors["gender"] = "请选择性别";
}
if (!$errors) {
// 成功:写入会话后重定向(PRG)
$_SESSION["contact_ok"] = true;
header("Location: thanks.php");
exit;
}
}
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}
?>
<form method="post" novalidate>
<p>
<label>姓名 *</label>
<input name="name" value="<?= e($old["name"]) ?>">
<em><?= e($errors["name"] ?? "") ?></em>
</p>
<p>
<label>邮箱 *</label>
<input name="email" value="<?= e($old["email"]) ?>">
<em><?= e($errors["email"] ?? "") ?></em>
</p>
<p>
<label>网址</label>
<input name="website" value="<?= e($old["website"]) ?>">
<em><?= e($errors["website"] ?? "") ?></em>
</p>
<p>
<label>评论</label>
<textarea name="comment"><?= e($old["comment"]) ?></textarea>
</p>
<p>
<label>性别 *</label>
<label><input type="radio" name="gender" value="female"
<?= $old["gender"] === "female" ? "checked" : "" ?>>女</label>
<label><input type="radio" name="gender" value="male"
<?= $old["gender"] === "male" ? "checked" : "" ?>>男</label>
<em><?= e($errors["gender"] ?? "") ?></em>
</p>
<button>提交</button>
</form>Submitting invalid data (empty name, bad@ email) renders errors alongside the retained values:
<p>
<label>姓名 *</label>
<input name="name" value="">
<em>姓名必填</em>
</p>
<p>
<label>邮箱 *</label>
<input name="email" value="bad@">
<em>邮箱格式不正确</em>
</p>The success page
<?php
// thanks.php
session_start();
if (empty($_SESSION["contact_ok"])) {
header("Location: contact.php");
exit;
}
unset($_SESSION["contact_ok"]);
echo "提交成功,我们会尽快联系你。";提交成功,我们会尽快联系你。Visiting thanks.php directly bounces back to the form because no success flag exists in the session — stricter than showing an empty success page.
The PRG pattern
Three steps of Post/Redirect/Get:
- Browser POSTs the form
- Server responds
302 Location: thanks.php - Browser GETs it; the address bar now shows a GET page
Refreshing re-requests the GET page, so no resubmission. The cost: errors cannot simply be echoed — store them in the session or render in place, as done here.
Pre-launch checklist
- **Everything passes
e()** —— includingvalue,textareaand error messages - **
novalidateonly disables browser hints**; server logic is unchanged - CSRF token —— write operations need one against cross-site forgery
- **
post_max_size/upload_max_filesize** —— tune ini for larger forms - Business-level dedupe —— unique constraints, idempotency keys
目标
做一个「联系我」表单:姓名、邮箱、网址、评论、性别。要求:必填项校验、格式校验、出错回显已填内容、成功页用 PRG 防重复提交。
表单页
<?php
// contact.php —— 表单与处理逻辑在同一文件
declare(strict_types=1);
$errors = [];
$old = ["name" => "", "email" => "", "website" => "", "comment" => "", "gender" => ""];
if (($_SERVER["REQUEST_METHOD"] ?? "") === "POST") {
foreach ($old as $k => $_) {
$old[$k] = trim((string)($_POST[$k] ?? ""));
}
if ($old["name"] === "") {
$errors["name"] = "姓名必填";
} elseif (mb_strlen($old["name"]) > 20) {
$errors["name"] = "姓名最多 20 个字";
}
if ($old["email"] === "") {
$errors["email"] = "邮箱必填";
} elseif (filter_var($old["email"], FILTER_VALIDATE_EMAIL) === false) {
$errors["email"] = "邮箱格式不正确";
}
if ($old["website"] !== "" && filter_var($old["website"], FILTER_VALIDATE_URL) === false) {
$errors["website"] = "网址格式不正确";
}
if (!in_array($old["gender"], ["female", "male"], true)) {
$errors["gender"] = "请选择性别";
}
if (!$errors) {
// 成功:写入会话后重定向(PRG)
$_SESSION["contact_ok"] = true;
header("Location: thanks.php");
exit;
}
}
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}
?>
<form method="post" novalidate>
<p>
<label>姓名 *</label>
<input name="name" value="<?= e($old["name"]) ?>">
<em><?= e($errors["name"] ?? "") ?></em>
</p>
<p>
<label>邮箱 *</label>
<input name="email" value="<?= e($old["email"]) ?>">
<em><?= e($errors["email"] ?? "") ?></em>
</p>
<p>
<label>网址</label>
<input name="website" value="<?= e($old["website"]) ?>">
<em><?= e($errors["website"] ?? "") ?></em>
</p>
<p>
<label>评论</label>
<textarea name="comment"><?= e($old["comment"]) ?></textarea>
</p>
<p>
<label>性别 *</label>
<label><input type="radio" name="gender" value="female"
<?= $old["gender"] === "female" ? "checked" : "" ?>>女</label>
<label><input type="radio" name="gender" value="male"
<?= $old["gender"] === "male" ? "checked" : "" ?>>男</label>
<em><?= e($errors["gender"] ?? "") ?></em>
</p>
<button>提交</button>
</form>提交非法数据(姓名留空、邮箱写 bad@)时,渲染结果里会带上错误提示和已填内容:
<p>
<label>姓名 *</label>
<input name="name" value="">
<em>姓名必填</em>
</p>
<p>
<label>邮箱 *</label>
<input name="email" value="bad@">
<em>邮箱格式不正确</em>
</p>成功页
<?php
// thanks.php
session_start();
if (empty($_SESSION["contact_ok"])) {
header("Location: contact.php");
exit;
}
unset($_SESSION["contact_ok"]);
echo "提交成功,我们会尽快联系你。";提交成功,我们会尽快联系你。直接访问 thanks.php 会被踢回表单页——因为会话里没有成功标记。这比「显示一个空的成功页」严谨。
PRG 模式
Post/Redirect/Get 的三步:
- 浏览器 POST 提交表单
- 服务端处理完,返回
302 Location: thanks.php - 浏览器 GET 那个地址,地址栏变成 GET 页
用户按 F5 刷新的是 GET 页面,不会重复提交。代价是错误信息不能直接 echo,得先存进会话或直接在同一页渲染(本例的做法)。
上线前检查清单
- **所有输出过
e()** —— 包括value、textarea、错误提示 - **
novalidate只是关掉浏览器提示**,服务端逻辑照旧 - CSRF token —— 写操作要带 token,防止跨站伪造提交
- **
post_max_size与upload_max_filesize** —— 表单大了要调 ini - 重复提交的业务兜底 —— 唯一约束、幂等键