Skip to content

表单提交与验证

表单是 Web 应用与用户交互的核心方式。PHP 通过 $_POST$_GET$_FILES 超全局变量接收表单数据。本节将深入讲解表单数据的接收、验证规则的设计、客户端与服务端双重验证的最佳实践,以及构建可复用的表单验证器。

前置知识

阅读本节前,建议先了解:GET 与 POST 请求PHP 超全局变量

基础概念

表单数据处理流程

用户填写表单 -> 客户端验证(JS) -> 提交到服务端 -> 服务端验证(PHP) -> 处理/存储 -> 返回响应
                                    ↓ 失败                    ↓ 失败
                               返回错误信息              回滚/错误提示

核心原则

永远不要信任客户端数据。所有验证必须在服务端执行。客户端验证只是提升用户体验,不能替代服务端验证。攻击者可以轻易绕过 JavaScript 验证直接提交恶意数据。

表单提交方式

方式method数据位置适用场景
URL 参数GETURL 查询字符串搜索、筛选、分页
表单体POSTHTTP 请求体创建、修改、删除
JSONPOST/PUT请求体(JSON)API 接口
FormDataPOST请求体(multipart)文件上传
AJAXGET/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 防护

参考链接