> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# The once() helper internals — Once, Onceable, and PreventsCircularRecursion

> A deep dive into the once() global helper, powered by Illuminate\Support\Once and Onceable, and how the same pattern powers Eloquent's PreventsCircularRecursion trait.

## 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.

```php theme={null}
function random(): int
{
    return once(function () {
        return random_int(1, 1000);
    });
}

random(); // 123
random(); // 123 (cached result)
random(); // 123 (cached result)
```

When called from a method on an object instance, the cache is scoped per instance.

```php theme={null}
class NumberService
{
    public function all(): array
    {
        return once(fn () => [1, 2, 3]);
    }
}

$service = new NumberService;

$service->all();
$service->all(); // (cached result)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (cached result, separate from $service)
```

<Info>
  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`.
</Info>

## The call in helpers.php

The `once()` function body in `Illuminate\Support\helpers.php` is quite small.

```php theme={null}
use Illuminate\Support\Once;
use Illuminate\Support\Onceable;

if (! function_exists('once')) {
    function once(callable $callback)
    {
        $onceable = Onceable::tryFromTrace(
            debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2),
            $callback,
        );

        return $onceable
            ? Once::instance()->value($onceable)
            : call_user_func($callback);
    }
}
```

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.

```mermaid theme={null}
flowchart TD
    A["Call once(callback)"] --> B["debug_backtrace() to find the caller"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"Hash computed?"}
    D -- No --> E["Run callback directly"]
    D -- Yes --> F["Once::instance()->value(onceable)"]
    F --> G{"Cached in the WeakMap?"}
    G -- Yes --> H["Return the cached value"]
    G -- No --> I["Run callback and store the result"]
```

## 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()`.

```php theme={null}
class Onceable
{
    public function __construct(
        public string $hash,
        public ?object $object,
        public $callable,
    ) {
        //
    }

    public static function tryFromTrace(array $trace, callable $callable)
    {
        if (! is_null($hash = static::hashFromTrace($trace, $callable))) {
            $object = static::objectFromTrace($trace);

            return new static($hash, $object, $callable);
        }
    }

    protected static function objectFromTrace(array $trace)
    {
        return $trace[1]['object'] ?? null;
    }

    protected static function hashFromTrace(array $trace, callable $callable)
    {
        if (str_contains($trace[0]['file'] ?? '', 'eval()\'d code')) {
            return null;
        }

        $uses = array_map(
            static function (mixed $argument) {
                if ($argument instanceof HasOnceHash) {
                    return $argument->onceHash();
                }

                if (is_object($argument)) {
                    return spl_object_id($argument);
                }

                return $argument;
            },
            $callable instanceof Closure
                ? (new ReflectionClosure($callable))->getClosureUsedVariables()
                : [],
        );

        $class = $callable instanceof Closure
            ? (new ReflectionClosure($callable))->getClosureCalledClass()?->getName()
            : null;

        $class ??= $trace[1]['class'] ?? null;

        return hash('xxh128', sprintf(
            '%s@%s%s:%s (%s)',
            $trace[0]['file'],
            $class ? $class.'@' : '',
            $trace[1]['function'],
            $trace[0]['line'],
            serialize($uses),
        ));
    }
}
```

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 `use`s differ**.

```php theme={null}
function greet(string $name)
{
    return once(fn () => "Hello, {$name}!");
}

greet('Alice'); // computes and caches "Hello, Alice!"
greet('Bob');   // different $name, different hash, recomputed
```

<Warning>
  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.
</Warning>

`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`.

```php theme={null}
class Once
{
    protected static ?self $instance = null;

    protected static bool $enabled = true;

    protected function __construct(protected WeakMap $values)
    {
        //
    }

    public static function instance()
    {
        return static::$instance ??= new static(new WeakMap);
    }

    public function value(Onceable $onceable)
    {
        if (! static::$enabled) {
            return call_user_func($onceable->callable);
        }

        $object = $onceable->object ?: $this;

        $hash = $onceable->hash;

        if (! isset($this->values[$object])) {
            $this->values[$object] = [];
        }

        if (array_key_exists($hash, $this->values[$object])) {
            return $this->values[$object][$hash];
        }

        return $this->values[$object][$hash] = call_user_func($onceable->callable);
    }

    public static function enable()
    {
        static::$enabled = true;
    }

    public static function disable()
    {
        static::$enabled = false;
    }

    public static function flush()
    {
        static::$instance = null;
    }
}
```

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.

<Info>
  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.
</Info>

### 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.

```php theme={null}
use Illuminate\Support\Once;

beforeEach(function () {
    Once::disable();
});

afterEach(function () {
    Once::enable();
    Once::flush();
});
```

`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.

```php theme={null}
trait PreventsCircularRecursion
{
    protected static $recursionCache;

    protected function withoutRecursion($callback, $default = null)
    {
        $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);

        $onceable = Onceable::tryFromTrace($trace, $callback);

        if (is_null($onceable)) {
            return call_user_func($callback);
        }

        $stack = static::getRecursiveCallStack($this);

        if (array_key_exists($onceable->hash, $stack)) {
            return is_callable($stack[$onceable->hash])
                ? static::setRecursiveCallValue($this, $onceable->hash, call_user_func($stack[$onceable->hash]))
                : $stack[$onceable->hash];
        }

        try {
            static::setRecursiveCallValue($this, $onceable->hash, $default);

            return call_user_func($onceable->callable);
        } finally {
            static::clearRecursiveCallValue($this, $onceable->hash);
        }
    }

    // getRecursiveCallStack() / getRecursionCache() / setRecursiveCallValue()
    // / clearRecursiveCallValue() are all WeakMap-based implementations
}
```

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.

```php theme={null}
use Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use PreventsCircularRecursion;

    public function getPathAttribute(): string
    {
        return $this->withoutRecursion(function () {
            // Any circular reference in this parent->path traversal
            // is caught here and the loop is broken.
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## 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

<Warning>
  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.
</Warning>

## Related pages

<Columns cols={2}>
  <Card title="tap() Helper and the Tappable Trait" icon="hand-point-right" href="/en/advanced/tap">
    Implementation patterns for the tap() helper, which lets you insert side effects while still returning the original value.
  </Card>

  <Card title="Eloquent Observers and Model Events" icon="eye" href="/en/advanced/eloquent-observers">
    Centralize Eloquent model events with Observer classes.
  </Card>
</Columns>


## Related topics

- [進階主題](/zh-TW/advanced/index.md)
- [Advanced Topics](/en/advanced/index.md)
- [tap() Helper and the Tappable Trait](/en/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Package auto-discovery internals](/en/advanced/package-discovery.md)
