PHP 表单会提交回输出它的那个脚本:写一个带有命名输入框的 <form method="post">,当 $_SERVER['REQUEST_METHOD'] 为 'POST' 时,用 $_POST['name'] 读取每个字段。任何要输出回页面的内容都要用 htmlspecialchars() 转义。
运行这段代码,在表单里输入一个名字,然后点 Send:同一个脚本会再运行一次,这次 $_POST 里有了数据。
<?php
$name = '';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$name = trim($_POST['name'] ?? '');
}
?>
<form method="post">
<label>Your name <input name="name" value="<?= htmlspecialchars($name) ?>"></label>
<button>Send</button>
</form>
<?php if ($name !== ''): ?>
<p>Hello, <?= htmlspecialchars($name) ?>.</p>
<?php endif; ?>Master PHP the hands-on way
Interactive lessons, real projects, and instant feedback.
START NOW
这里发生了三件事,每个表单处理程序都需要它们:
检查请求方法。 第一次访问时请求是 GET,没有什么需要处理。?? 'GET' 只在从命令行运行文件时才有用,因为那时没有设置 REQUEST_METHOD;在 Web 服务器上它总是存在的。
每个字段都加上 ?? ''。 字段可能不存在(输入框被改了名,或者请求是由脚本发出的),而读取不存在的键会输出警告。
输出时转义。 名字被输出回页面两次,htmlspecialchars() 确保像 <b>Ada</b> 这样的值显示为文本,而不是变成 HTML 标记。详情见 htmlspecialchars 页面 。
验证表单并在字段旁显示错误
把错误收集到一个以字段名为键的数组里。如果数组为空,表单就是有效的;否则重新输出表单,把每条信息显示在对应字段下方,并保留访问者已经填写的内容。先提交一个空表单,再用错误的邮箱提交,最后填入有效的值。
<?php
$values = ['name' => '', 'email' => '', 'age' => ''];
$errors = [];
$sent = false;
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
foreach ($values as $field => $_) {
$values[$field] = trim((string) ($_POST[$field] ?? ''));
}
if ($values['name'] === '') {
$errors['name'] = 'Please enter your name.';
} elseif (mb_strlen($values['name']) > 50) {
$errors['name'] = 'Use 50 characters or fewer.';
}
if (filter_var($values['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'That does not look like an email address.';
}
$age = filter_var($values['age'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 13, 'max_range' => 120]]);
if ($age === false) {
$errors['age'] = 'Enter a whole number from 13 to 120.';
}
$sent = $errors === [];
}
function e(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<style>.err { color: #c62828; margin: 2px 0 10px; } label { display: block; margin-top: 8px; }</style>
<?php if ($sent): ?>
<p>Thanks, <?= e($values['name']) ?>. We will write to <?= e($values['email']) ?>.</p>
<?php else: ?>
<form method="post" novalidate>
<label>Name <input name="name" value="<?= e($values['name']) ?>"></label>
<?php if (isset($errors['name'])): ?><p class="err"><?= e($errors['name']) ?></p><?php endif; ?>
<label>Email <input name="email" value="<?= e($values['email']) ?>"></label>
<?php if (isset($errors['email'])): ?><p class="err"><?= e($errors['email']) ?></p><?php endif; ?>
<label>Age <input name="age" value="<?= e($values['age']) ?>"></label>
<?php if (isset($errors['age'])): ?><p class="err"><?= e($errors['age']) ?></p><?php endif; ?>
<button>Sign up</button>
</form>
<?php endif; ?>
novalidate 关闭了浏览器自带的检查,这样你就能看到 PHP 的检查在起作用。真实的表单两者都要保留:浏览器验证只是方便访问者,PHP 验证才是真正算数的,因为任何人都可以不经过你的表单直接发送请求。filter_var() 各用一次调用就完成了邮箱验证和整数范围验证;它的其他过滤器见 filter_var 页面 。
复选框、单选按钮和下拉框
文本框总会发送一个值,哪怕是空值。其他控件则不然:
未勾选的复选框什么都不发送,所以 $_POST 中没有它的键。
名为 topics[] 的复选框会以数组形式到达:$_POST['topics'] 是 ['php', 'sql']。
单选按钮组或 <select> 发送所选选项的 value。
要在提交后保留选择,就在匹配的选项上输出 checked 或 selected。勾选几个复选框,选一个级别,然后点 Save。
<?php
$topics = ['php' => 'PHP', 'sql' => 'SQL', 'js' => 'JavaScript'];
$levels = ['new' => 'New to coding', 'some' => 'Some experience', 'pro' => 'Professional'];
$picked = $_POST['topics'] ?? [];
$picked = is_array($picked) ? array_values(array_intersect($picked, array_keys($topics))) : [];
$level = $_POST['level'] ?? 'new';
if (!is_string($level) || !array_key_exists($level, $levels)) {
$level = 'new';
}
?>
<form method="post">
<p>Topics:
<?php foreach ($topics as $key => $label): ?>
<label><input type="checkbox" name="topics[]" value="<?= $key ?>"
<?= in_array($key, $picked, true) ? 'checked' : '' ?>> <?= $label ?></label>
<?php endforeach; ?>
</p>
<p>Level:
<select name="level">
<?php foreach ($levels as $key => $label): ?>
<option value="<?= $key ?>" <?= $key === $level ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</p>
<button>Save</button>
</form>
<p>You picked: <?= $picked ? implode(', ', array_map(fn ($k) => $topics[$k], $picked)) : 'nothing yet' ?>
(<?= $levels[$level] ?>)</p>
读取输入后的这两项检查比看上去更重要。array_intersect() 会丢掉不在你列表里的主题;如果有人发送了你从未提供的值(或者一个数组,这会让 $levels[$level] 抛出错误),is_string() 加 array_key_exists() 的检查会把级别恢复为默认值。下拉框限制的是浏览器能发送什么,而不是请求里能包含什么。
用隐藏字段实现多步骤表单
隐藏输入框可以把一个值从这一步带到下一步,而不显示出来。这里第 1 步询问名字,第 2 步询问语言,并通过隐藏字段把名字一起发送,第 3 步把两者都显示出来。把三个步骤都走一遍。
<?php
$name = trim((string) ($_POST['name'] ?? ''));
$lang = (string) ($_POST['lang'] ?? '');
$step = $name === '' ? 1 : ($lang === '' ? 2 : 3);
$e = fn (string $s) => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
?>
<p>Step <?= $step ?> of 3</p>
<?php if ($step === 1): ?>
<form method="post">
<label>Name <input name="name"></label> <button>Next</button>
</form>
<?php elseif ($step === 2): ?>
<form method="post">
<input type="hidden" name="name" value="<?= $e($name) ?>">
<p>Hi <?= $e($name) ?>, which language do you want to learn?</p>
<label><input type="radio" name="lang" value="PHP" checked> PHP</label>
<label><input type="radio" name="lang" value="Python"> Python</label>
<button>Next</button>
</form>
<?php else: ?>
<p><?= $e($name) ?> wants to learn <?= $e($lang) ?>.</p>
<?php endif; ?>
隐藏字段在页面源代码中是可见的,也能像其他字段一样被修改,所以用它在步骤之间传递访问者自己的回答没有问题,但绝不能用来传递价格、用户 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 调用它,你也可以用任意数组调用它,这样各种边界情况都很容易尝试。把你自己的测试用例加到列表里。
<?php
function validateSignup(array $input): array
{
$errors = [];
$name = $input['name'] ?? '';
$email = $input['email'] ?? '';
if (!is_string($name) || trim($name) === '') {
$errors['name'] = 'required';
}
if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors['email'] = 'invalid';
}
return $errors;
}
$cases = [
['name' => 'Ada', 'email' => 'ada@example.com'],
['name' => ' ', 'email' => 'ada@example.com'],
['name' => 'Ada', 'email' => 'ada@'],
['name' => ['x'], 'email' => 'ada@example.com'],
[],
];
foreach ($cases as $case) {
echo json_encode($case), ' => ', json_encode(validateSignup($case)), "\n";
}
第四个用例是大家最容易忽略的。请求可以发送 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'] 是两个不同的键。