Skip to content

表单处理

表单是 Web 应用中用户与服务器交互的核心方式。PHP 作为服务端脚本语言,提供了丰富的功能来接收、验证和处理表单数据。本节将全面介绍如何使用 PHP 处理 HTML 表单,包括 GET 和 POST 请求的区别、表单验证、数据安全(防 XSS 攻击)、表单持久化(Sticky Forms)以及多值表单(复选框、多选框)的处理。

前置知识

阅读本节前,你需要:

  • 已阅读过 基础交互脚本 一节,了解 PHP 基本语法
  • 了解 HTML 表单的基础知识(<form><input><select> 等标签)
  • 知道 HTTP GET 和 POST 请求的基本区别

基础概念

HTML 表单基础

HTML 表单通过 <form> 标签定义,用户在表单中输入的数据会被发送到服务器指定的 URL 进行处理。以下是一个基本的 HTML 表单结构:

php
<?php
declare(strict_types=1);

// basic-form.php — 最简单的表单示例
// 将 PHP 处理逻辑和 HTML 表单放在同一个文件中
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>基本表单示例</title>
</head>
<body>
    <h1>用户注册</h1>
    <?php
    // 处理表单提交
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $username = $_POST['username'] ?? '';
        $email = $_POST['email'] ?? '';

        echo "<p>提交成功!</p>";
        echo "<p>用户名:" . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . "</p>";
        echo "<p>邮箱:" . htmlspecialchars($email, ENT_QUOTES, 'UTF-8') . "</p>";
    }
    ?>
    <form method="POST" action="">
        <label for="username">用户名:</label>
        <input type="text" id="username" name="username" required><br><br>

        <label for="email">邮箱:</label>
        <input type="email" id="email" name="email" required><br><br>

        <button type="submit">提交</button>
    </form>
</body>
</html>

GET 与 POST 的区别

PHP 提供了两个超全局数组来接收表单数据:$_GET$_POST,分别对应 HTTP 的 GET 和 POST 请求方法:

特性GET ($_GET)POST ($_POST)
数据位置URL 查询字符串中HTTP 请求体中
数据长度有 URL 长度限制(约 2000 字符)理论上无限制(受 post_max_size 配置)
可见性数据在 URL 中可见(不安全)数据不在 URL 中(相对安全)
浏览器历史参数保存在历史记录中不保存在历史记录中
缓存可被缓存不可缓存
安全性不适合敏感数据适合敏感数据
用途搜索、筛选、分页注册、登录、数据修改
幂等性幂等(重复请求安全)非幂等

安全原则

  • 永远不要用 GET 请求提交密码等敏感信息。
  • POST 并不意味着绝对安全,仍需配合 HTTPS 使用。
  • 所有的表单数据,无论来自 GET 还是 POST,都必须进行验证和转义。

PHP 接收表单数据

使用 $_GET 接收数据

$_GET 用于接收通过 URL 查询字符串传递的数据:

php
<?php
declare(strict_types=1);

// search.php — 使用 GET 方法的搜索页面
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>商品搜索</title>
</head>
<body>
    <h1>商品搜索</h1>

    <form method="GET" action="">
        <label for="keyword">关键词:</label>
        <input type="text" id="keyword" name="q"
               value="<?= htmlspecialchars($_GET['q'] ?? '', ENT_QUOTES, 'UTF-8') ?>">

        <label for="category">分类:</label>
        <select id="category" name="category">
            <option value="">全部分类</option>
            <option value="electronics" <?= ($_GET['category'] ?? '') === 'electronics' ? 'selected' : '' ?>>电子产品</option>
            <option value="books" <?= ($_GET['category'] ?? '') === 'books' ? 'selected' : '' ?>>图书</option>
            <option value="clothing" <?= ($_GET['category'] ?? '') === 'clothing' ? 'selected' : '' ?>>服装</option>
        </select>

        <button type="submit">搜索</button>
    </form>

    <hr>

    <?php
    $keyword = trim($_GET['q'] ?? '');
    $category = $_GET['category'] ?? '';

    if ($keyword !== '') {
        echo "<p>搜索关键词:<strong>" . htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8') . "</strong></p>";
        if ($category !== '') {
            $categoryNames = [
                'electronics' => '电子产品',
                'books' => '图书',
                'clothing' => '服装',
            ];
            echo "<p>分类:" . ($categoryNames[$category] ?? $category) . "</p>";
        }
        echo "<p>(这里应该是实际的搜索结果展示区域)</p>";
    } else {
        echo "<p>请输入关键词进行搜索。</p>";
    }
    ?>
