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

# Implémentation interne du helper once() — Once, Onceable et PreventsCircularRecursion

> Décryptage de l'implémentation interne de la fonction globale once() à travers les classes Illuminate\Support\Once et Onceable, et présentation de son utilisation dans le trait PreventsCircularRecursion d'Eloquent.

## Qu'est-ce que once()

`once()` est un helper global qui exécute un callback puis met en cache son résultat en mémoire pour la durée de la requête. Si le même callback est rappelé depuis le même point d'appel, la valeur mise en cache est renvoyée.

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

random(); // 123
random(); // 123 (résultat mis en cache)
random(); // 123 (résultat mis en cache)
```

Lorsqu'il est appelé depuis une méthode d'instance d'un objet, le cache est indépendant pour chaque instance.

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

$service = new NumberService;

$service->all();
$service->all(); // (résultat mis en cache)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (résultat mis en cache, distinct de celui de $service)
```

<Info>
  Ce mécanisme d'un cache indépendant « par point d'appel et par instance » diffère d'un helper de mémoïsation simple comme `memoize`. Pour comprendre son fonctionnement, il faut examiner les deux classes `Illuminate\Support\Once` et `Illuminate\Support\Onceable`.
</Info>

## Appel dans helpers.php

Le corps de la fonction `once()` dans `Illuminate\Support\helpers.php` est très simple.

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

Le point clé est que `once()` lui-même ne stocke aucun cache : à partir des informations sur l'appelant obtenues via `debug_backtrace()`, il construit une instance de `Onceable` et délègue le traitement à la classe `Once`.

```mermaid theme={null}
flowchart TD
    A["Appel de once(callback)"] --> B["debug_backtrace() récupère l'appelant"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"Un hash a-t-il pu être calculé ?"}
    D -- Non --> E["Exécute callback directement"]
    D -- Oui --> F["Once::instance()->value(onceable)"]
    F --> G{"Déjà en cache dans le WeakMap ?"}
    G -- Oui --> H["Retourne la valeur en cache"]
    G -- Non --> I["Exécute callback et met en cache"]
```

## Onceable — Calcul du hash identifiant le point d'appel

La classe `Onceable` a pour rôle de calculer, à partir de la backtrace, un hash identifiant de façon unique « depuis quel point d'appel » l'appel provient.

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

Les informations qui servent à calculer le hash sont : `chemin du fichier + nom de classe + nom de fonction + numéro de ligne + valeurs des variables capturées (use) par la closure`. Autrement dit, **même pour un `once()` appelé depuis la même ligne, si la valeur d'une variable capturée via `use` change, le cache est considéré comme différent**.

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

