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

> グローバル関数once()の内部実装をIlluminate\Support\OnceクラスとOnceableクラスから解説し、Eloquentの循環参照防止トレイト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` の2クラスを見る必要があります。
</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()` の1階層上（`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` トレイトでも再利用されています。こちらは「同じオブジェクトの同じ呼び出し箇所を、コールスタック内で再入させない」ためのトレイトです。

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

## パッケージ開発での応用

自作パッケージで「同じインスタンスの同じメソッド呼び出しを1リクエスト中は1回だけ実行したい」場合、`once()` をそのまま使うのが最も簡単です。`Onceable`/`Once` を直接使う機会は少ないですが、以下のような場面では内部実装の理解が役立ちます。

* Artisanコマンドやテストで `Once::disable()` / `Once::flush()` を使ってキャッシュ挙動を制御したいとき
* 自作トレイトで「呼び出し箇所単位」のキャッシュや再入防止を実装したいとき(`PreventsCircularRecursion` と同じパターンが使えます)
* `once()` の結果が想定と異なりデバッグが必要なとき、ハッシュの元になる要素(ファイル・クラス・関数・行・use変数)を理解しておくと原因を特定しやすくなります

<Warning>
  `once()` はクロージャが `use` している変数までハッシュに含めるため、ループの中で毎回異なる値をキャプチャするクロージャを渡すと、意図せず毎回別キャッシュになり実質的にキャッシュされないことがあります。ループ内で使う場合は、キャッシュキーとして意図した粒度になっているか確認してください。
</Warning>

## 関連ページ

<Columns cols={2}>
  <Card title="tap() ヘルパーと Tappable トレイト" icon="hand-point-right" href="/jp/advanced/tap">
    副作用を挟みつつ値を返すtap()ヘルパーの実装パターン。
  </Card>

  <Card title="Eloquent Observers とモデルイベント" icon="eye" href="/jp/advanced/eloquent-observers">
    Eloquentモデルのイベントとオブザーバーによる一元管理。
  </Card>
</Columns>


## Related topics

- [応用トピック](/jp/advanced/index.md)
- [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap.md)
- [Macroableトレイト](/jp/advanced/macroable.md)
- [ヘルパー関数](/jp/helpers.md)
- [パッケージ自動検出の内部構造](/jp/advanced/package-discovery.md)