</body>
</html>

使用 $_POST 接收数据

$_POST 用于接收通过 HTTP POST 请求体传递的数据,适合提交敏感信息或大量数据:

php
<?php
declare(strict_types=1);

// login.php — 使用 POST 方法的登录页面

$errors = [];
$formData = [
    'email' => '',
    'password' => '',
    'remember' => false,
];

// 处理 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $formData['email'] = trim($_POST['email'] ?? '');
    $formData['password'] = $_POST['password'] ?? '';
    $formData['remember'] = isset($_POST['remember']);

    if ($formData['email'] === '') {
        $errors['email'] = '请输入邮箱地址';
    } elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = '邮箱格式不正确';
    }

    if ($formData['password'] === '') {
        $errors['password'] = '请输入密码';
    } elseif (strlen($formData['password']) < 6) {
        $errors['password'] = '密码长度不能少于 6 个字符';
    }

    if (empty($errors)) {
        echo "<p>登录验证通过!(示例演示)</p>";
        echo "<p>邮箱:" . htmlspecialchars($formData['email'], ENT_QUOTES, 'UTF-8') . "</p>";
        echo "<p>记住我:" . ($formData['remember'] ? '是' : '否') . "</p>";
    }
}
?>
<!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;">
        <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="email">邮箱:</label>
            <input type="email" id="email" name="email"
                   value="<?= htmlspecialchars($formData['email'], ENT_QUOTES, 'UTF-8') ?>"
                   required>
            <?php if (isset($errors['email'])): ?>
            <span style="color: red;"><?= htmlspecialchars($errors['email'], ENT_QUOTES, 'UTF-8') ?></span>
            <?php endif; ?>
        </div>
        <br>
        <div>
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required>
            <?php if (isset($errors['password'])): ?>
            <span style="color: red;"><?= htmlspecialchars($errors['password'], ENT_QUOTES, 'UTF-8') ?></span>
            <?php endif; ?>
        </div>
        <br>
        <div>
            <label>
                <input type="checkbox" name="remember"
                       <?= $formData['remember'] ? 'checked' : '' ?>>
                记住我
            </label>
        </div>
        <br>
        <button type="submit">登录</button>
    </form>
</body>
</html>

表单验证

服务端验证

无论客户端做了多少验证,服务端验证都是必须的。以下是一个完整的表单验证器类:

php
<?php
declare(strict_types=1);

/**
 * 表单验证器类
 * 提供常用的表单验证方法
 */
class FormValidator
{
    private array $errors = [];
    private array $data = [];

    public function validate(array $data, array $rules): bool
    {
        $this->data = $data;
        $this->errors = [];

        foreach ($rules as $field => $fieldRules) {
            $value = $data[$field] ?? null;

            foreach ($fieldRules as $rule => $param) {
                $this->applyRule($field, $value, $rule, $param);
                if (isset($this->errors[$field])) {
                    break;
                }
            }
        }

        return empty($this->errors);
    }

