真正锁死 Laravel 模型字段需重写 setAttribute() 拦截敏感字段并抛异常,因 $fillable/$guarded 仅控批量赋值、访问器不阻写入、hidden/setAppends 不影响可写性,forceFill() 可绕过需额外校验,boot() 中全局钩子可复用保护逻辑。

如何让 Laravel 模型字段真正不可写($fillable 不够用)
仅靠 $fillable 或 $guarded 无法阻止通过 fill()、update()、批量赋值等方式修改字段——它只控制“哪些字段允许被批量赋值”,不等于“禁止写入”。真正想锁死字段(比如 created_at、status_updated_by、自增 ID),得从模型行为层面拦截。
- 优先用
$casts+ 访问器/修改器组合,但注意:访问器(getXXXAttribute)只影响读,不影响写;修改器(setXXXAttribute)能劫持写入,但需手动 throw 异常才能阻断 - 更可靠的方式是在
setAttribute()中判断字段名并拒绝:重写模型方法,对敏感字段直接throw new RuntimeException('Field XXX is read-only') - 别忘了 Eloquent 的
forceFill()会绕过setAttribute(),如果业务中存在管理员强制覆盖场景,需额外在forceFill()前加校验或禁用该方法
Laravel 10+ 中用 setHidden() 和 setAppends() 不能解决只读问题
这两个方法只影响序列化(toArray()、toJson())结果,和属性可写性完全无关。有人误以为把字段设为 hidden 就“隐藏了修改入口”,其实 Model::create(['hidden_field' => 'xxx']) 照样生效——只要它在 $fillable 里或没被 $guarded 拦住。
-
setHidden()只是不让字段出现在 JSON 输出里,不影响数据库写入 - 想让字段“既不显示也不可写”,必须双管齐下:不在
$fillable列表中 + 重写setAttribute()拦截 - 注意迁移文件里定义的
->nullable()或->default()是数据库层约束,PHP 层仍可强行传值,不能替代模型逻辑校验
使用 boot() 静态钩子做全局只读字段保护
如果多个模型都要锁定相同字段(如 tenant_id、version),与其每个模型都重写 setAttribute(),不如在基类模型中统一处理。Laravel 的 boot() 是注册静态监听的最佳位置。
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
- 在基类
Model的boot()里调用static::updating()和static::creating(),检查$this->getDirty()是否包含只读字段 - 检测到非法修改时,用
$this->forceFill()恢复原始值(如果已加载),再throw异常,避免脏数据入库 - 注意:这个方式无法拦截
DB::table()->update()这类 Query Builder 操作,只对 Eloquent 实例有效
常见报错和调试线索:MassAssignmentException ≠ 字段只读
看到 MassAssignmentException 错误,第一反应不是“字段被锁了”,而是“它没进 $fillable,也没被 $guarded = [] 放行”。这个异常和只读逻辑无关,是 Laravel 最基础的批量赋值防护机制。
- 真实只读失败的表现是:无报错,但数据库字段没变(比如你设了修改器却忘了
return或throw) - 调试时先 dump
$model->getAttributes()和$model->getDirty(),确认字段是否进入 dirty list - 如果用了
protected $casts = ['status_updated_by' => 'integer'],但又在修改器里做了类型转换,可能因类型不匹配导致赋值静默失败
字段只读不是开关式配置,是模型行为干预。最易忽略的是:数据库触发器、视图字段、JSON 类型列的更新语义,和 PHP 层的“只读”预期常常错位——得按实际写入路径一一分辨。










