$readonly仅是模型层save()/update()前的过滤器,不防Db::update、force赋值、大小写不一致或关联修改;应只设create_time、update_time、delete_time、source等少数字段;user_id、id、status等不宜设;失效主因是绕过模型层、强制赋值或字段名不匹配;需结合验证器、数据库生成列和setAttr异常实现真正防护。

$readonly 不是数据库锁,只是 save()/update() 前的数据过滤器。它不会阻止 Db::name()->update()、force 强制赋值、字段名大小写不一致或关联模型修改——这些场景下 $readonly 完全不触发。
哪些字段该加 $readonly?别乱设 id 或 user_id
必须设只读的字段只有几个:create_time、update_time(除非你真要手动回滚)、delete_time(软删除标记)、source(如 'wechat')。这些字段一旦被意外覆盖,会导致时间错乱、软删除失效或来源不可追溯。
- 谨慎设只读:
user_id这类关联 ID —— 如果接口支持客服代操作、归属切换,加了反而卡住正常流程 - 别碰
id—— 主键在新增时框架自动忽略写入,加$readonly不但没用,还可能干扰自增或 UUID 生成逻辑 - 状态字段如
status别靠$readonly挡,应封装changeStatus($new)方法做状态机校验
$readonly 失效的三个高频原因
不是框架坏了,而是写法绕过了它:
- 用了
Db::name('order')->update():原生 SQL 完全不走模型层,$readonly形同虚设 - 调了
$model->data($data, true)->save():第二个参数true表示强制赋值,跳过所有过滤 - 字段名大小写或下划线不一致:数据库列是
pay_status,但$readonly写成payStatus或PAY_STATUS,框架根本匹配不上
真正防住的关键:分层控制,不能只靠 $readonly
$readonly 是应用层“礼貌性拒绝”,不是安全边界。多语言客户端、定时任务、DBA 直连、第三方工具都可能绕过 PHP 层。
立即学习“PHP免费学习笔记(深入)”;
- 验证器里用
only(['name', 'email'])明确放行字段,比$readonly更早拦截非法参数 - 数据库加
GENERATED ALWAYS AS (CURRENT_TIMESTAMP)(MySQL 5.7+),让create_time根本无法被 INSERT/UPDATE 覆盖 - 对关键字段,在关联模型的
setAttr方法里抛异常,例如protected function setNameAttr($value) { throw new Exception('name is readonly'); }
最常被忽略的是:update_time 加进 $readonly 后,它就真不会自动更新了——框架不会补、不会报错,数据悄悄“冻住”。改完记得检查时间戳是否还在动。



















