用获取器(getxxxattr)+ hidden 隐藏字段实现只读计算值,不入库、不可写、可缓存;缓存 key 应含模型类名、主键、字段名及数据版本号,由 afterupdate 钩子维护版本。

怎么让模型字段变成只读计算值(不存库,但能像普通字段一样取)
ThinkPHP 的模型字段默认映射数据库列,想加一个「运行时计算、不入库、只读」的字段,不能直接写 $model->xxx = ...,否则下次 save() 会试图写入空值或报错。核心是用「获取器(accessor)」+「隐藏字段」组合。
- 在模型里定义
getXXXAttr()方法(比如字段叫full_name,就写getFullNameAttr()),方法返回计算结果 - 把该字段名加入模型的
protected $hidden = ['full_name'],避免序列化时意外透出或干扰写入 - 确保没在数据库表里建同名字段——否则 ThinkPHP 会优先读库值,覆盖你的计算逻辑
- 调用时直接
$model->full_name即可,框架自动触发获取器,且不会出现在data数组里参与save()
为什么用 getAttr 而不是 setAttr 或属性赋值
setAttr 是为入库前修改值准备的,它发生在 save() 流程中;而只读计算字段根本不需要入库,也不该被 save() 捕获。手动赋值(如 $model->full_name = 'xxx')会导致该字段进入模型的 $data,后续 save() 尝试写入 null 或触发类型转换错误。
- 只读字段必须「不可写」:不定义
setXXXAttr(),也不在$fillable或$allowField中包含它 - 获取器函数内可安全调用其他模型方法、缓存、甚至简单 SQL 查询,但要注意别引入 N+1
- 如果计算逻辑耗时(比如拼接用户头像 URL + 域名配置),建议在获取器里加
Cache::get(),键用模型主键 + 字段名组合
缓存计算结果时,key 怎么设计才不踩坑
缓存只读字段的核心矛盾是:既要复用,又不能跨模型实例污染。直接用固定 key(如 'user_fullname_123')看似简单,但模型更新后缓存不会自动失效——除非你手动 Cache::delete(),而这容易漏掉。
- 推荐用「模型类名 + 主键 + 字段名 + 数据版本号」作为 key,例如:
'app\model\User_123_full_name_v2' - 版本号存在哪?放在模型的
$cacheVersion静态属性里,每次模型关键字段(如nickname,realname)变更时,由afterUpdate钩子统一自增并刷新所有相关缓存 - 避免用
md5(serialize($this->data))做版本——序列化含时间戳、对象引用等不稳定内容,key 会频繁变,缓存形同虚设 - 不要在获取器里用
Cache::rememberForever():永久缓存无法响应数据库直连更新(比如后台 SQL 修改),至少设个 1 小时过期
为什么不用原生 PHP 属性或 __get() 代理
有人图省事在模型里加 public 属性或重写 __get(),这会破坏 ThinkPHP 的数据隔离机制:字段不再受 visible/hidden 控制,toArray() 时无法过滤,JSON 输出可能泄露敏感计算逻辑,且和验证器、自动完成等生命周期钩子脱节。
- ThinkPHP 的获取器是唯一被框架原生支持的「虚拟字段」方案,兼容
with()关联预载、append()显式追加、以及 API 返回时的字段白名单控制 - 若需动态决定是否启用某计算字段(比如按用户权限),可用
$model->append(['full_name'])按需加载,而不是全局开启 - 调试时注意:
dump($model)不会显示获取器结果,得用echo $model->full_name或$model->toArray()才能看到
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











