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

# Implementazione interna dell'helper once() — Once, Onceable e PreventsCircularRecursion

> Analisi dell'implementazione interna della funzione globale once() a partire dalle classi Illuminate\Support\Once e Onceable, con applicazione al trait PreventsCircularRecursion di Eloquent per la prevenzione dei riferimenti circolari.

## Cos'è once()

`once()` è un helper globale che esegue una callback e memorizza il risultato in memoria per la durata della richiesta. Se viene chiamato di nuovo dallo stesso punto con la stessa callback, restituisce il risultato memorizzato.

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

random(); // 123
random(); // 123 (risultato in cache)
random(); // 123 (risultato in cache)
```

Se chiamato all'interno del metodo di un'istanza, la cache è indipendente per ciascuna istanza.

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

$service = new NumberService;

$service->all();
$service->all(); // (risultato in cache)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (risultato in cache, separato da quello di $service)
```

<Info>
  Questo meccanismo "cache indipendente per punto di chiamata e per istanza" differisce da un semplice helper di memoizzazione come `memoize`. Per capire il funzionamento occorre guardare le due classi `Illuminate\Support\Once` e `Illuminate\Support\Onceable`.
</Info>

## La chiamata in helpers.php

La funzione `once()` in `Illuminate\Support\helpers.php` è molto semplice.

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

Il punto chiave è che `once()` di per sé non contiene alcuna cache: assembla un'istanza di `Onceable` con le informazioni sul chiamante ottenute da `debug_backtrace()` e delega l'elaborazione alla classe `Once`.

```mermaid theme={null}
flowchart TD
    A["Chiama once(callback)"] --> B["Ottiene il chiamante con debug_backtrace()"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"Hash calcolato?"}
    D -- no --> E["Esegue direttamente la callback"]
    D -- sì --> F["Once::instance()->value(onceable)"]
    F --> G{"Già in cache nella WeakMap?"}
    G -- sì --> H["Restituisce il valore in cache"]
    G -- no --> I["Esegue la callback e la salva in cache"]
```

## Onceable — calcolo dell'hash che identifica il punto di chiamata

La classe `Onceable` ha il compito di calcolare, a partire dalla backtrace, un hash che identifica univocamente "da quale punto è stata effettuata la chiamata".

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

Le informazioni da cui deriva l'hash sono `percorso del file + nome della classe + nome della funzione + numero di riga + valori delle variabili use della closure`. In altre parole, **anche una `once()` chiamata dalla stessa riga viene considerata una cache diversa se cambiano i valori delle variabili in `use`**.

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

greet('Alice'); // Calcola "Hello, Alice!" e lo mette in cache
greet('Bob');   // $name è diverso, l'hash cambia e viene ricalcolato
```

<Warning>
  Se chiami `once()` da codice eseguito con `eval()`, `hashFromTrace()` restituisce `null` e non viene effettuata alcuna cache. Si tratta di una misura di sicurezza pensata per l'esecuzione tramite eval, come nelle viste Blade compilate.
</Warning>

`object` indica "se il chiamante è un metodo di istanza". `$trace[1]['object']` è l'oggetto un livello sopra nella `debug_backtrace()` (cioè chi ha chiamato `once()`), quindi contiene `$this` all'interno di un metodo di istanza e `null` in un metodo statico o in una funzione globale.

## Once — il vero contenitore della cache basato su WeakMap

La conservazione effettiva della cache è a carico di `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 tecnologia chiave è `WeakMap`. Usa `$onceable->object` (l'istanza chiamante) come chiave e conserva come valore un array associativo hash → risultato. Quando `object` è assente (funzione globale o metodo statico), la chiave diventa `$this` di `Once` stesso, che di fatto rappresenta un'unica area di cache condivisa in tutto il processo.

<Info>
  Poiché viene usata una `WeakMap`, quando non esistono più altri riferimenti all'oggetto usato come chiave, anche la cache associata a quell'oggetto diventa candidata alla garbage collection. È un design pensato per evitare memory leak anche quando `once()` viene chiamato da un gran numero di oggetti.
</Info>

### Uso nei test — enable / disable / flush

Chiamando `Once::disable()` la cache viene completamente disabilitata e `once()` esegue la callback ogni volta. È utile nei test quando "vuoi un nuovo valore a ogni chiamata".

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

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

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

`Once::flush()` scarta l'intera cache e ricrea una nuova `WeakMap` al prossimo accesso. È utile nei test dei comandi Artisan o in ambienti come Octane in cui i processi vengono riutilizzati e non vuoi che la cache sopravviva alle richieste.

## PreventsCircularRecursion — un'applicazione di Onceable

`Onceable::tryFromTrace()` non è esclusiva di `once()`: viene riutilizzata anche nel trait `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` di Eloquent. Questo trait serve a "evitare che lo stesso punto di chiamata sullo stesso oggetto rientri nella stessa call stack".

```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() sono tutti implementati con WeakMap
}
```

La differenza decisiva rispetto a `Once` è che nel blocco `finally` la cache **viene ripulita al termine della chiamata**. Mentre `Once` "mette in cache in modo persistente per tutta la durata della richiesta", `PreventsCircularRecursion` riutilizza lo stesso meccanismo `Onceable` per "mettere in cache (in pratica bloccare) solo per la durata della call stack in esecuzione".

Un esempio tipico di uso in un modello Eloquent è la prevenzione dei casi in cui il metodo `toArray()` o un accessor del modello finisce per riferirsi ricorsivamente a se stesso.

```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 () {
            // Nella logica che consulta il path del parent
            // qui viene evitato il loop infinito anche se
            // si verifica un riferimento circolare
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## Applicazione nello sviluppo di pacchetti

Nel tuo pacchetto, quando vuoi "eseguire una sola volta per richiesta la stessa chiamata di metodo sulla stessa istanza", il modo più semplice è usare direttamente `once()`. Raramente userai `Onceable`/`Once` in modo diretto, ma comprendere l'implementazione interna è utile nei seguenti casi:

* Quando vuoi controllare il comportamento della cache nei comandi Artisan o nei test con `Once::disable()` / `Once::flush()`
* Quando vuoi implementare in un tuo trait una cache "per punto di chiamata" o una prevenzione del rientro (puoi usare lo stesso schema di `PreventsCircularRecursion`)
* Quando il risultato di `once()` non è quello atteso e devi fare debug: capire da quali elementi deriva l'hash (file, classe, funzione, riga, variabili use) rende più semplice individuare la causa

<Warning>
  Poiché `once()` include nell'hash anche le variabili in `use` della closure, se dentro un ciclo passi una closure che cattura valori diversi a ogni iterazione, ti ritroverai involontariamente con una cache diversa ogni volta e di fatto non avrai alcuna cache. Quando lo usi in un ciclo, verifica che la granularità della chiave di cache sia quella voluta.
</Warning>

## Pagine correlate

<Columns cols={2}>
  <Card title="Helper tap() e trait Tappable" icon="hand-point-right" href="/it/advanced/tap">
    Pattern di implementazione dell'helper tap() che intercala side effect restituendo il valore.
  </Card>

  <Card title="Eloquent Observers ed eventi del modello" icon="eye" href="/it/advanced/eloquent-observers">
    Gestione centralizzata tramite eventi e observer dei modelli Eloquent.
  </Card>
</Columns>


## Related topics

- [Argomenti avanzati](/it/advanced/index.md)
- [進階主題](/zh-TW/advanced/index.md)
- [Helper tap() e trait Tappable](/it/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Trait Macroable](/it/advanced/macroable.md)
