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

# Interne Implementierung des once()-Helpers — Once, Onceable und PreventsCircularRecursion

> Erläutert die interne Implementierung der globalen Funktion once() anhand der Klassen Illuminate\Support\Once und Onceable und stellt die Anwendung im Eloquent-Trait PreventsCircularRecursion zur Vermeidung zirkulärer Referenzen vor.

## Was ist once()?

`once()` ist ein globaler Helper, der einen Callback ausführt und dessen Ergebnis für die Dauer des Requests im Speicher cacht. Wird derselbe Callback erneut von derselben Aufrufstelle heraus aufgerufen, gibt der Helper das zwischengespeicherte Ergebnis zurück.

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

random(); // 123
random(); // 123 (zwischengespeichertes Ergebnis)
random(); // 123 (zwischengespeichertes Ergebnis)
```

Rufen Sie den Helper aus einer Instanzmethode eines Objekts auf, ist der Cache auf diese Instanz beschränkt.

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

$service = new NumberService;

$service->all();
$service->all(); // (zwischengespeichertes Ergebnis)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (zwischengespeichertes Ergebnis, unabhängig vom Cache von $service)
```

<Info>
  Der Mechanismus, dass der Cache „pro Aufrufstelle und pro Instanz" isoliert wird, unterscheidet sich von einfachen Memoization-Helpern wie `memoize`. Um ihn zu verstehen, muss man sich die beiden Klassen `Illuminate\Support\Once` und `Illuminate\Support\Onceable` anschauen.
</Info>

## Der Aufruf in helpers.php

Der Rumpf der Funktion `once()` in `Illuminate\Support\helpers.php` ist denkbar einfach.

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

Entscheidend ist: `once()` selbst hält keinen Cache. Es baut aus den Informationen zum Aufrufer, die `debug_backtrace()` liefert, eine `Onceable`-Instanz zusammen und delegiert die eigentliche Verarbeitung an die Klasse `Once`.

```mermaid theme={null}
flowchart TD
    A["once(callback) aufrufen"] --> B["debug_backtrace() für den Aufrufer holen"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"Konnte ein Hash berechnet werden?"}
    D -- Nein --> E["callback direkt ausführen"]
    D -- Ja --> F["Once::instance()->value(onceable)"]
    F --> G{"Bereits im WeakMap gecacht?"}
    G -- Ja --> H["Zwischengespeicherten Wert zurückgeben"]
    G -- Nein --> I["callback ausführen und im Cache ablegen"]
```

## Onceable — Hash-Berechnung zur Identifikation der Aufrufstelle

Die Klasse `Onceable` ist dafür zuständig, aus dem Backtrace einen Hash zu berechnen, der eindeutig festlegt, „von welcher Aufrufstelle" der Aufruf kommt.

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

Grundlage des Hashes sind `Dateipfad + Klassenname + Funktionsname + Zeilennummer + Werte der von der Closure per use eingefangenen Variablen`. Anders gesagt: **Selbst wenn `once()` aus derselben Zeile aufgerufen wird, gilt es als anderer Cache, sobald sich der Wert einer per `use` eingefangenen Variablen ändert.**

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

