ThinkPHP 模型 `hidden` 字段我写了数组,API 却把密码原样吐了出去:原来是 `collection` 调用时我搞混了两种隐藏姿势
昨晚给移动端接口做数据脱敏,明明在模型里写了 `protected $hidden = ['password', 'salt']`,Postman 一测,用户列表里密码字段大摇大摆地躺在 JSON 里。我差点怀疑人生——这字段是叛变了还是 TP 不认账了?
折腾四十分钟,问题出在 查询结果集的处理方式 上。我以为是同一个东西,其实两种写法藏着完全不同的逻辑路径。
❌ 错误写法:先转数组,再套 collection
当时图省事,从别的接口复制了一段代码:
// 控制器里
$list = User::select()->toArray();
return json([
'code' => 200,
'data' => collection($list)->hidden(['password'])
]);
看起来没毛病对吧?`collection` helper 函数包一层,再调 `hidden`。但这里 `toArray()` 已经把模型实例变成了纯数组,模型的 `$hidden` 属性在转数组那一刻就已经失效了。后面的 `collection($list)` 其实创建的是 `think\Collection` 普通集合,不是模型集合,它的 `hidden` 方法参数只对该次调用生效,而且我手贱只写了 `['password']`,把 `salt` 漏了——更要命的是,这种写法完全绕过了模型本身的数据过滤机制。
结果:密码、盐值全暴露,字段白名单黑名单统统不生效。
✅ 正确写法:保持模型集合,让 `$hidden` 自己干活
改完后的版本:
// 控制器里
$list = User::select();
return json([
'code' => 200,
'data' => $list->hidden(['password', 'salt'])
]);
关键点:不要 `toArray()`。`User::select()` 返回的是 `think\model\Collection` 模型集合,它继承自普通 Collection 但重写了 `hidden` 方法。这时候调用 `hidden`,会先检查 模型自身定义的 `$hidden`,再合并你传入的参数,最终通过模型的 `toArray()` 输出过滤后的结果。
或者更懒一点,完全信任模型定义:
$list = User::select(); return json(['code' => 200, 'data' => $list]);
因为 JSON 序列化时会自动走模型的 `toArray()`,`$hidden` 和 `$visible` 都会生效。我之前是多此一举还搞砸了。
另一个隐蔽变体:分页场景的坑
如果你用分页,错误写法更容易出现:
// ❌ 错的:分页结果先拆再转 $page = User::paginate(10); $items = $page->items(); // 返回数组! return json(['data' => collection($items)->hidden(['password'])]);
`paginate()->items()` 返回的是 已经 toArray 过的数组,模型属性早没了。正确做法是直接操作分页对象,或者取原始模型集合:
// ✅ 对的 $page = User::paginate(10); $page->hidden(['password', 'salt']); // 模型集合支持链式调用 return json(['data' => $page]);
我总结的防翻车检查点
1. 看见 `toArray()` 就警惕:一旦调了,后面所有模型层面的过滤、追加、隐藏都失效,变成裸数组裸奔。
2. `collection()` vs 模型集合:用 `var_dump(get_class($list))` 确认类型,是 `think\model\Collection` 还是 `think\Collection`,两者 `hidden` 实现完全不同。
3. 分页别拆 `items()`:要隐藏字段就在分页对象上操作,或者自定义分页资源类,别半路把模型剥了皮。
4. 模型 `$hidden` 是底线,控制器参数是补丁:敏感字段统一写进模型,控制器临时 `hidden` 只补漏,别当主力用。
这次踩坑让我意识到,TP 里很多"看起来一样"的方法,差一个调用顺序就是安全和裸奔的区别。尤其是做 API 接口,数据出口一定要把好关,别学我,等 Postman 亮出密码那一刻才后背发凉。
你们有没有类似"写法差一行,结果差一界"的经历?欢迎丢出来一起复盘。