    private function applyRule(string $field, mixed $value, string $rule, mixed $param): void
    {
        $fieldLabel = is_array($param) ? ($param['label'] ?? $field) : $field;

        match ($rule) {
            'required' => $this->validateRequired($field, $value, $fieldLabel),
            'email' => $this->validateEmail($field, $value, $fieldLabel),
            'min_length' => $this->validateMinLength($field, $value, (int) $param, $fieldLabel),
            'max_length' => $this->validateMaxLength($field, $value, (int) $param, $fieldLabel),
            'pattern' => $this->validatePattern($field, $value, $param, $fieldLabel),
            'confirmed' => $this->validateConfirmed($field, $value, $param, $fieldLabel),
            default => null,
        };
    }

    private function validateRequired(string $field, mixed $value, string $label): void
    {
        if ($value === null || $value === '') {
            $this->errors[$field] = "{$label} 不能为空";
        }
    }

    private function validateEmail(string $field, mixed $value, string $label): void
    {
        if ($value !== '' && $value !== null && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = "{$label} 格式不正确";
        }
    }

    private function validateMinLength(string $field, mixed $value, int $min, string $label): void
    {
        if ($value !== null && mb_strlen((string) $value, 'UTF-8') < $min) {
            $this->errors[$field] = "{$label} 长度不能少于 {$min} 个字符";
        }
    }

    private function validateMaxLength(string $field, mixed $value, int $max, string $label): void
    {
        if ($value !== null && mb_strlen((string) $value, 'UTF-8') > $max) {
            $this->errors[$field] = "{$label} 长度不能超过 {$max} 个字符";
        }
    }

    private function validatePattern(string $field, mixed $value, string $pattern, string $label): void
    {
        if ($value !== '' && $value !== null && !preg_match($pattern, (string) $value)) {
            $this->errors[$field] = "{$label} 格式不正确";
        }
    }

    private function validateConfirmed(string $field, mixed $value, string $otherField, string $label): void
    {
        $otherValue = $this->data[$otherField] ?? null;
        if ($value !== $otherValue) {
            $this->errors[$field] = "{$label} 两次输入不一致";
        }
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function getFirstError(): ?string
    {
        return empty($this->errors) ? null : reset($this->errors);
    }

    public function hasError(string $field): bool
    {
        return isset($this->errors[$field]);
    }

    public function getError(string $field): string
    {
        return $this->errors[$field] ?? '';
    }
}

// 使用示例
$validator = new FormValidator();
$rules = [
    'username' => [
        'required' => '用户名',
        'min_length' => 2,
        'max_length' => 20,
        'pattern' => '/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u',
    ],
    'email' => [
        'required' => '邮箱',
        'email' => '邮箱',
    ],
    'password' => [
        'required' => '密码',
        'min_length' => 8,
    ],
    'password_confirm' => [
        'required' => '确认密码',
        'confirmed' => 'password',
    ],
];

// 模拟表单数据
$formData = [
    'username' => '张三',
    'email' => 'zhangsan@example.com',
    'password' => '12345678',
    'password_confirm' => '12345678',
];

$isValid = $validator->validate($formData, $rules);
echo "验证结果: " . ($isValid ? '通过' : '失败') . "\n";

if (!$isValid) {
    foreach ($validator->getErrors() as $field => $error) {
        echo "  {$field}: {$error}\n";
    }
}

数据安全 — 防止 XSS 攻击

htmlspecialchars 的使用

跨站脚本攻击(XSS)是 Web 应用中最常见的安全漏洞之一。PHP 提供了 htmlspecialchars() 函数来对输出进行转义,防止 XSS 攻击:

php
<?php
declare(strict_types=1);

/**
 * 安全输出辅助类
 * 封装常用的安全转义函数
 */
class SecurityHelper
{
    /**
     * 安全地输出 HTML 内容
     * 将特殊字符转换为 HTML 实体
     */
    public static function escapeHtml(string $string): string
    {
        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    /**
     * 安全地输出 HTML 属性
     */
    public static function escapeAttr(string $string): string
    {
        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    /**
     * 净化 HTML 内容(移除危险的标签和属性)
     * 简单实现,生产环境建议使用 HTML Purifier 库
     */
    public static function sanitizeHtml(string $html): string
    {
        // 移除 script 标签
        $html = preg_replace('#<script[^>]*>.*?</script>#is', '', $html);
        // 移除 onclick 等事件属性
        $html = preg_replace('#\s*on\w+\s*=\s*["\'][^"\']*["\']#i', '', $html);
        // 移除 javascript: 协议
        $html = preg_replace('#javascript:#i', '', $html);
        return $html;
    }

    /**
     * 生成 CSRF 令牌
     */
    public static function generateCsrfToken(): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $token = bin2hex(random_bytes(32));
        $_SESSION['csrf_token'] = $token;
        return $token;
    }

    /**
     * 验证 CSRF 令牌
     */
    public static function verifyCsrfToken(string $token): bool
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
    }
}

// XSS 攻击演示 — 不转义的危险
$maliciousInput = '<script>alert("XSS攻击!")</script><img src="x" onerror="alert(\'hack\')">';
echo "原始输入: {$maliciousInput}\n";
echo "转义输出: " . SecurityHelper::escapeHtml($maliciousInput) . "\n";
echo "净化HTML: " . SecurityHelper::sanitizeHtml($maliciousInput) . "\n";

// 用户输入的安全处理示例
$userInput = $_GET['name'] ?? 'World';
$safeOutput = SecurityHelper::escapeHtml($userInput);
echo "安全输出: {$safeOutput}\n";

htmlspecialchars 参数说明

