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

# once() 辅助函数的内部实现 — Once、Onceable 与 PreventsCircularRecursion

> 从 Illuminate\Support\Once 类与 Onceable 类的角度剖析全局函数 once() 的内部实现，并介绍其在 Eloquent 防循环引用 trait PreventsCircularRecursion 中的应用。

## 什么是 once()

`once()` 是一个执行回调，并在整个请求期间把结果缓存在内存中的全局辅助函数。当从同一调用位置以相同回调再次调用时，会返回已缓存的结果。

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

random(); // 123
random(); // 123 (缓存结果)
random(); // 123 (缓存结果)
```

从对象实例的方法内部调用时，缓存以该实例为单位相互独立。

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

$service = new NumberService;

$service->all();
$service->all(); // (缓存结果)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (缓存结果，与 $service 的缓存分离)
```

<Info>
  这种「每个调用位置、每个实例」独立缓存的机制，与 `memoize` 这类简单的记忆化辅助函数不同。要理解其原理，需要看 `Illuminate\Support\Once` 和 `Illuminate\Support\Onceable` 两个类。
</Info>

## helpers.php 中的调用

`Illuminate\Support\helpers.php` 里 `once()` 函数本体非常简单。

```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);
    }
}
```

关键点在于：`once()` 本身并不持有缓存，而是根据 `debug_backtrace()` 获取的调用方信息组装出 `Onceable` 实例，然后把处理委托给 `Once` 类。

```mermaid theme={null}
flowchart TD
    A["调用 once(callback)"] --> B["用 debug_backtrace() 取得调用方"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"能计算出哈希吗？"}
    D -- 否 --> E["直接执行 callback"]
    D -- 是 --> F["Once::instance()->value(onceable)"]
    F --> G{"WeakMap 已缓存？"}
    G -- 是 --> H["返回缓存值"]
    G -- 否 --> I["执行 callback 并存入缓存"]
```

## Onceable — 用于唯一定位调用位置的哈希计算

`Onceable` 类负责根据调用栈计算出用于唯一定位「哪个调用位置」的哈希。

```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),
        ));
    }
}
```

哈希的输入信息是 `文件路径 + 类名 + 函数名 + 行号 + 闭包 use 引用变量的值`。也就是说，**即便从同一行调用 `once()`，只要 `use` 引用的变量值发生变化，就会被视为不同的缓存**。

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

greet('Alice'); // 计算并缓存 "Hello, Alice!"
greet('Bob');   // $name 的值不同，哈希也不同，会重新计算
```

<Warning>
  在 `eval()` 出来的代码中调用 `once()` 时，`hashFromTrace()` 会返回 `null`，完全不进行缓存。这是为了对 Blade 编译后的视图等经由 eval 执行的场景做安全处理。
</Warning>

`object` 表示「调用方是否是实例方法」。`$trace[1]['object']` 是 `debug_backtrace()` 上一层（调用 `once()` 的一方）的对象，所以在实例方法内就是 `$this`，在静态方法或全局函数中则为 `null`。

## Once — 基于 WeakMap 的缓存本体

真正持有缓存的是 `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;
    }
}
```

关键技术是 `WeakMap`。以 `$onceable->object`（调用方实例）作为键，值是「哈希 → 结果」的关联数组。当没有 `object`（全局函数或静态方法）时，键使用 `Once` 自身的 `$this`，实际上就形成了在整个进程内共享的单一缓存区域。

<Info>
  由于使用了 `WeakMap`，一旦作为键的对象在其他地方失去引用，它所关联的缓存也会成为垃圾回收对象。即便从大量对象调用 `once()`，也不容易造成内存泄漏。
</Info>

### 在测试中活用 — enable / disable / flush

调用 `Once::disable()` 会完全禁用缓存，`once()` 每次都会执行回调。测试中「每次都想得到新值」时非常有用。

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

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

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

`Once::flush()` 会清空整个缓存，并在下次访问时新建 `WeakMap`。用于 Artisan 命令测试，或在 Octane 那样进程会被复用的环境中，避免缓存跨请求残留的场景。

## PreventsCircularRecursion — Onceable 的应用示例

`Onceable::tryFromTrace()` 并非只服务于 `once()`，Eloquent 的 `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` trait 也复用了它。该 trait 用于「防止同一对象的同一调用位置在调用栈中重入」。

```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() 均为基于 WeakMap 的实现
}
```

与 `Once` 的决定性区别在于：在 `finally` 块中**调用完成后立即清空缓存**。`Once` 的定位是「请求期间持续缓存」，而 `PreventsCircularRecursion` 则复用相同的 `Onceable` 机制，实现「仅在当前调用栈期间缓存（实际上是加锁）」的效果。

Eloquent 模型的典型用法，是防止模型的 `toArray()` 或访问器递归引用自身。

```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 () {
            // 在引用 parent 的 path 的处理中
            // 即使发生循环引用，这里也能阻断无限循环
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## 在包开发中的应用

在自定义包中想实现「同一实例的同一方法调用在一次请求中只执行一次」时，直接使用 `once()` 是最简单的方式。虽然直接使用 `Onceable`/`Once` 的场景不多，但在下列场景中理解其内部实现会很有帮助。

* 在 Artisan 命令或测试中，想通过 `Once::disable()` / `Once::flush()` 控制缓存行为时
* 在自定义 trait 中想实现「以调用位置为单位」的缓存或防重入时（可以复用 `PreventsCircularRecursion` 的相同模式）
* 当 `once()` 的结果与预期不同、需要排查时，理解哈希输入要素（文件、类、函数、行号、use 变量）能更快定位原因

<Warning>
  `once()` 会把闭包 `use` 引用的变量也纳入哈希，因此在循环中传入每次都捕获不同值的闭包时，可能会意外地在每次迭代得到不同的缓存键，实际上完全没起到缓存效果。在循环中使用时，请确认缓存粒度是否符合预期。
</Warning>

## 相关页面

<Columns cols={2}>
  <Card title="tap() 辅助函数与 Tappable trait" icon="hand-point-right" href="/zh-CN/advanced/tap">
    在保持返回值不变的同时插入副作用的 tap() 辅助函数的实现模式。
  </Card>

  <Card title="Eloquent Observer 与模型事件" icon="eye" href="/zh-CN/advanced/eloquent-observers">
    通过 Eloquent 模型的事件与 Observer 进行集中管理。
  </Card>
</Columns>


## Related topics

- [进阶主题](/zh-CN/advanced/index.md)
- [tap() 辅助函数与 Tappable trait](/zh-CN/advanced/tap.md)
- [進階主題](/zh-TW/advanced/index.md)
- [辅助函数](/zh-CN/helpers.md)
- [包自动发现的内部结构](/zh-CN/advanced/package-discovery.md)
