$readonly仅在模型实例的save()和update()中生效,是应用层过滤器而非安全锁;须声明为protected static $readonly数组,字段名必须与数据库列名完全一致(全小写、下划线),不作用于insert()、静态调用、Db原生操作、强制赋值及关联模型。

$readonly 只在模型 save() 和 update() 里起作用,其他所有写入路径都绕过它——不是锁,是过滤器。
ThinkPHP8.0 中 $readonly 怎么写才生效
必须是模型类的 protected $readonly 静态数组,字段名要和数据库列名**完全一致**(小写、下划线,不能驼峰或大小写混用):
class OrderModel extends Model
{
protected $readonly = ['create_time', 'delete_time', 'source'];
}
- 只对实例方法
save()和update()生效;insert()不过滤,User::update($data)静态调用也不触发 - 字段名写错(比如写成
createTime或PAY_STATUS)→ 匹配失败,等于没设 - 子类不会继承父类的
$readonly,需显式合并:protected $readonly = array_merge(parent::$readonly, ['xxx']);
为什么写了 $readonly 还是被改了
不是框架失效,而是你没走模型层写入逻辑。常见绕过方式:
- 用了
Db::name('order')->update($data):原生 SQL 完全不进模型,$readonly形同虚设 - 调了
$model->data($data, true)->save():第二个参数true表示强制赋值,跳过所有过滤(包括$readonly、$fillable、验证器) - 关联模型单独保存:
$model->user->status = 1; $model->user->save();→ 主模型的$readonly对关联模型无效
只读字段该设哪些,哪些千万别碰
设错比不设更危险,核心原则是:防误写,不防恶意写(后者靠分层控制)。
立即学习“PHP免费学习笔记(深入)”;
-
必须设:
create_time、update_time(除非明确支持时间回滚)、delete_time(软删除标记)、source(来源标识)——写错直接导致审计断链或数据“假复活” -
谨慎设:
user_id等关联 ID —— 若接口本就支持客服代操作、归属切换,加了反而卡住业务 -
别碰:
id—— 主键在新增时框架自动忽略写入,加$readonly不但无效,还可能干扰 UUID 或自增逻辑
比 $readonly 更早、更稳的拦截点在哪
$readonly 是应用层“礼貌性过滤”,真要防住定时任务、DBA 直连、多语言客户端等多入口写入,得靠组合:
- 验证器里用
only(['name', 'email'])明确放行可改字段,比$readonly更早拦截非法参数 - 数据库加
GENERATED ALWAYS AS(MySQL 5.7+)或默认值 + 触发器,让create_time根本无法被 INSERT/UPDATE 覆盖 - 关键状态字段(如
status)别靠只读扛,封装专用方法changeStatus($newStatus),内部做状态流转校验
真正容易被忽略的是:$readonly 一旦设错(比如把 update_time 加进去又忘了在 beforeUpdate 里手动更新),它就真的“冻住”了——框架不报错、不提醒,数据静默失活。



