  • ENT_QUOTES:转义单引号和双引号(推荐)
  • ENT_HTML5:使用 HTML5 实体模式
  • 'UTF-8':指定字符编码(必须与页面编码一致)

始终使用这三个参数的组合:htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8')

表单持久化 — Sticky Forms

保持表单数据

当表单验证失败时,应该保留用户已输入的数据(称为 Sticky Forms),避免用户重新填写所有字段:

php
<?php
declare(strict_types=1);

// contact-form.php — 带有持久化和完整验证的联系表单

session_start();

$errors = [];
$oldInput = [];

// 定义表单字段及其默认值
$fields = [
    'name' => '',
    'email' => '',
    'phone' => '',
    'subject' => 'general',
    'message' => '',
    'priority' => 'normal',
];

// 处理 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 保存旧输入(用于表单回填)
    $oldInput = [
        'name' => trim($_POST['name'] ?? ''),
        'email' => trim($_POST['email'] ?? ''),
        'phone' => trim($_POST['phone'] ?? ''),
        'subject' => $_POST['subject'] ?? 'general',
        'message' => trim($_POST['message'] ?? ''),
        'priority' => $_POST['priority'] ?? 'normal',
    ];

    // 验证规则
    if ($oldInput['name'] === '') {
        $errors['name'] = '请输入您的姓名';
    } elseif (mb_strlen($oldInput['name'], 'UTF-8') > 50) {
        $errors['name'] = '姓名不能超过 50 个字符';
    }