greet('Alice'); // "Hello, Alice!" wird berechnet und gecacht
greet('Bob');   // Der Wert von $name ist anders, daher ändert sich auch der Hash und es wird neu berechnet
```

<Warning>
  Wird `once()` aus per `eval()` ausgeführtem Code aufgerufen, liefert `hashFromTrace()` `null` zurück und es wird gar nicht gecacht. Das ist eine Sicherheitsmaßnahme für Ausführungen über eval, etwa in kompilierten Blade-Views.
</Warning>

`object` gibt an, ob der Aufrufer eine Instanzmethode ist. `$trace[1]['object']` bezieht sich auf eine Ebene über `once()` (also die Seite, die `once()` aufgerufen hat). Bei einer Instanzmethode landet dort `$this`; bei einer statischen Methode oder einer globalen Funktion ist der Wert `null`.

## Once — der eigentliche Cache über WeakMap

Die tatsächliche Haltung des Caches übernimmt `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;
    }
}
```

Die entscheidende Technik ist `WeakMap`. Als Schlüssel dient `$onceable->object` (die Aufrufer-Instanz); der Wert ist ein assoziatives Array von Hash → Ergebnis. Fehlt `object` (bei globalen Funktionen oder statischen Methoden), dient `$this` von `Once` selbst als Schlüssel – das bedeutet praktisch einen einzigen, prozessweit geteilten Cache-Bereich.

<Info>
  Da `WeakMap` verwendet wird, wird auch der an das Objekt gebundene Cache Garbage-Collection-fähig, sobald es keine weiteren Referenzen mehr auf das Schlüssel-Objekt gibt. Selbst wenn Sie `once()` aus vielen Objekten heraus aufrufen, ist das Design robust gegen Memory-Leaks.
</Info>

### Einsatz in Tests — enable / disable / flush

Ein Aufruf von `Once::disable()` deaktiviert den Cache vollständig; `once()` führt den Callback dann jedes Mal aus. Praktisch für Tests, in denen Sie „jedes Mal einen neuen Wert" möchten.

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

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

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

`Once::flush()` verwirft den gesamten Cache und legt beim nächsten Zugriff einen neuen `WeakMap` an. Sinnvoll bei Tests für Artisan-Befehle oder in Umgebungen, in denen Prozesse wiederverwendet werden (z. B. Octane), wenn Sie den Cache nicht zwischen Requests behalten möchten.

## PreventsCircularRecursion — ein Anwendungsbeispiel von Onceable

`Onceable::tryFromTrace()` wird nicht nur von `once()` genutzt. Auch das Eloquent-Trait `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` verwendet es wieder. Dieses Trait dient dazu, „denselben Aufruf desselben Objekts innerhalb des Call-Stacks nicht erneut zuzulassen".

```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() sind allesamt WeakMap-basierte Implementierungen
}
```

Der entscheidende Unterschied zu `Once` liegt darin, dass der Cache im `finally`-Block **nach Abschluss des Aufrufs wieder gelöscht wird**. Während `Once` „während des Requests dauerhaft cacht", nutzt `PreventsCircularRecursion` denselben `Onceable`-Mechanismus, um „nur für die Dauer des aktuellen Call-Stacks zu cachen (praktisch als Lock)".

Ein typischer Einsatz bei Eloquent-Modellen ist die Vermeidung von Rekursionen, wenn `toArray()` oder ein Accessor sich selbst rekursiv referenziert.

```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 () {
            // Auch wenn beim Zugriff auf den path des parent
            // eine zirkuläre Referenz entsteht, wird hier die Endlosschleife verhindert
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## Einsatz in der Paketentwicklung

Möchten Sie in einem eigenen Paket „denselben Methodenaufruf derselben Instanz während eines Requests nur einmal ausführen", ist es am einfachsten, `once()` direkt zu verwenden. Direkt mit `Onceable`/`Once` zu arbeiten kommt seltener vor, doch in folgenden Situationen hilft das Verständnis der internen Implementierung:

* Wenn Sie in Artisan-Befehlen oder Tests mit `Once::disable()` / `Once::flush()` das Cache-Verhalten steuern möchten
* Wenn Sie in einem eigenen Trait einen Cache oder eine Rekursionssperre „pro Aufrufstelle" implementieren möchten (dasselbe Muster wie `PreventsCircularRecursion`)
* Wenn das Ergebnis von `once()` von der Erwartung abweicht und Sie debuggen müssen: Ein Verständnis der Bestandteile, die in den Hash einfließen (Datei, Klasse, Funktion, Zeile, per `use` gefangene Variablen), erleichtert die Ursachensuche

<Warning>
  Da `once()` auch die per `use` in der Closure eingefangenen Variablen mit in den Hash einbezieht, kann eine Closure, die in einer Schleife bei jedem Durchlauf andere Werte einfängt, ungewollt jedes Mal einen neuen Cache erhalten und damit faktisch gar nichts cachen. Wenn Sie `once()` innerhalb einer Schleife verwenden, prüfen Sie, ob der Cache-Schlüssel die von Ihnen gewünschte Granularität besitzt.
</Warning>

## Verwandte Seiten

<Columns cols={2}>
  <Card title="tap()-Helper und Tappable-Trait" icon="hand-point-right" href="/de/advanced/tap">
    Das Implementierungsmuster des tap()-Helpers, der einen Seiteneffekt einschleift und dennoch den Wert zurückgibt.
  </Card>

  <Card title="Eloquent-Observer und Model-Events" icon="eye" href="/de/advanced/eloquent-observers">
    Zentrale Verwaltung über Events und Observer von Eloquent-Modellen.
  </Card>
</Columns>


## Related topics

- [Weiterführende Themen](/de/advanced/index.md)
- [進階主題](/zh-TW/advanced/index.md)
- [tap()-Helper und Tappable-Trait](/de/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Conditionable-Trait](/de/advanced/conditionable.md)
