Menu

PHP 表单处理:提交、验证与回显

PHP 表单用 <form method="post"> 提交给自己,当 $_SERVER['REQUEST_METHOD'] 为 'POST' 时,脚本从 $_POST['name'] 读取字段。本文讲解表单验证、在字段旁显示错误信息、保留已填写的值、复选框和下拉框,以及输出转义。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

PHP 表单会提交回输出它的那个脚本:写一个带有命名输入框的 <form method="post">,当 $_SERVER['REQUEST_METHOD'] 为 'POST' 时,用 $_POST['name'] 读取每个字段。任何要输出回页面的内容都要用 htmlspecialchars() 转义。

运行这段代码,在表单里输入一个名字,然后点 Send:同一个脚本会再运行一次,这次 $_POST 里有了数据。

这里发生了三件事,每个表单处理程序都需要它们:

  • 检查请求方法。 第一次访问时请求是 GET,没有什么需要处理。?? 'GET' 只在从命令行运行文件时才有用,因为那时没有设置 REQUEST_METHOD;在 Web 服务器上它总是存在的。
  • 每个字段都加上 ?? ''。 字段可能不存在(输入框被改了名,或者请求是由脚本发出的),而读取不存在的键会输出警告。
  • 输出时转义。 名字被输出回页面两次,htmlspecialchars() 确保像 <b>Ada</b> 这样的值显示为文本,而不是变成 HTML 标记。详情见 htmlspecialchars 页面。

验证表单并在字段旁显示错误

把错误收集到一个以字段名为键的数组里。如果数组为空,表单就是有效的;否则重新输出表单,把每条信息显示在对应字段下方,并保留访问者已经填写的内容。先提交一个空表单,再用错误的邮箱提交,最后填入有效的值。

novalidate 关闭了浏览器自带的检查,这样你就能看到 PHP 的检查在起作用。真实的表单两者都要保留:浏览器验证只是方便访问者,PHP 验证才是真正算数的,因为任何人都可以不经过你的表单直接发送请求。filter_var() 各用一次调用就完成了邮箱验证和整数范围验证;它的其他过滤器见 filter_var 页面。

复选框、单选按钮和下拉框

文本框总会发送一个值,哪怕是空值。其他控件则不然:

  • 未勾选的复选框什么都不发送,所以 $_POST 中没有它的键。
  • 名为 topics[] 的复选框会以数组形式到达:$_POST['topics'] 是 ['php', 'sql']。
  • 单选按钮组或 <select> 发送所选选项的 value。

要在提交后保留选择,就在匹配的选项上输出 checked 或 selected。勾选几个复选框,选一个级别,然后点 Save。

读取输入后的这两项检查比看上去更重要。array_intersect() 会丢掉不在你列表里的主题;如果有人发送了你从未提供的值(或者一个数组,这会让 $levels[$level] 抛出错误),is_string() 加 array_key_exists() 的检查会把级别恢复为默认值。下拉框限制的是浏览器能发送什么,而不是请求里能包含什么。

用隐藏字段实现多步骤表单

隐藏输入框可以把一个值从这一步带到下一步,而不显示出来。这里第 1 步询问名字,第 2 步询问语言,并通过隐藏字段把名字一起发送,第 3 步把两者都显示出来。把三个步骤都走一遍。

隐藏字段在页面源代码中是可见的,也能像其他字段一样被修改,所以用它在步骤之间传递访问者自己的回答没有问题,但绝不能用来传递价格、用户 id 或角色。这些要保存在服务器端的会话里;见会话和 Cookie。

Post/Redirect/Get:防止刷新时重复提交

POST 成功后,如果脚本直接输出“感谢”页面,按下刷新时浏览器会再次发送这个 POST,订单或评论就会被保存两次。解决办法是先保存,再重定向,然后通过 GET 显示结果:

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $errors = validate($_POST);
    if (!$errors) {
        save_comment($_POST);
        $_SESSION['flash'] = 'Comment posted.';
        header('Location: /comments.php', true, 303);
        exit;
    }
    // On errors, fall through and print the form with the messages.
}

$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);

303 See Other 告诉浏览器用 GET 跟随重定向。header() 之后的 exit 很重要:没有它,脚本的其余部分会继续运行。重定向还必须在任何输出之前发生,否则 PHP 会拒绝执行并报 "headers already sent"。

真实的表单还会加上 CSRF 令牌:一个随机值,保存在会话和隐藏字段中,提交时用 hash_equals() 比较,这样其他网站就无法冒充已登录的访问者向你的表单提交数据。

不用浏览器测试验证逻辑

把验证规则放进一个函数,它接收一个数组并返回错误。页面用 $_POST 调用它,你也可以用任意数组调用它,这样各种边界情况都很容易尝试。把你自己的测试用例加到列表里。

第四个用例是大家最容易忽略的。请求可以发送 name[]=x,这会让 $_POST['name'] 变成数组,而把数组传给 trim() 会抛出 TypeError 并让脚本停止。先检查 is_string(),就能把恶意输入变成一个普通的验证错误。前面代码中的 (string) 转换也能避免 TypeError,但它会把数组变成单词 Array 并输出警告,所以 is_string() 是更干净的检查方式。

常见错误:在表单提交之前读取 $_POST

最常见的第一个表单脚本会在文件开头直接读取字段:

<?php
$name = $_POST['name'];
echo "Hello, $name";

第一次访问时还没有提交任何数据,所以 PHP 会输出:

Warning: Undefined array key "name" in /var/www/form.php on line 2

解决办法就是第一段代码中的模式:只在请求是 POST 时处理表单,并且读取每个字段时都给出默认值,$_POST['name'] ?? ''。如果警告在提交后出现,就对比 HTML 中的 name 属性和 PHP 中的键:name="Name" 和 $_POST['name'] 是两个不同的键。

常见问题

PHP 表单怎么提交到当前页面?

写 <form method="post">,不写 action(或写 action="")。浏览器会把数据 POST 回当前 URL,于是同一个脚本既显示表单又处理表单:检查 $_SERVER['REQUEST_METHOD'] === 'POST',再从 $_POST 读取字段。

PHP 表单提交后怎么保留填写的值?

把提交的值转义后输出回字段里:<input name="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">。对于复选框,在提交的值匹配时输出 checked;对于下拉框的选项,输出 selected。

PHP 中怎么获取多个复选框的值?

给这些复选框起同一个以方括号结尾的名字:name="topics[]"。PHP 会把勾选的值放进数组 $_POST['topics']。未勾选的复选框什么都不发送,所以要写 $_POST['topics'] ?? [],并把每个值与你提供的选项列表逐一比对。

为什么刷新页面会重复提交 PHP 表单?

上一个请求是 POST,所以浏览器在刷新时会重复发送它。提交成功后,用 header('Location: /thanks.php'); exit; 重定向(即 Post/Redirect/Get 模式)。这样刷新时重复的只是一个无害的 GET 请求。

PHP 中直接使用 $_POST 安全吗?

不安全。$_POST 中的每个值都来自访问者,可能是任何内容,包括 HTML、SQL,或者在你期望字符串的地方出现一个数组。输入时要验证(filter_var、长度检查和白名单检查),输出时要转义(HTML 用 htmlspecialchars,SQL 用预处理语句)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始