    if ($oldInput['email'] === '') {
        $errors['email'] = '请输入邮箱地址';
    } elseif (!filter_var($oldInput['email'], FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = '邮箱格式不正确';
    }

    if ($oldInput['phone'] !== '' && !preg_match('/^1[3-9]\d{9}$/', $oldInput['phone'])) {
        $errors['phone'] = '手机号格式不正确';
    }

    if ($oldInput['message'] === '') {
        $errors['message'] = '请输入留言内容';
    } elseif (mb_strlen($oldInput['message'], 'UTF-8') < 10) {
        $errors['message'] = '留言内容不能少于 10 个字符';
    } elseif (mb_strlen($oldInput['message'], 'UTF-8') > 2000) {
        $errors['message'] = '留言内容不能超过 2000 个字符';
    }

    // 验证通过
    if (empty($errors)) {
        $_SESSION['form_success'] = true;
        $_SESSION['form_message'] = '感谢您的留言!我们会尽快回复。';
        header('Location: ' . $_SERVER['PHP_SELF']);
        exit;
    }
}

// 显示成功消息
if (isset($_SESSION['form_success'])) {
    $successMessage = $_SESSION['form_message'];
    unset($_SESSION['form_success'], $_SESSION['form_message']);
}

// 辅助函数:获取旧输入值
function old(string $field, string $default = ''): string
{
    global $oldInput;
    return htmlspecialchars($oldInput[$field] ?? $default, ENT_QUOTES, 'UTF-8');
}

// 辅助函数:检查字段是否有错误
function hasError(string $field): bool
{
    global $errors;
    return isset($errors[$field]);
}

// 辅助函数:获取错误信息
function getError(string $field): string
{
    global $errors;
    return htmlspecialchars($errors[$field] ?? '', ENT_QUOTES, 'UTF-8');
}

// 辅助函数:选中选项
function selected(string $field, string $value): string
{
    global $oldInput;
    return ($oldInput[$field] ?? '') === $value ? 'selected' : '';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>联系我们</title>
</head>
<body>
    <h1>联系我们</h1>

    <?php if (isset($successMessage)): ?>
    <div style="color: green; padding: 10px; background: #e8f5e9; border-radius: 4px;">
        <?= htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8') ?>
    </div>
    <?php endif; ?>

    <form method="POST" action="">
        <div>
            <label for="name">姓名 *</label>
            <input type="text" id="name" name="name" value="<?= old('name') ?>" required>
            <?php if (hasError('name')): ?>
            <small style="color: red;"><?= getError('name') ?></small>
            <?php endif; ?>
        </div>
        <br>

        <div>
            <label for="email">邮箱 *</label>
            <input type="email" id="email" name="email" value="<?= old('email') ?>" required>
            <?php if (hasError('email')): ?>
            <small style="color: red;"><?= getError('email') ?></small>
            <?php endif; ?>
        </div>
        <br>

        <div>
            <label for="phone">手机号</label>
            <input type="tel" id="phone" name="phone" value="<?= old('phone') ?>"
                   placeholder="请输入 11 位手机号">
            <?php if (hasError('phone')): ?>
            <small style="color: red;"><?= getError('phone') ?></small>
            <?php endif; ?>
        </div>
        <br>

        <div>
            <label for="subject">主题</label>
            <select id="subject" name="subject">
                <option value="general" <?= selected('subject', 'general') ?>>一般咨询</option>
                <option value="support" <?= selected('subject', 'support') ?>>技术支持</option>
                <option value="business" <?= selected('subject', 'business') ?>>商务合作</option>
                <option value="other" <?= selected('subject', 'other') ?>>其他</option>
            </select>
        </div>
        <br>

        <div>
            <label for="message">留言内容 *</label>
            <textarea id="message" name="message" rows="5" cols="40"
                      required><?= old('message') ?></textarea>
            <?php if (hasError('message')): ?>
            <small style="color: red;"><?= getError('message') ?></small>
            <?php endif; ?>
        </div>
        <br>

        <div>
            <label>优先级:</label>
            <label>
                <input type="radio" name="priority" value="low"
                       <?= selected('priority', 'low') ?>>

            </label>
            <label>
                <input type="radio" name="priority" value="normal"
                       <?= selected('priority', 'normal') ?>>
                普通
            </label>
            <label>
                <input type="radio" name="priority" value="high"
                       <?= selected('priority', 'high') ?>>

            </label>
        </div>
        <br>

        <button type="submit">提交</button>
    </form>
</body>
</html>

多值表单处理

复选框和多选框

处理复选框(checkbox)和多选框(select multiple)需要特别注意,因为这类表单元素可以提交多个值:

php
<?php
declare(strict_types=1);

// multi-value-form.php — 多值表单处理示例

// PHP 中处理多值表单的关键是在 HTML 中使用数组语法
// 例如:name="skills[]" 而不是 name="skills"
// 也可以使用 name="user[address][city]" 创建嵌套数组

$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 收集多值表单数据
    $username = trim($_POST['username'] ?? '');
    $email = trim($_POST['email'] ?? '');

    // 复选框 — 兴趣爱好(可能没有选中任何项)
    $hobbies = $_POST['hobbies'] ?? [];
    if (!is_array($hobbies)) {
        $hobbies = [];
    }

    // 多选框 — 编程语言技能
    $skills = $_POST['skills'] ?? [];
    if (!is_array($skills)) {
        $skills = [];
    }

    // 单选按钮 — 经验等级
    $experience = $_POST['experience'] ?? 'junior';

    // 验证
    if ($username === '') {
        $errors['username'] = '请输入用户名';
    }
    if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = '请输入有效的邮箱地址';
    }

