ThinkPHP模型中通过静态属性$readonly声明只读字段数组,仅在save()和update()方法中生效,于数据组装阶段过滤字段,优先级高于$fillable/$guarded,但不作用于原生SQL、批量更新等场景。

ThinkPHP 模型里怎么设只读字段(禁止更新)
直接在模型类里用 $readonly 属性声明字段名数组,TP 就会在 save()、update() 时自动忽略这些字段。它不拦截写入逻辑,而是在数据组装阶段过滤掉——所以不是“数据库层面锁死”,而是框架层的写入屏蔽。
-
$readonly是模型静态属性,必须是数组,元素为字符串字段名,比如['id', 'create_time'] - 只对模型的
save()和update()方法生效;手动写 SQL 或用Db::table()->update()完全绕过它 - 字段名必须和数据库列名完全一致(区分大小写,取决于数据库配置),别写成驼峰或下划线混用
- 如果同时用了
$fillable或$guarded,$readonly的过滤发生在它们之后,优先级更高
为什么 $readonly 有时没效果?常见失效场景
最常踩的坑是误以为它能阻止所有更新路径。实际上它只作用于模型实例的写操作,对原生查询、批量更新、关联保存等都不起作用。
- 调用
UserModel::where('id', 1)->update(['status' => 2]):绕过模型实例,$readonly完全不触发 - 用
saveAll()批量保存多个模型:每个实例单独走$readonly过滤,但若传入的是关联数组而非对象,可能跳过模型校验 - 字段在
$append或获取器(getXXXAttr)里被动态计算:这些字段本身就不在数据表中,设进$readonly没意义,也不会报错 - 模型继承了父类,父类定义了
$readonly,子类没合并而是直接覆盖:子类需显式合并,如protected $readonly = array_merge(parent::$readonly, ['xxx'])
想更严格地禁字段更新?得配合其他机制
$readonly 是轻量级防护,真要防住所有写入入口,得加一层校验或改用事务控制。尤其在 API 接口或后台管理这类高风险场景。
- 在模型的
beforeUpdate钩子中手动检查:if (isset($data['id']) && $data['id'] !== $this->id) { throw new Exception('id 不可修改'); } - 数据库层面加 CHECK 约束(如 PostgreSQL 的
CHECK (id = OLD.id)),但 MySQL 对 UPDATE 的 CHECK 支持有限且不报错 - 敏感字段(如余额、状态机关键字段)建议不用
$readonly单独扛,而是封装专用方法,比如changeStatus($newStatus),内部做状态流转校验 - 注意时间字段:如果
update_time也放进$readonly,那它不会自动更新——除非你额外在beforeUpdate里手动赋值
TP6 和 TP8 在只读字段处理上的差异
TP6 开始 $readonly 成为标准模型属性,TP8 没改动,但要注意底层行为一致性。
立即学习“PHP免费学习笔记(深入)”;
- TP6.0+ 所有模型基类(
think\Model)默认支持$readonly;TP5.1 需要自己在基类里加兼容逻辑 - TP8 中
$readonly依然只过滤data数组,不干涉force强制写入(即save(['id'=>999], true)仍会写入) - TP8 的验证规则(
validate)和$readonly无耦合,不能靠验证规则替代字段只读控制 - 如果项目混合使用模型和查询构造器,别指望
$readonly能全局生效——它只活在模型实例的生命期内
$readonly 很容易成为幻觉里的保险丝。


















