thinkphp 8.0中模型只读字段需通过静态$readonly属性严格声明,字段名须与数据库列名大小写完全一致;仅模型实例update()生效,静态调用、原生查询、强制赋值等均绕过该机制。

在ThinkPHP 8.0中为模型配置只读字段,是为了防止业务逻辑误写关键数据,比如创建时间被前端提交覆盖、软删除标记被意外清空——这一步必须在模型类中静态声明,且字段名大小写与数据库列名完全一致,否则$readonly形同虚设。
定义模型的$readonly静态属性
在模型类文件顶部的类定义内,添加protected static $readonly属性,值为包含字段名的数组。
字段名必须与数据库实际列名严格一致:例如表中是create_time、delete_time、source,则写成['create_time', 'delete_time', 'source'];若写成['createTime', 'deleted_at']或大小写混用,【框架完全无法匹配,该字段仍可被更新】。
主键id不建议加入$readonly:新增时框架自动忽略写入,加了反而可能干扰UUID生成或自增逻辑。
验证只读字段是否生效
方法一:使用模型实例调用update()并传入含只读字段的数据:
执行(new OrderModel())->update(['id' => 999, 'create_time' => '2020-01-01', 'status' => 'shipped']),观察生成的SQL语句——其中create_time字段不应出现在SET子句中。
方法二:检查日志或数据库实际变更结果:只读字段即使传入新值,数据库记录也不会改变,且无报错提示。
注意:若用UserModel::update($data)静态调用,【$readonly完全不触发,字段照常写入】,必须使用模型实例方式。
常见绕过场景与应对路径
① 原生查询绕过:Db::name('order')->update(['create_time' => '1970-01-01']) → 完全跳过模型层,$readonly无效。
② 强制赋值绕过:$model->data($data, true)->save() → 第二个参数true会跳过所有过滤,包括$readonly。
③ 字段名不匹配绕过:数据库列为pay_status,但$readonly写成['payStatus'] → 匹配失败,过滤失效。
④ 关联模型更新绕过:with('user')预载入后,$order->user->name = 'xxx'; $order->user->save() → 主模型的$readonly对关联模型无约束。
比$readonly更早、更稳的拦截点
验证器层用only(['name', 'email'])显式放行可改字段,非法字段在参数解析阶段就被剔除,不进入模型流程。
数据库层加GENERATED ALWAYS AS(MySQL 5.7+):让create_time字段根本无法被INSERT/UPDATE显式赋值,任何尝试都会报错。
关键状态字段如status,不要依赖$readonly,而应在beforeUpdate钩子中校验状态流转规则,例如从'delivered'不可回退到'shipped'。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











