$readonly仅在模型save/update中过滤字段,非数据库锁;需设只读的有create_time、update_time、source、delete_time;id不可设;子类不继承;失效场景包括原生sql、强制赋值、大小写不一致;可靠控制需验证器+数据库约束分层实现。

ThinkPHP 的 $readonly 不是数据库锁,它只在模型的 save() 和 update() 调用中过滤字段——传了也进不了 SQL。 真要防住所有写入路径,单靠它上线等于没设防。
哪些字段该加 $readonly?别乱动 id
必须设只读的字段有:create_time、update_time(除非你明确要回滚时间)、source、delete_time。这些字段一旦被意外覆盖,会导致软删除失效、时间错乱或来源不可追溯。
谨慎设只读的是关联 ID,比如 user_id:如果接口支持客服代操作或归属转移,加了 $readonly 反而卡住正常流程。
千万别给 id 加 $readonly:
- 主键在新增时框架自动忽略写入,加了白费劲
- 可能干扰自增逻辑或 UUID 生成
- TP 不会继承父类的
$readonly,子类得自己显式合并
$readonly 失效的三种典型场景
明明写了 protected $readonly = ['status'],日志里还是看到 status 被改了——不是框架坏了,而是你绕过了模型层:
- 用了
Db::name('order')->update(...):原生 SQL 完全不走模型,$readonly形同虚设 - 调了
$model->data($data, true)->save():第二个参数true表示强制赋值,跳过所有过滤 - 字段名大小写或命名风格不一致:数据库列是
pay_status,但$readonly写成payStatus或PAY_STATUS,框架认不出来
比 $readonly 更早、更稳的拦截点
$readonly 是应用层“礼貌性拒绝”,尤其当系统存在定时任务、DBA 直连、多语言客户端时,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 就上线,等于把门锁换成贴纸。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











