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

# De interne implementatie van de once()-helper — Once, Onceable en PreventsCircularRecursion

> Uitleg van de interne implementatie van de globale functie once() aan de hand van de klassen Illuminate\Support\Once en Onceable, plus de toepassing in Eloquents PreventsCircularRecursion-trait die circulaire referenties voorkomt.

## Wat is once()?

`once()` is een globale helper die een callback uitvoert en het resultaat gedurende het request in het geheugen cachet. Wordt hij vanaf hetzelfde aanroeppunt opnieuw aangeroepen met dezelfde callback, dan wordt de gecachte waarde teruggegeven.

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

random(); // 123
random(); // 123 (gecachte waarde)
random(); // 123 (gecachte waarde)
```

Wordt hij aangeroepen vanuit een methode op een object, dan is de cache per instantie onafhankelijk.

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

$service = new NumberService;

$service->all();
$service->all(); // (gecachte waarde)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (gecachte waarde, aparte cache van $service)
```

<Info>
  Het feit dat de cache per aanroeppunt én per instantie onafhankelijk is, onderscheidt `once()` van een eenvoudige memoization-helper zoals `memoize`. Om het mechanisme te begrijpen, moet je naar de twee klassen `Illuminate\Support\Once` en `Illuminate\Support\Onceable` kijken.
</Info>

## De aanroep in helpers.php

De implementatie van de functie `once()` in `Illuminate\Support\helpers.php` is bijzonder eenvoudig.

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

Wat opvalt is dat `once()` zelf geen cache bijhoudt: het bouwt met behulp van de aanroepinformatie uit `debug_backtrace()` een `Onceable`-instantie en delegeert de verwerking aan de klasse `Once`.

```mermaid theme={null}
flowchart TD
    A["once(callback) aanroepen"] --> B["debug_backtrace() haalt de aanroeper op"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"Kon een hash worden berekend?"}
    D -- nee --> E["callback direct uitvoeren"]
    D -- ja --> F["Once::instance()->value(onceable)"]
    F --> G{"Al in de WeakMap gecachet?"}
    G -- ja --> H["De gecachte waarde teruggeven"]
    G -- nee --> I["callback uitvoeren en in de cache opslaan"]
```

## Onceable — een hash berekenen die het aanroeppunt identificeert

De klasse `Onceable` heeft als taak om vanuit de backtrace een hash te berekenen die "welk aanroeppunt" uniek identificeert.

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

De informatie waaruit de hash wordt opgebouwd is: `bestandspad + klassenaam + functienaam + regelnummer + waarden van de variabelen die de closure via use binnenhaalt`. Met andere woorden: **zelfs bij een `once()` op dezelfde regel wordt het als een andere cache behandeld als de waarden van de `use`-variabelen veranderen**.

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