    if (empty($errors)) {
        $success = true;
    }
}

// 辅助函数:检查复选框/多选框是否被选中
function isChecked(string $name, string $value): string
{
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        return '';
    }
    $array = $_POST[$name] ?? [];
    return in_array($value, (array) $array, true) ? 'checked' : '';
}

function isSelected(string $name, string $value): string
{
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        return '';
    }
    $array = $_POST[$name] ?? [];
    return in_array($value, (array) $array, true) ? 'selected' : '';
}

// 允许的兴趣爱好选项
$availableHobbies = [
    'reading' => '阅读',
    'gaming' => '游戏',
    'coding' => '编程',
    'music' => '音乐',
    'sports' => '运动',
    'travel' => '旅行',
];

// 允许的编程语言选项
$availableSkills = [
    'php' => 'PHP',
    'javascript' => 'JavaScript',
    'python' => 'Python',
    'java' => 'Java',
    'go' => 'Go',
    'rust' => 'Rust',
    'typescript' => 'TypeScript',
    'csharp' => 'C#',
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>多值表单示例</title>
</head>
<body>
    <h1>开发者调查问卷</h1>

    <?php if ($success): ?>
    <div style="color: green;">
        <p>提交成功!以下是您的选择:</p>
        <ul>
            <li>用户名:<?= htmlspecialchars($username, ENT_QUOTES, 'UTF-8') ?></li>
            <li>邮箱:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?></li>
            <li>兴趣:<?= implode(', ', array_map(fn($h) => $availableHobbies[$h] ?? $h, $hobbies)) ?></li>
            <li>技能:<?= implode(', ', array_map(fn($s) => $availableSkills[$s] ?? $s, $skills)) ?></li>
            <li>经验:<?= htmlspecialchars($experience, ENT_QUOTES, 'UTF-8') ?></li>
        </ul>
    </div>
    <?php endif; ?>

    <form method="POST" action="">
        <!-- 复选框组 -->
        <fieldset>
            <legend>兴趣爱好(可多选)</legend>
            <?php foreach ($availableHobbies as $value => $label): ?>
            <label>
                <input type="checkbox" name="hobbies[]" value="<?= $value ?>"
                       <?= isChecked('hobbies', $value) ?>>
                <?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
            </label>
            <?php endforeach; ?>
        </fieldset>
        <br>

        <!-- 多选下拉框 -->
        <fieldset>
            <legend>编程语言技能(按住 Ctrl/Cmd 多选)</legend>
            <select name="skills[]" multiple size="5">
                <?php foreach ($availableSkills as $value => $label): ?>
                <option value="<?= $value ?>" <?= isSelected('skills', $value) ?>>
                    <?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
                </option>
                <?php endforeach; ?>
            </select>
        </fieldset>
        <br>

        <!-- 单选按钮 -->
        <fieldset>
            <legend>经验等级</legend>
            <label>
                <input type="radio" name="experience" value="junior"
                       <?= ($_POST['experience'] ?? 'junior') === 'junior' ? 'checked' : '' ?>>
                初级(1-3 年)
            </label>
            <label>
                <input type="radio" name="experience" value="mid"
                       <?= ($_POST['experience'] ?? '') === 'mid' ? 'checked' : '' ?>>
                中级(3-5 年)
            </label>
            <label>
                <input type="radio" name="experience" value="senior"
                       <?= ($_POST['experience'] ?? '') === 'senior' ? 'checked' : '' ?>>
                高级(5 年以上)
            </label>
        </fieldset>
        <br>

        <button type="submit">提交</button>
    </form>
</body>
</html>

实战示例

带文件上传的完整注册表单

php
<?php
declare(strict_types=1);

/**
 * 完整的用户注册表单处理
 * 展示了表单验证、CSRF 防护、文件上传、数据持久化
 */
class RegistrationHandler
{
    private array $errors = [];
    private array $data = [];
    private string $csrfToken = '';
    private ?string $uploadedAvatar = null;

    public function __construct()
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    public function generateCsrfToken(): string
    {
        $this->csrfToken = bin2hex(random_bytes(32));
        $_SESSION['csrf_token'] = $this->csrfToken;
        $_SESSION['csrf_token_time'] = time();
        return $this->csrfToken;
    }

    private function verifyCsrf(): bool
    {
        $token = $_POST['_token'] ?? '';
        $storedToken = $_SESSION['csrf_token'] ?? '';
        $tokenTime = $_SESSION['csrf_token_time'] ?? 0;

        // 令牌过期检查(30 分钟)
        if (time() - $tokenTime > 1800) {
            return false;
        }

        return hash_equals($storedToken, $token);
    }

    public function handleSubmission(): bool
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            return false;
        }

        // CSRF 验证
        if (!$this->verifyCsrf()) {
            $this->errors['_token'] = '安全验证失败,请重新提交';
            return false;
        }

        // 收集数据
        $this->data = [
            'username' => trim($_POST['username'] ?? ''),
            'email' => trim($_POST['email'] ?? ''),
            'password' => $_POST['password'] ?? '',
            'passwordConfirm' => $_POST['password_confirm'] ?? '',
            'bio' => trim($_POST['bio'] ?? ''),
            'website' => trim($_POST['website'] ?? ''),
            'gender' => $_POST['gender'] ?? '',
        ];

        // 表单验证
        $this->validateForm();

        // 处理头像上传
        if (!isset($this->errors['avatar'])) {
            $this->handleAvatarUpload();
        }

        return empty($this->errors);
    }

    private function validateForm(): void
    {
        // 用户名验证
        if ($this->data['username'] === '') {
            $this->errors['username'] = '用户名不能为空';
        } elseif (mb_strlen($this->data['username'], 'UTF-8') < 3) {
            $this->errors['username'] = '用户名至少 3 个字符';
        } elseif (mb_strlen($this->data['username'], 'UTF-8') > 20) {
            $this->errors['username'] = '用户名不能超过 20 个字符';
        } elseif (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $this->data['username'])) {
            $this->errors['username'] = '用户名只能包含字母、数字、下划线和中文';
        }

        // 邮箱验证
        if ($this->data['email'] === '') {
            $this->errors['email'] = '邮箱不能为空';
        } elseif (!filter_var($this->data['email'], FILTER_VALIDATE_EMAIL)) {
            $this->errors['email'] = '邮箱格式不正确';
        }

        // 密码验证
        if ($this->data['password'] === '') {
            $this->errors['password'] = '密码不能为空';
        } elseif (strlen($this->data['password']) < 8) {
            $this->errors['password'] = '密码至少 8 个字符';
        } elseif (!preg_match('/[a-z]/', $this->data['password']) || !preg_match('/[A-Z]/', $this->data['password'])) {
            $this->errors['password'] = '密码必须包含大小写字母';
        } elseif (!preg_match('/\d/', $this->data['password'])) {
            $this->errors['password'] = '密码必须包含数字';
        }

        // 确认密码
        if ($this->data['password'] !== $this->data['passwordConfirm']) {
            $this->errors['password_confirm'] = '两次密码输入不一致';
        }

        // 个人简介
        if (mb_strlen($this->data['bio'], 'UTF-8') > 500) {
            $this->errors['bio'] = '个人简介不能超过 500 个字符';
        }

        // 网站验证
        if ($this->data['website'] !== '' && !filter_var($this->data['website'], FILTER_VALIDATE_URL)) {
            $this->errors['website'] = '网站地址格式不正确';
        }
    }

    private function handleAvatarUpload(): void
    {
        if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] === UPLOAD_ERR_NO_FILE) {
            return;
        }

        $file = $_FILES['avatar'];

        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors['avatar'] = '文件上传失败';
            return;
        }

        // 检查文件大小(最大 2MB)
        if ($file['size'] > 2 * 1024 * 1024) {
            $this->errors['avatar'] = '头像文件大小不能超过 2MB';
            return;
        }

        // 检查文件类型
        $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->file($file['tmp_name']);

        if (!in_array($mimeType, $allowedTypes, true)) {
            $this->errors['avatar'] = '只支持 JPG、PNG、GIF、WebP 格式的图片';
            return;
        }

        // 生成安全的文件名
        $extension = match ($mimeType) {
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'image/gif' => 'gif',
            'image/webp' => 'webp',
            default => 'bin',
        };
        $safeName = bin2hex(random_bytes(16)) . '.' . $extension;
        $this->uploadedAvatar = '/uploads/avatars/' . $safeName;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function getData(): array
    {
        return $this->data;
    }

    public function getCsrfToken(): string
    {
        return $this->csrfToken;
    }
}

