表单提交与验证
表单是 Web 应用与用户交互的核心方式。PHP 通过 $_POST、$_GET 和 $_FILES 超全局变量接收表单数据。本节将深入讲解表单数据的接收、验证规则的设计、客户端与服务端双重验证的最佳实践,以及构建可复用的表单验证器。
前置知识
阅读本节前,建议先了解:GET 与 POST 请求、PHP 超全局变量
基础概念
表单数据处理流程
用户填写表单 -> 客户端验证(JS) -> 提交到服务端 -> 服务端验证(PHP) -> 处理/存储 -> 返回响应
↓ 失败 ↓ 失败
返回错误信息 回滚/错误提示核心原则
永远不要信任客户端数据。所有验证必须在服务端执行。客户端验证只是提升用户体验,不能替代服务端验证。攻击者可以轻易绕过 JavaScript 验证直接提交恶意数据。
表单提交方式
| 方式 | method | 数据位置 | 适用场景 |
|---|---|---|---|
| URL 参数 | GET | URL 查询字符串 | 搜索、筛选、分页 |
| 表单体 | POST | HTTP 请求体 | 创建、修改、删除 |
| JSON | POST/PUT | 请求体(JSON) | API 接口 |
| FormData | POST | 请求体(multipart) | 文件上传 |
| AJAX | GET/POST | 取决于 method | 动态交互 |
语法与代码示例
基本 HTML 表单与 PHP 处理
php
<?php
declare(strict_types=1);
// === 完整的表单处理流程 ===
$errors = [];
$formData = [
'username' => '',
'email' => '',
'age' => '',
'website' => '',
'bio' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 获取数据
$formData['username'] = trim($_POST['username'] ?? '');
$formData['email'] = trim($_POST['email'] ?? '');
$formData['age'] = trim($_POST['age'] ?? '');
$formData['website'] = trim($_POST['website'] ?? '');
$formData['bio'] = trim($_POST['bio'] ?? '');
// 2. 验证数据
// 用户名:必填,3-20 个字符,仅字母数字下划线
if ($formData['username'] === '') {
$errors['username'] = '用户名不能为空';
} elseif (mb_strlen($formData['username']) < 3 || mb_strlen($formData['username']) > 20) {
$errors['username'] = '用户名长度为 3-20 个字符';
} elseif (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $formData['username'])) {
$errors['username'] = '用户名只能包含字母、数字、下划线和中文';
}
// 邮箱:必填,有效格式
if ($formData['email'] === '') {
$errors['email'] = '邮箱不能为空';
} elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
// 年龄:可选,必须是有效整数 1-150
if ($formData['age'] !== '') {
$age = filter_var($formData['age'], FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 150],
]);
if ($age === false) {
$errors['age'] = '年龄必须是 1-150 之间的整数';
}
}
// 网站:可选,必须是有效 URL
if ($formData['website'] !== '' && !filter_var($formData['website'], FILTER_VALIDATE_URL)) {
$errors['website'] = 'URL 格式不正确';
}
// 简介:可选,最大 500 字符
if (mb_strlen($formData['bio']) > 500) {
$errors['bio'] = '简介不能超过 500 个字符';
}
// 3. 处理验证结果
if (empty($errors)) {
// 验证通过,处理数据
echo "<p>表单提交成功!</p>";
// PRG 模式重定向
// header('Location: /success');
// exit;
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>用户注册</title>
</head>
<body>
<h1>用户注册</h1>
<?php if (!empty($errors)): ?>
<div style="color: red;">
<p>请修正以下错误:</p>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST" action="">
<div>
<label for="username">用户名 *</label><br>
<input type="text" id="username" name="username"
value="<?= htmlspecialchars($formData['username'], ENT_QUOTES, 'UTF-8') ?>"
maxlength="20" required>
</div>
<div>
<label for="email">邮箱 *</label><br>
<input type="email" id="email" name="email"
value="<?= htmlspecialchars($formData['email'], ENT_QUOTES, 'UTF-8') ?>"
required>
</div>
<div>
<label for="age">年龄</label><br>
<input type="number" id="age" name="age"
value="<?= htmlspecialchars($formData['age'], ENT_QUOTES, 'UTF-8') ?>"
min="1" max="150">
</div>
<div>
<button type="submit">注册</button>
</div>
</form>
</body>
</html>详细说明
使用 filter_var 验证
php
<?php
declare(strict_types=1);
// === filter_var 验证函数 ===
// 邮箱验证
$email = 'user@example.com';
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo "有效邮箱";
}
// URL 验证
$url = 'https://www.example.com';
if (filter_var($url, FILTER_VALIDATE_URL)) {
echo "有效 URL";
}
// IP 验证
$ip = '192.168.1.1';
if (filter_var($ip, FILTER_VALIDATE_IP)) {
echo "有效 IP";
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
echo "IPv4";
}
// 整数验证
$result = filter_var(42, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 100],
]);
// 浮点数验证
$result = filter_var(3.14, FILTER_VALIDATE_FLOAT, [
'options' => ['min_range' => 0.0, 'max_range' => 100.0],
'flags' => FILTER_FLAG_ALLOW_THOUSAND,
]);
// 布尔值验证
$result = filter_var('yes', FILTER_VALIDATE_BOOLEAN); // true
// 正则表达式验证
$result = filter_var('13800138000', FILTER_VALIDATE_REGEXP, [
'options' => ['regexp' => '/^1[3-9]\d{9}$/'],
]);
// 域名验证(PHP 7.0+)
if (filter_var('www.example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
echo "有效域名";
}
// MAC 地址验证
if (filter_var('00:1A:2B:3C:4D:5E', FILTER_VALIDATE_MAC)) {
echo "有效 MAC 地址";
}filter_input 验证表单数据
php
<?php
declare(strict_types=1);
// filter_input 直接获取并验证(推荐)
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 1, 'min_range' => 1],
]);
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$search = filter_input(INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);
$ip = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
// 获取数组参数
$tags = filter_input(INPUT_POST, 'tags', FILTER_DEFAULT, [
'flags' => FILTER_REQUIRE_ARRAY,
]);
// 过滤特殊字符(用于显示)
$username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_SPECIAL_CHARS);实战示例:可复用的表单验证器
php
<?php
declare(strict_types=1);
/**
* 可复用的表单验证器
*/
class Validator
{
private array $errors = [];
private array $data = [];
private array $rules = [];
public function setData(array $data): self
{
$this->data = $data;
return $this;
}
public function addRule(string $field, string $label, array $rules): self
{
$this->rules[$field] = ['label' => $label, 'rules' => $rules];
return $this;
}
public function validate(): bool
{
$this->errors = [];
foreach ($this->rules as $field => $config) {
$value = $this->data[$field] ?? null;
$label = $config['label'];
foreach ($config['rules'] as $rule => $params) {
$this->applyRule($field, $label, $value, $rule, $params);
if (isset($this->errors[$field])) {
break; // 一个字段只显示第一个错误
}
}
}
return empty($this->errors);
}
private function applyRule(string $field, string $label, mixed $value, string $rule, mixed $params): void
{
$paramStr = is_array($params) ? implode(', ', $params) : (string) $params;
match ($rule) {
'required' => $value !== null && $value !== ''
?: $this->errors[$field] = "{$label}不能为空",
'minLength' => is_string($value) && mb_strlen($value) >= (int) $params
?: $this->errors[$field] = "{$label}至少需要 {$paramStr} 个字符",
'maxLength' => is_string($value) && mb_strlen($value) <= (int) $params
?: $this->errors[$field] = "{$label}不能超过 {$paramStr} 个字符",
'email' => $value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)
?: $this->errors[$field] = "{$label}格式不正确",
'url' => $value === '' || filter_var($value, FILTER_VALIDATE_URL)
?: $this->errors[$field] = "{$label}格式不正确",
'integer' => $value === '' || filter_var($value, FILTER_VALIDATE_INT)
?: $this->errors[$field] = "{$label}必须是整数",
'min' => $value === '' || (is_numeric($value) && (float) $value >= (float) $params)
?: $this->errors[$field] = "{$label}不能小于 {$paramStr}",
'max' => $value === '' || (is_numeric($value) && (float) $value <= (float) $params)
?: $this->errors[$field] = "{$label}不能大于 {$paramStr}",
'regex' => $value === '' || preg_match((string) $params, (string) $value)
?: $this->errors[$field] = "{$label}格式不正确",
'confirmed' => isset($this->data[$params]) && $value === $this->data[$params]
?: $this->errors[$field] = "{$label}两次输入不一致",
'in' => in_array($value, (array) $params, true)
?: $this->errors[$field] = "{$label}值无效",
default => null,
};
}
public function getErrors(): array
{
return $this->errors;
}
public function hasError(string $field): bool
{
return isset($this->errors[$field]);
}
public function getError(string $field): string
{
return $this->errors[$field] ?? '';
}
}
// === 使用示例 ===
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$validator = (new Validator())
->setData($_POST)
->addRule('username', '用户名', [
'required' => true, 'minLength' => 3, 'maxLength' => 20,
'regex' => '/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u',
])
->addRule('email', '邮箱', [
'required' => true, 'email' => true, 'maxLength' => 255,
])
->addRule('password', '密码', [
'required' => true, 'minLength' => 8,
'regex' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
])
->addRule('password_confirm', '确认密码', [
'required' => true, 'confirmed' => 'password',
]);
if ($validator->validate()) {
echo "验证通过!";
} else {
foreach ($validator->getErrors() as $field => $error) {
echo "{$field}: {$error}\n";
}
}
}PRG 模式(Post/Redirect/Get)
php
<?php
declare(strict_types=1);
/**
* PRG 模式防止表单重复提交
*/
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$success = processForm($_POST);
if ($success) {
$_SESSION['flash_message'] = '保存成功';
$_SESSION['flash_type'] = 'success';
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
}
// 读取并清除 Flash 消息
$flashMessage = $_SESSION['flash_message'] ?? null;
$flashType = $_SESSION['flash_type'] ?? null;
unset($_SESSION['flash_message'], $_SESSION['flash_type']);注意事项
1. 永远不信任客户端验证
php
<?php
// 错误:依赖前端 HTML5 验证
// <input type="email" required> 可被绕过
// 正确:服务端始终验证
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if (!$email) {
// 拒绝
}2. 输出已验证的数据仍需编码
php
<?php
// 验证通过的数据在输出到 HTML 时仍需编码
$username = $_POST['username'] ?? '';
echo htmlspecialchars($username, ENT_QUOTES, 'UTF-8');最佳实践
1. 服务端验证 + 客户端验证
html
<!-- 客户端验证提升体验 -->
<input type="email" required minlength="5" maxlength="255">
<!-- 服务端验证确保安全(PHP 端必须执行) -->2. 验证规则集中管理
php
<?php
declare(strict_types=1);
class ValidationRules
{
public static function userRegistration(): array
{
return [
'username' => 'required|minLength:3|maxLength:20',
'email' => 'required|email|maxLength:255',
'password' => 'required|minLength:8',
'password_confirm' => 'required|confirmed:password',
];
}
}下一节
继续学习:CSRF 防护