PHP CRUD 白名单过滤空字符串,编辑时字段清空后自动复原
某后台管理系统上线了一个"编辑资料"功能。测试时发现一个奇怪现象:把某个可选字段(比如联系电话)清空然后点保存,接口返回成功,但刷新页面后旧值又回来了。没有任何错误提示,接口 200,前端也没报错。像是数据被一只无形的手悄悄写回去了。
现象
- 用户在编辑表单中删除联系电话字段的内容
- 提交表单,前端发送
{"id": 123, "phone": "", "name": "某某某"} - 接口响应
{"code": 0, "msg": "保存成功"} - 刷新页面,联系电话字段仍然显示旧值
"13800138000" - 数据库里这条记录的 phone 字段纹丝未动
根因
后台控制器里有一段白名单字段过滤逻辑,用来防止新建时把空字符串写进数据库(避免触发 NOT NULL 约束或覆盖数据库默认值):
foreach ($allowFields as $field) {
if (isset($params[$field]) && $params[$field] !== '') {
$data[$field] = $params[$field];
}
}这段逻辑在新建场景下是合理的:空字段跳过,让数据库 DEFAULT 生效。
问题在于编辑场景复用了同一段代码。用户提交 "phone": "",进入循环时 $params['phone'] !== '' 为 false,于是 phone 被静默丢弃,最终生成的 SQL 里根本没有 phone:
-- 期望:UPDATE records SET name='某某某', phone='' WHERE id=123
-- 实际:UPDATE records SET name='某某某' WHERE id=123phone 字段原值保持不变,表面上保存成功,实则数据完整性已被破坏。
isset($val) && $val !== '' 的语义是「跳过空值」,在新建时没问题(用数据库默认值)。但在编辑时,用户主动把字段清空同样会被跳过——后端看不到用户的「清空意图」,旧值永远不会被覆盖。
更隐蔽的是:接口正常返回 200,完全没有错误信号,排查难度极高。
正解
按操作类型分流,新建和编辑各自用不同的过滤策略:
$data = [];
foreach ($allowFields as $field) {
$camelField = lcfirst(str_replace('_', '', ucwords($field, '_')));
if ($id > 0) {
// 编辑:用 array_key_exists,只要字段在请求里就收录,空字符串也算
if (array_key_exists($field, $params)) {
$data[$field] = $params[$field];
} elseif (array_key_exists($camelField, $params)) {
$data[$field] = $params[$camelField];
}
} else {
// 新建:过滤空值,让数据库 DEFAULT 生效
if (isset($params[$field]) && $params[$field] !== '') {
$data[$field] = $params[$field];
} elseif (isset($params[$camelField]) && $params[$camelField] !== '') {
$data[$field] = $params[$camelField];
}
}
}关键差异在于用 array_key_exists() 替代 isset():
isset($params['phone'])—— 当值为null或键不存在时均返回falsearray_key_exists('phone', $params)—— 只要键存在就返回true,不管值是null还是""
编辑场景的语义是「用户提交了什么就写什么」,array_key_exists 才能正确捕获「清空」这个意图。
两种操作的语义不同:新建是「提供初始值」,编辑是「表达变更意图(包括清空)」。用同一套过滤逻辑服务两种语义,必然有一方会出错。分流之后,新建行为不变,编辑的清空操作得以正确落库。
连锁陷阱
如果该字段还会被同步到下游系统(如消息推送、第三方平台),下游若也有同款过滤:
// 下游同步代码——同样的坑
if ($phone !== '') {
$data['phone'] = $phone;
}即使主库修好了,清空操作也不会向下游传播。下游同步代码同样需要改成 array_key_exists 驱动的逻辑,确保「清空」这个事件能完整透传。
一句话外卖
新建用
isset()跳过空值是正确的,编辑用array_key_exists()保留空值也是正确的——两种语义需要两套策略,合并成一套必定踩坑。