// 使用示例(CLI 模式下的演示)
if (PHP_SAPI === 'cli') {
    $handler = new RegistrationHandler();
    $token = $handler->generateCsrfToken();
    echo "CSRF Token 已生成: " . substr($token, 0, 16) . "...\n";
    echo "注册处理器已就绪,在 Web 模式下可处理完整的表单提交。\n";
}

注意事项

  • 永远不要信任用户输入:无论前端做了多少验证,服务端必须进行完整的验证。
  • 始终转义输出:使用 htmlspecialchars() 转义所有输出到 HTML 的用户数据,防止 XSS 攻击。
  • 使用 CSRF 令牌:所有修改数据的表单都应该包含 CSRF 令牌,防止跨站请求伪造攻击。
  • 文件上传安全:处理文件上传时,始终检查文件类型(使用 finfo 而不是扩展名)、文件大小、上传路径。不要使用原始文件名。
  • 密码安全:永远不要以明文存储密码。使用 password_hash()password_verify() 处理密码。
  • POST 重定向模式:表单处理成功后,使用 PRG(Post/Redirect/Get)模式,避免用户刷新页面导致重复提交。
  • $_POST 的限制$_POST 只能接收 application/x-www-form-urlencodedmultipart/form-data 类型的数据。如果需要接收 JSON 数据,使用 file_get_contents('php://input')

最佳实践

  • 使用 PRG 模式(Post/Redirect/Get)防止表单重复提交。
  • 使用 CSRF 令牌保护所有修改数据的表单。
  • 使用 filter_var() 配合预定义过滤器进行数据验证,避免手写正则表达式。
  • 为每个表单字段提供清晰的错误提示信息。
  • 使用 <label> 标签关联表单控件,提高可访问性。
  • 文件名使用数组语法(name="files[]")处理多文件上传。
  • 使用 readonlydisabled 属性防止用户修改不应修改的字段。
  • 在生产环境中使用框架的表单组件(如 Laravel 的 Form Request Validation、Symfony 的 Form Component)。

下一节

掌握了表单处理后,你已经能够构建交互式的 Web 页面。接下来让我们学习 数据库交互,了解如何将数据持久化到数据库中。

参考链接