返回博客

PHP CRUD 白名单过滤空字符串,编辑时字段清空后自动复原

某后台管理系统上线了一个"编辑资料"功能。测试时发现一个奇怪现象:把某个可选字段(比如联系电话)清空然后点保存,接口返回成功,但刷新页面后旧值又回来了。没有任何错误提示,接口 200,前端也没报错。像是数据被一只无形的手悄悄写回去了。

现象

  1. 用户在编辑表单中删除联系电话字段的内容
  2. 提交表单,前端发送 {"id": 123, "phone": "", "name": "某某某"}
  3. 接口响应 {"code": 0, "msg": "保存成功"}
  4. 刷新页面,联系电话字段仍然显示旧值 "13800138000"
  5. 数据库里这条记录的 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=123

phone 字段原值保持不变,表面上保存成功,实则数据完整性已被破坏。

坑:isset() + !== '' 过滤在编辑场景会吞掉用户主动清空的字段

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 或键不存在时均返回 false
  • array_key_exists('phone', $params) —— 只要键存在就返回 true,不管值是 null 还是 ""

编辑场景的语义是「用户提交了什么就写什么」,array_key_exists 才能正确捕获「清空」这个意图。

正解:编辑用 array_key_exists,新建用 isset + !== ''

两种操作的语义不同:新建是「提供初始值」,编辑是「表达变更意图(包括清空)」。用同一套过滤逻辑服务两种语义,必然有一方会出错。分流之后,新建行为不变,编辑的清空操作得以正确落库。

连锁陷阱

如果该字段还会被同步到下游系统(如消息推送、第三方平台),下游若也有同款过滤:

// 下游同步代码——同样的坑
if ($phone !== '') {
    $data['phone'] = $phone;
}

即使主库修好了,清空操作也不会向下游传播。下游同步代码同样需要改成 array_key_exists 驱动的逻辑,确保「清空」这个事件能完整透传。

一句话外卖

新建用 isset() 跳过空值是正确的,编辑用 array_key_exists() 保留空值也是正确的——两种语义需要两套策略,合并成一套必定踩坑。