thinkphp中字段只读需在beforewrite钩子unset;虚拟字段靠getattr访问器+append声明+beforewrite过滤;缓存穿透须存{'__empty__':true}标记;空结果缓存与字段控制必须分层隔离,不可混写。

ThinkPHP 的模型字段只读、虚拟字段、缓存穿透防护、空结果缓存——这四个需求不能靠单一配置或一个钩子解决,必须分层处理:字段控制在模型层,缓存逻辑在查询层,穿透防护在缓存层,空值策略在业务判断层。
怎么让模型字段真正只读(不被 save() 修改)
ThinkPHP 没有 readonly 字段属性,protected $readonly 只影响 create()(已废弃),对 save() 无效。实际生效的方式是拦截写入逻辑:
- 重写模型的
data()方法,在设置数据时过滤掉只读字段(如created_at、version) - 更稳妥的是在
beforeWrite钩子中 unset:protected static function boot() { parent::boot(); static::beforeWrite(function ($model) { unset($model['updated_at'], $model['status_lock']); }); } - 注意:不要依赖数据库字段的
DEFAULT或GENERATED,TP 不会自动跳过这些字段的写入
怎么定义和使用虚拟字段(不映射到数据库)
虚拟字段不是靠 $schema 或 $type 声明的,而是靠手动注入 + 屏蔽持久化:
- 在模型中定义访问器(
getXXXAttr)并返回计算值,例如:getFullnameAttr拼接first_name和last_name - 必须同时在
$append中声明,否则toArray()不包含该字段 - 关键:在
beforeWrite钩子里unset对应的虚拟字段名(如fullname),否则它会被当成真实字段塞进 SQL - 若用
withAttr定义转换,它只是输出格式化,不解决“是否写入”问题
缓存穿透防护:空结果到底该不该存?怎么存?
TP 自带的 cache() 方法默认不缓存 null 或空数组,直接导致穿透。必须显式控制:
- 查不到时,用
Cache::set($key, ['__empty__' => true], $ttl)存一个标记,避免反复查库 - 查询逻辑要统一包装:先
Cache::get($key),命中后检查是否含__empty__键,是则直接返回null - 不要用
0、false、''作空标记——它们可能是合法业务值;用结构化标记(如数组键)才可靠 - TP6.1+ 的
think-orm支持withCache()的empty参数,但仅限于主键查询,关联查询仍需手写
为什么「缓存穿透防护」和「字段只读」容易串在一起出错
很多人把字段控制和缓存逻辑混在同一个方法里,比如在 getById() 中既做 unset 又做 Cache::get/set,结果出现两个典型问题:
- 缓存中存了带虚拟字段的对象,下次取出时字段还在,但数据库其实没这列——因为没在
beforeWrite清理,导致误更新 - 空结果缓存用了
Cache::remember(),但它内部对null直接跳过存储,你以为防穿透了,其实没生效 - 最隐蔽的坑:模型开启
auto_write_timestamp后,save()会强制写入update_time,哪怕你前面 unset 过——得关掉或改用updateTime钩子手动控制
字段逻辑和缓存逻辑必须隔离:字段由模型生命周期钩子管,缓存由服务类或 Repository 封装,别让一个方法承担两层职责。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










