最安全的字段控制方式是用 protected $visible 声明白名单,但仅限静态场景;动态场景必须用 visible() 实例方法,禁用 $hidden 黑名单。

直接结论:用 $visible 声明白名单最安全,但必须是 protected 且字段名与数据库列名完全一致;动态场景一律改用 visible() 实例方法,别碰 $hidden。
为什么 $visible 不生效?检查这三点
最常见的失效不是代码写错,而是 PHP 层级或 ThinkPHP 行为理解偏差:
-
$visible必须声明为protected,写成public $visible = [...]或漏掉修饰符,直接被忽略 - 字段名必须和数据库列名**完全一致**(大小写、下划线),比如数据库是
user_name,就不能写成userName(除非你关了下划线转驼峰) -
$visible只在toArray()、toJson()、toXml()这些导出方法里起作用,它不干预 SQL 查询,也不影响getAttr访问器返回的字段
visible() 动态调用比 $visible 更实用
硬编码 $visible 在模型里,等于把接口契约写死——用户列表要 4 个字段,详情页要 8 个,你就得复制模型或加一堆 if。用 visible() 才是正解:
- 调用顺序必须是
$model->visible(['id', 'title'])->toJson(),写成$model->toJson()->visible()没用(toJson()返回的是字符串) -
visible([])表示“一个字段都不返回”,不是“恢复全部”;想恢复全部,直接别调用visible() - 关联模型字段不受父模型
visible()影响,比如with('profile')后想限制profile.nickname,得单独对关联模型调用visible()或在关联定义里设$visible - 如果用了
append()追加字段,这些字段默认参与$visible控制;但访问器(如getFullnameAttr)默认不参与,得手动在访问器里加判断或改用append()
敏感字段别依赖 $hidden,它根本不可靠
$hidden 是黑名单思维,一漏就泄露;更糟的是,它无法动态修改——你在控制器里写 $model->hidden = ['password'] 完全无效,因为 $hidden 是类属性,PHP 解析时就固化了。
立即学习“PHP免费学习笔记(深入)”;
- 真正能动态生效的只有实例方法:
makeHidden(['password'])(追加隐藏)或setHidden(['password'])(覆盖隐藏) - 但要注意:
makeHidden()对集合无效,$users->makeHidden('password')不会批量生效,得用$users->each->makeHidden('password')或更高效的$users->setHidden(['password']) - 如果最终输出走的是
json_encode($model->attributes)或$model->getAttributes(),$hidden和所有实例方法都彻底失效——因为绕过了模型的序列化逻辑 - 最稳的方案是查数据时就砍字段:
UserModel::field(['id', 'name', 'status'])->select(),既省带宽又防泄露
权限驱动的字段控制必须放在响应层
字段可见性由当前用户角色决定?那绝对不能塞进模型定义里。下面这些写法全是错的:
-
protected $hidden = [auth()->user()->can('view_phone') ? 'phone' : ''];→ PHP 报错,属性必须是常量表达式 - 在中间件里改模型类的
$hidden→ 全局污染,缓存失效,测试崩溃 - 在控制器里先查再过滤数组,但最后却调用
$model->toJson()→ 过滤白做了,toJson()读的是原始模型状态
正确做法:查出完整数据 → 调用一个字段过滤函数(如 filterFields($data, $allowedFields))→ 得到精简数组 → 再 json($filteredArray)。白名单配置建议存在 Redis,避免每次请求查权限表。



















