Skip to main content

What is once()?

once() is a global helper that executes a callback and caches the result in memory for the duration of the request. When the same call site invokes the same callback again, once() returns the cached value.
When called from a method on an object instance, the cache is scoped per instance.
This “per call site, per instance” caching behavior is very different from a simple memoization helper. To understand how it works, you need to look at the two classes Illuminate\Support\Once and Illuminate\Support\Onceable.

The call in helpers.php

The once() function body in Illuminate\Support\helpers.php is quite small.
The important thing is that once() itself holds no cache: it builds an Onceable from the call site information returned by debug_backtrace() and delegates the actual work to the Once class.

Onceable — computing a hash that identifies the call site

The Onceable class is responsible for computing a hash from the backtrace that uniquely identifies “which call site” is invoking once().
The hash is derived from: file path + class name + function name + line number + values captured by the closure's use. In other words, even two once() calls on the same line become separate cache entries when the values the closure uses differ.
If once() is called from eval()ed code, hashFromTrace() returns null and no caching happens. This is a safety net for eval-based execution paths like compiled Blade views.
object indicates whether the caller was an instance method. $trace[1]['object'] is the object one level above once() in the backtrace (the caller), so it will be $this inside an instance method and null inside a static method or a global function.

Once — the WeakMap-backed cache

The actual cache is held by Illuminate\Support\Once.
The key technique is WeakMap. $onceable->object (the calling instance) is used as the map key, with a hash => result array as the value. If no object is available (global function or static method), the Once instance itself ($this) becomes the key, which effectively creates a single cache region shared across the entire process.
Because a WeakMap is used, once no other references to the key object remain, the associated cache entries become eligible for garbage collection. This makes once() safe to call from a large number of objects without leaking memory.

Using it in tests — enable / disable / flush

Calling Once::disable() turns caching off entirely, so once() invokes the callback every time. This is handy in tests when you want a fresh value on each call.
Once::flush() discards the entire cache and creates a new WeakMap on the next access. Use it in Artisan command tests, or in long-lived environments like Octane where processes are reused and you don’t want the cache to leak across requests.

PreventsCircularRecursion — Onceable in action

Onceable::tryFromTrace() isn’t exclusive to once(). Eloquent’s Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion trait reuses it to prevent “the same call site on the same object” from re-entering itself within a single call stack.
The critical difference from Once is the finally block, which clears the cache once the call completes. Once caches persistently for the duration of a request, while PreventsCircularRecursion uses the same Onceable infrastructure to cache (in effect, lock) only for the duration of the currently executing call stack. A typical use in an Eloquent model is preventing accessors or toArray() from recursively referencing themselves.

Applying it in package development

If you want “run this once per request per instance for the same method call” in your own package, using once() directly is by far the easiest option. You rarely need to touch Onceable/Once directly, but understanding the internals pays off in these situations:
  • When you want to control caching behavior from Artisan commands or tests via Once::disable() / Once::flush()
  • When you want to build your own trait that caches or prevents re-entry per call site (you can copy the PreventsCircularRecursion pattern)
  • When once() returns something unexpected and you need to debug why — knowing the hash inputs (file, class, function, line, and captured variables) makes it much easier to spot the cause
Because once() includes the closure’s captured variables in the hash, passing a closure that captures a different value on each iteration of a loop can silently produce a separate cache entry every time and effectively defeat caching. When calling once() inside a loop, confirm that the cache key granularity matches your intent.

tap() Helper and the Tappable Trait

Implementation patterns for the tap() helper, which lets you insert side effects while still returning the original value.

Eloquent Observers and Model Events

Centralize Eloquent model events with Observer classes.
Last modified on September 11, 2026