$readonly仅在模型save/update中过滤字段,不防原生SQL或强制赋值;关键字段如create_time、delete_time、source须设只读并配验证器+数据库约束,分层防护缺一不可。

ThinkPHP 的 $readonly 不是数据库锁,它只在模型的 save() 和 update() 中过滤字段——传了也进不了 SQL。真要防死,得靠验证器 + 数据库约束组合。
哪些字段必须设为只读
只读字段的核心作用不是防黑客,而是防止业务逻辑中意外覆盖关键值。设错会直接引发数据不一致。
-
create_time、update_time:除非明确支持时间回滚,否则必须只读;加了$readonly后update_time不会自动更新,需手动在beforeUpdate里赋值 -
delete_time:软删除标记,被 POST 覆盖会导致记录“假复活”或永久丢失 -
source(如'wechat'、'admin'):来源标识一旦写错,审计链就断了 - 别碰
id:主键在新增时框架自动忽略写入,加$readonly不但无效,还可能干扰 UUID 或自增逻辑
为什么 $readonly 有时完全没效果
它只拦截模型实例的 save() 和 update(),其他所有路径都绕过它。
- 用了
Db::name('user')->update($data):原生 SQL 完全不走模型层,$readonly形同虚设 - 调用
$model->data($data, true)->save():第二个参数true表示强制赋值,跳过所有过滤(包括$readonly、$fillable、验证器) - 字段名大小写或格式不一致:数据库列是
pay_status,但$readonly写成payStatus或PAY_STATUS,框架无法匹配 - 静态调用
User::update($data):不初始化模型实例,$readonly不触发
比 $readonly 更早、更稳的拦截点
$readonly 是应用层“礼貌性拒绝”,尤其当系统存在定时任务、DBA 直连、多语言客户端时,PHP 层拦不住。
立即学习“PHP免费学习笔记(深入)”;
- 验证器里用
only(['name', 'email'])明确放行可改字段,比$readonly更早拦截非法参数 - 数据库加
GENERATED ALWAYS AS(MySQL 5.7+)或默认值,让create_time根本无法被 INSERT/UPDATE 覆盖 - 对关键状态字段如
status,在验证规则里加rule('status', 'in:0,1,2'),防止用户 POSTstatus=999突破业务状态机 - 敏感字段(如余额、订单状态)建议不用
$readonly单独扛,而是封装专用方法,比如changeStatus($newStatus),内部做状态流转校验
真正容易被忽略的是分层控制
只读控制必须分层:模型层过滤、验证器校验、数据库约束三者缺一不可。单靠 $readonly 就上线,等于把门锁换成贴纸。



