greet('Alice'); // Berekent "Hello, Alice!" en cachet die
greet('Bob');   // Andere waarde van $name → andere hash → wordt opnieuw berekend
```

<Warning>
  Als je `once()` aanroept vanuit code die via `eval()` is uitgevoerd, retourneert `hashFromTrace()` `null` en wordt er helemaal niets gecachet. Dit is een veiligheidsmaatregel voor uitvoering via eval, zoals bij gecompileerde Blade-views.
</Warning>

`object` geeft aan of de aanroeper een instantiemethode is. `$trace[1]['object']` is het object één niveau boven `once()` (de aanroeper), dus binnen een instantiemethode bevat het `$this` en binnen een statische methode of globale functie is het `null`.

## Once — het eigenlijke cachemechanisme met WeakMap

Het daadwerkelijke bijhouden van de cache is de verantwoordelijkheid van `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;
    }
}
```

De kerntechniek is `WeakMap`. `$onceable->object` (de aanroepende instantie) wordt gebruikt als sleutel, en als waarde wordt een associatief array van hash → resultaat opgeslagen. Ontbreekt `object` (bij globale functies of statische methoden), dan wordt `$this` van `Once` zelf de sleutel, waardoor er in feite één gedeelde cache voor het hele proces ontstaat.

<Info>
  Doordat er een `WeakMap` gebruikt wordt, komt de cache die aan een object hangt in aanmerking voor garbage collection zodra er geen andere verwijzingen naar dat object bestaan. Zo blijft `once()` weinig gevoelig voor geheugenlekken, zelfs als hij vanuit heel veel objecten wordt aangeroepen.
</Info>

### Toepassing bij tests — enable / disable / flush

Door `Once::disable()` aan te roepen wordt de cache volledig uitgeschakeld en voert `once()` de callback iedere keer opnieuw uit. Dat is handig als je in tests "elke keer een nieuwe waarde" wilt.

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

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

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

`Once::flush()` gooit de volledige cache weg en maakt bij de volgende toegang een nieuwe `WeakMap` aan. Dit is nuttig bij het testen van Artisan-commando's of in omgevingen zoals Octane, waar het proces wordt hergebruikt en je de cache niet tussen requests wilt behouden.

## PreventsCircularRecursion — een toepassing van Onceable

`Onceable::tryFromTrace()` is niet exclusief voor `once()`; ook Eloquents trait `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` hergebruikt hem. Deze trait dient om "hetzelfde aanroeppunt op hetzelfde object niet opnieuw binnen dezelfde call stack te laten uitvoeren".

```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() zijn allemaal WeakMap-gebaseerde implementaties
}
```

Het beslissende verschil met `Once` is dat er in het `finally`-blok **de cache wordt gewist nadat de aanroep is voltooid**. Waar `Once` "gedurende het request permanent cachet", gebruikt `PreventsCircularRecursion` hetzelfde `Onceable`-mechanisme om "alleen tijdens de huidige call stack te cachen (feitelijk een lock)".

Een typisch gebruik op een Eloquent-model is het voorkomen van gevallen waarin `toArray()` of een accessor van het model zichzelf recursief refereert.

```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 () {
            // Voorkomt dat het verwerken van parent->path
            // hier in een oneindige lus terechtkomt bij circulaire referenties
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## Toepassing bij packageontwikkeling

Wil je in je eigen package "dezelfde methodeaanroep op dezelfde instantie tijdens één request maar één keer uitvoeren", dan is `once()` rechtstreeks gebruiken het eenvoudigst. Je zult `Onceable`/`Once` zelden rechtstreeks nodig hebben, maar het begrip van de interne werking is nuttig in situaties als deze:

* Wanneer je in Artisan-commando's of tests met `Once::disable()` / `Once::flush()` het cachegedrag wilt sturen
* Wanneer je in een eigen trait cache of herintredebeveiliging per aanroeppunt wilt implementeren (hetzelfde patroon als `PreventsCircularRecursion` is bruikbaar)
* Wanneer het resultaat van `once()` niet is wat je verwacht en je moet debuggen; als je begrijpt uit welke elementen de hash bestaat (bestand, klasse, functie, regel, use-variabelen), kun je de oorzaak sneller opsporen

<Warning>
  Omdat `once()` ook de variabelen die de closure via `use` binnenhaalt in de hash meeneemt, kan het gebeuren dat je in een lus een closure doorgeeft die telkens andere waarden vastlegt. Dan wordt onbedoeld iedere keer een aparte cache aangemaakt en wordt er feitelijk niets gecachet. Controleer bij gebruik binnen een lus of je de cachesleutel op het bedoelde detailniveau hebt.
</Warning>

## Gerelateerde pagina's

<Columns cols={2}>
  <Card title="De tap()-helper en de Tappable-trait" icon="hand-point-right" href="/nl/advanced/tap">
    Implementatiepatronen van de tap()-helper waarmee je bijeffecten tussenvoegt en de waarde teruggeeft.
  </Card>

  <Card title="Eloquent Observers en model events" icon="eye" href="/nl/advanced/eloquent-observers">
    Gecentraliseerd beheer van Eloquent-model events en observers.
  </Card>
</Columns>


## Related topics

- [Geavanceerde onderwerpen](/nl/advanced/index.md)
- [進階主題](/zh-TW/advanced/index.md)
- [De tap()-helper en de Tappable-trait](/nl/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [De interne structuur van package discovery](/nl/advanced/package-discovery.md)
