thinkphp模型字段无法真正只读,但可通过getattr拦截读取、setattr拦截写入、cache::tag()精准失效缓存来模拟;关键在于绕过写入、拦截读取、依赖变更时主动清除关联缓存。

ThinkPHP 的模型字段无法真正“只读”,但可以通过 getAttr + setAttr 配合缓存策略模拟只读计算字段,并在依赖字段变更时主动失效缓存——关键不在“锁住写入”,而在“绕过写入、拦截读取、精准失效”。
用 getAttr 拦截读取,实现运行时计算字段
ThinkPHP 不提供原生只读字段,但模型的 getAttr 方法会在读取属性时被调用。只要不定义对应字段的数据库列,又在 getAttr 中返回计算结果,就能做到“看起来像字段,实际不存库”。
常见错误:直接在 getAttr 里查数据库或调用耗时方法,导致 N+1 查询或性能抖动。
- 依赖字段必须显式声明在
protected $append = ['xxx']中,否则不会触发getAttr - 计算逻辑应轻量,如
return $this->getAttribute('price') * $this->getAttribute('discount'),避免嵌套查询 - 若需缓存计算结果,用
think\Cache::tag()打标,例如Cache::tag('calc_'.$this->id)->get('total_price')
用 setAttr 拦截写入,拒绝非法赋值
setAttr 是写入前的最后一道闸口。对计算字段(如 total_price)重写该方法,可直接抛异常或静默忽略,阻止外部误设值。
使用场景:API 接收 JSON 数据时,前端可能传入 total_price 字段,需防止污染业务逻辑。
- 判断
$name === 'total_price'后,throw new Exception('total_price is read-only')最清晰 - 若选择静默忽略,务必加日志:
Log::warning('Ignored write to read-only field: total_price') - 注意:该拦截对
save(['total_price' => 100])有效,但对$model->total_price = 100; $model->save()也生效——因为两者最终都走setAttr
依赖字段变更时,用缓存标签自动失效关联计算缓存
计算字段的缓存必须和其依赖字段绑定。ThinkPHP 的缓存标签(Cache::tag())是唯一能批量清除的机制,不能靠 key 命名硬匹配。
容易踩的坑:在 afterWrite 钩子中只清当前模型缓存,却忘了关联的其他模型计算字段(比如商品变更后,订单列表页的摘要缓存也该失效)。
- 在模型的
afterWrite中,先获取本次修改的字段:$changed = $this->getChangedData() - 若
in_array('price', array_keys($changed)) || in_array('discount', array_keys($changed)),则执行Cache::tag('calc_'.$this->id)->clear() - 更健壮的做法:把所有依赖该模型的计算场景打同一组 tag,如
Cache::tag(['goods_'.$this->id, 'order_summary'])->set(...),失效时一次清空多个维度
为什么不用 __get/__set?
ThinkPHP 模型继承自 think\Model,已重写了魔术方法,直接定义 __get 会覆盖框架行为,导致 relation、attribute 等功能异常。官方明确不推荐。
真实风险:开启严格模式后,__get 可能绕过字段过滤、时间戳自动处理、软删除判断等核心逻辑。
- 坚持用
getAttr/setAttr,它们是 ThinkPHP 官方预留的扩展点,兼容性有保障 -
__get仅适合极简封装类,模型级业务必须走框架生命周期钩子 - 如果真需要动态字段映射,优先考虑
scope或自定义查询构建器,而非侵入属性访问层
最易被忽略的一点:缓存标签的生命周期必须和模型实例强绑定。用 $this->id 构造 tag 名时,要确保 id 在写入前已存在(即非新增未保存状态),否则 tag 为空,失效就失效了个寂寞。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