greet('Alice'); // Calcule et met en cache "Hello, Alice!"
greet('Bob');   // La valeur de $name diffère : le hash change, recalcul
```

<Warning>
  Si `once()` est appelé depuis du code évalué via `eval()`, `hashFromTrace()` renvoie `null` et aucun cache n'est effectué. C'est une garde de sécurité pour les exécutions passant par eval, comme les vues Blade compilées.
</Warning>

`object` indique si « l'appelant est une méthode d'instance ». `$trace[1]['object']` correspond à un niveau au-dessus dans `debug_backtrace()` (le code qui a appelé `once()`) : dans une méthode d'instance il contient `$this`, tandis que dans une méthode statique ou une fonction globale il vaut `null`.

## Once — Le stockage de cache basé sur WeakMap

Le stockage effectif du cache est assuré par `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;
    }
}
```

La technique clé est `WeakMap`. `$onceable->object` (l'instance appelante) sert de clé, associée à un tableau associatif hash → résultat. Lorsqu'il n'y a pas d'`object` (fonction globale ou méthode statique), c'est le `$this` de `Once` lui-même qui sert de clé, ce qui constitue en pratique un espace de cache unique partagé pour tout le processus.

<Info>
  Grâce à `WeakMap`, dès qu'il n'existe plus d'autre référence à l'objet servant de clé, le cache qui lui est associé devient éligible au ramasse-miettes. La conception rend donc peu probable une fuite mémoire, même en appelant `once()` depuis un grand nombre d'objets.
</Info>

### Utilisation dans les tests — enable / disable / flush

Appeler `Once::disable()` désactive complètement le cache : `once()` exécute alors le callback à chaque appel. Utile pour les tests où « on veut à chaque fois une nouvelle valeur ».

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

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

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

`Once::flush()` détruit l'intégralité du cache : au prochain accès, un nouveau `WeakMap` sera créé. C'est utile pour tester des commandes Artisan ou pour éviter de conserver le cache d'une requête à l'autre dans un environnement où le processus est réutilisé, comme Octane.

## PreventsCircularRecursion — Un exemple d'utilisation d'Onceable

`Onceable::tryFromTrace()` n'est pas réservé à `once()` : il est également réutilisé par le trait `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` d'Eloquent. Ce trait sert à éviter qu'un même point d'appel, sur un même objet, ne soit ré-entrant au sein d'une même pile d'appels.

```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() sont toutes basées sur WeakMap
}
```

La différence décisive avec `Once` est que **le cache est nettoyé après l'appel dans le bloc `finally`**. Là où `Once` « met en cache de façon persistante pendant la requête », `PreventsCircularRecursion` réutilise le même mécanisme `Onceable` pour « mettre en cache (en pratique verrouiller) uniquement pendant la pile d'appels en cours d'exécution ».

Un cas d'usage typique dans les modèles Eloquent : empêcher qu'une méthode `toArray()` ou un accesseur se référence lui-même de façon récursive.

```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 () {
            // Empêche une boucle infinie ici, même en cas de référence
            // circulaire lors de l'accès au path du parent
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## Utilisation en développement de packages

Si, dans votre package, vous souhaitez « n'exécuter qu'une seule fois par requête un appel donné sur une même instance », le plus simple est d'utiliser directement `once()`. Utiliser `Onceable`/`Once` directement est rare, mais comprendre l'implémentation interne est utile dans les cas suivants :

* Lorsque, dans une commande Artisan ou un test, vous souhaitez contrôler le comportement du cache avec `Once::disable()` / `Once::flush()`
* Lorsque vous voulez implémenter, dans votre propre trait, un cache « par point d'appel » ou une protection contre la ré-entrance (le même pattern que `PreventsCircularRecursion` est réutilisable)
* Lorsqu'un résultat de `once()` ne correspond pas à vos attentes et que vous devez déboguer : connaître les éléments qui composent le hash (fichier, classe, fonction, ligne, variables `use`) facilite l'identification de la cause

<Warning>
  Comme `once()` inclut dans le hash les variables capturées via `use` par la closure, passer dans une boucle une closure qui capture à chaque itération une valeur différente peut créer, involontairement, un cache différent à chaque tour — le résultat n'est alors, en pratique, pas mis en cache. Lorsque vous l'utilisez dans une boucle, vérifiez que la granularité de la clé de cache correspond bien à votre intention.
</Warning>

## Pages associées

<Columns cols={2}>
  <Card title="Helper tap() et trait Tappable" icon="hand-point-right" href="/fr/advanced/tap">
    Le pattern d'implémentation du helper tap(), qui glisse un effet de bord tout en renvoyant une valeur.
  </Card>

  <Card title="Observers Eloquent et événements de modèle" icon="eye" href="/fr/advanced/eloquent-observers">
    Gestion centralisée via les événements et les observers des modèles Eloquent.
  </Card>
</Columns>


## Related topics

- [Sujets avancés](/fr/advanced/index.md)
- [進階主題](/zh-TW/advanced/index.md)
- [Helper tap() et trait Tappable](/fr/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Fonctionnement interne de la découverte automatique des packages](/fr/advanced/package-discovery.md)
