动态控制字段暴露应使用运行时方法而非静态 $hidden/$visible:用 makeHidden()/makeVisible() 局部覆盖、unset() 操作实例属性、资源类按角色过滤字段,并统一 toArray() 与 jsonSerialize() 行为。

用 $hidden 和 $visible 静态控制字段暴露不灵活?
静态定义在模型里确实简单,但一换场景就得改代码、重部署。比如 API 返回给管理员要全字段,给普通用户得砍掉 password_hash 和 is_deleted;或者导出 Excel 时临时加个计算字段。硬编码 $hidden 列表会卡死这种需求。
真正能 runtime 控制的,是靠模型实例上的动态属性操作:
-
unset($model->password_hash)只影响当前实例,序列化时自动过滤 -
unset($model->attributes['password_hash'])更底层,连toArray()都不带它 - 想临时加字段?直接
$model->append('full_name'),前提是这个 accessor 已定义
用 makeHidden() / makeVisible() 方法做局部覆盖
这两个方法是 Laravel 5.5+ 提供的“安全覆盖”方式:只对当前模型实例生效,不影响其他地方或后续查询。比直接改 $hidden 数组更可控。
常见错误是链式调用后忘了赋值或没触发序列化:
- 写法必须是
$user->makeHidden(['api_token', 'remember_token'])->toArray(),单独调用makeHidden()不改变原始对象状态 - 如果用在 Eloquent Collection 上,得配合
map():$users->map(fn($u) => $u->makeHidden(['email'])) - 注意和
setVisible()的区别:makeVisible()是“白名单模式”,其余字段默认隐藏;makeHidden()是“黑名单模式”,其余字段照常输出
API 响应中按角色动态过滤字段,别在控制器里硬写 if-else
控制器里一堆 if ($user->is_admin) { ... } 很难维护,也容易漏掉嵌套关系(比如 $user->posts 的字段也要过滤)。
推荐方案是封装成资源类(ApiResource),并在 toArray() 里做判断:
public function toArray($request)
{
$base = [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
if ($request->user()?->can('view_sensitive_data')) {
$base['last_login_at'] = $this->last_login_at;
$base['ip_address'] = $this->ip_address;
}
return $base;
}
这样字段逻辑收口在一处,关联资源也能复用同一套规则。别试图在模型里用 $request —— 模型不该感知 HTTP 层。
小心 toArray() 和 jsonSerialize() 的行为差异
前端调 response()->json($model) 时走的是 jsonSerialize(),而手动调 $model->toArray() 是另一套逻辑。两者默认都尊重 $hidden,但自定义 jsonSerialize() 会完全绕过它。
容易踩的坑:
- 重写了
jsonSerialize()却没调$this->toArray(),导致$hidden失效 - 用了
makeHidden()后仍返回全量字段 —— 因为jsonSerialize()没转发这个状态 - 想兼容两者?统一在
toArray()里处理,然后让jsonSerialize()直接 return $this->toArray()
动态可见性本质是数据投影问题,不是模型本身该管的事。越早把字段裁剪移到响应层(Resource 或 Transformer),后期加权限、多端适配就越不容易崩。


















