$readonly 是 ThinkPHP 模型层字段写入过滤机制,仅在 save()/update() 组装数据时移除指定字段,非数据库级锁定;需严格匹配列名、避免主键误设,并配合验证器与数据库约束分层防护。

在 ThinkPHP 中,$readonly 是模型层最直接的字段写入防护手段,但它不是“锁”,而是“过滤”——只在 save() 和 update() 方法的数据组装阶段移除指定字段,不参与 SQL 执行或数据库约束。用对了省事,用错了形同虚设。
怎么正确声明只读字段
在模型类中定义 protected $readonly 静态数组,字段名必须与数据库列名完全一致(区分大小写、下划线风格):
- ✅ 正确:
protected $readonly = ['create_time', 'source', 'delete_time']; - ❌ 错误:
['createTime', 'SOURCE', 'deleted_at']—— 名字不匹配就失效 - 主键
id不建议加入$readonly,新增时框架自动忽略,加了反而可能干扰 UUID 或自增逻辑
哪些字段适合设为只读
核心原则:防业务误写,不是防所有写入。重点关注那些“写一次就固定”或“必须由系统控制”的字段:
-
必须设只读:创建时间(
create_time)、软删除标记(delete_time)、来源标识(source) -
谨慎设只读:关联 ID(如
user_id),若接口支持客服代操作或归属迁移,加只读会阻断正常流程 -
状态类字段(如
status)建议结合状态机校验,而非单纯靠$readonly;否则用户 POSTstatus=999可能绕过业务规则
为什么有时候 readonly 没效果
它只拦截模型实例的 save()/update(),以下情况完全绕过:
立即学习“PHP免费学习笔记(深入)”;
- 使用
Db::name('user')->update($data)等原生查询,不走模型逻辑 - 调用
$model->data($data, true)->save(),第二个参数true表示强制赋值,跳过所有过滤 - 子类继承父模型但未合并
$readonly,例如:protected $readonly = array_merge(parent::$readonly, ['pay_status']) - 字段被动态计算(如通过
$append或获取器),本身不在数据表中,设只读无意义
更可靠的只读组合策略
单靠 $readonly 属于应用层“礼貌性拒绝”,生产环境需分层加固:
-
验证器层早拦截:用
only(['name', 'email'])明确放行可改字段,比模型过滤更前置 -
数据库层兜底:MySQL 5.7+ 可设
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,或用GENERATED ALWAYS AS让字段不可更新 -
关键字段封装方法:如
changeStatus($new)内部做状态流转判断,不暴露原始字段写入入口 - 时间类字段如
update_time若设进$readonly,需在beforeUpdate钩子里手动赋值,否则不会自动更新



















