什么是 once()
once() 是一个执行回调,并在整个请求期间把结果缓存在内存中的全局辅助函数。当从同一调用位置以相同回调再次调用时,会返回已缓存的结果。
这种「每个调用位置、每个实例」独立缓存的机制,与
memoize 这类简单的记忆化辅助函数不同。要理解其原理,需要看 Illuminate\Support\Once 和 Illuminate\Support\Onceable 两个类。helpers.php 中的调用
Illuminate\Support\helpers.php 里 once() 函数本体非常简单。
once() 本身并不持有缓存,而是根据 debug_backtrace() 获取的调用方信息组装出 Onceable 实例,然后把处理委托给 Once 类。
Onceable — 用于唯一定位调用位置的哈希计算
Onceable 类负责根据调用栈计算出用于唯一定位「哪个调用位置」的哈希。
文件路径 + 类名 + 函数名 + 行号 + 闭包 use 引用变量的值。也就是说,即便从同一行调用 once(),只要 use 引用的变量值发生变化,就会被视为不同的缓存。
object 表示「调用方是否是实例方法」。$trace[1]['object'] 是 debug_backtrace() 上一层(调用 once() 的一方)的对象,所以在实例方法内就是 $this,在静态方法或全局函数中则为 null。
Once — 基于 WeakMap 的缓存本体
真正持有缓存的是Illuminate\Support\Once。
WeakMap。以 $onceable->object(调用方实例)作为键,值是「哈希 → 结果」的关联数组。当没有 object(全局函数或静态方法)时,键使用 Once 自身的 $this,实际上就形成了在整个进程内共享的单一缓存区域。
由于使用了
WeakMap,一旦作为键的对象在其他地方失去引用,它所关联的缓存也会成为垃圾回收对象。即便从大量对象调用 once(),也不容易造成内存泄漏。在测试中活用 — enable / disable / flush
调用Once::disable() 会完全禁用缓存,once() 每次都会执行回调。测试中「每次都想得到新值」时非常有用。
Once::flush() 会清空整个缓存,并在下次访问时新建 WeakMap。用于 Artisan 命令测试,或在 Octane 那样进程会被复用的环境中,避免缓存跨请求残留的场景。
PreventsCircularRecursion — Onceable 的应用示例
Onceable::tryFromTrace() 并非只服务于 once(),Eloquent 的 Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion trait 也复用了它。该 trait 用于「防止同一对象的同一调用位置在调用栈中重入」。
Once 的决定性区别在于:在 finally 块中调用完成后立即清空缓存。Once 的定位是「请求期间持续缓存」,而 PreventsCircularRecursion 则复用相同的 Onceable 机制,实现「仅在当前调用栈期间缓存(实际上是加锁)」的效果。
Eloquent 模型的典型用法,是防止模型的 toArray() 或访问器递归引用自身。
在包开发中的应用
在自定义包中想实现「同一实例的同一方法调用在一次请求中只执行一次」时,直接使用once() 是最简单的方式。虽然直接使用 Onceable/Once 的场景不多,但在下列场景中理解其内部实现会很有帮助。
- 在 Artisan 命令或测试中,想通过
Once::disable()/Once::flush()控制缓存行为时 - 在自定义 trait 中想实现「以调用位置为单位」的缓存或防重入时(可以复用
PreventsCircularRecursion的相同模式) - 当
once()的结果与预期不同、需要排查时,理解哈希输入要素(文件、类、函数、行号、use 变量)能更快定位原因
相关页面
tap() 辅助函数与 Tappable trait
在保持返回值不变的同时插入副作用的 tap() 辅助函数的实现模式。
Eloquent Observer 与模型事件
通过 Eloquent 模型的事件与 Observer 进行集中管理。