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

# Implementación interna del helper once() — Once, Onceable y PreventsCircularRecursion

> Explicamos la implementación interna de la función global once() a partir de las clases Illuminate\Support\Once y Onceable, y cómo se aplica en el trait PreventsCircularRecursion de Eloquent para evitar referencias circulares.

## Qué es once()

`once()` es un helper global que ejecuta una callback y guarda su resultado en memoria durante la petición. Si se invoca de nuevo con la misma callback desde el mismo lugar de llamada, devuelve el resultado cacheado.

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

random(); // 123
random(); // 123 (resultado cacheado)
random(); // 123 (resultado cacheado)
```

Cuando se llama desde el método de una instancia de un objeto, la caché es independiente por instancia.

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

$service = new NumberService;

$service->all();
$service->all(); // (resultado cacheado)

$secondService = new NumberService;

$secondService->all();
$secondService->all(); // (resultado cacheado, distinto del de $service)
```

<Info>
  Este mecanismo, en el que la caché es independiente «por lugar de llamada» y «por instancia», es distinto de un simple helper de memoización tipo `memoize`. Para entenderlo hay que mirar las dos clases `Illuminate\Support\Once` y `Illuminate\Support\Onceable`.
</Info>

## La llamada en helpers.php

El cuerpo de la función `once()` en `Illuminate\Support\helpers.php` es muy sencillo.

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

Lo importante es que `once()` en sí no guarda ninguna caché: monta una instancia de `Onceable` a partir de la información del llamador obtenida con `debug_backtrace()` y delega el trabajo en la clase `Once`.

```mermaid theme={null}
flowchart TD
    A["Se llama once(callback)"] --> B["Se obtiene el llamador con debug_backtrace()"]
    B --> C["Onceable::tryFromTrace()"]
    C --> D{"¿Se pudo calcular el hash?"}
    D -- No --> E["Se ejecuta la callback directamente"]
    D -- Sí --> F["Once::instance()->value(onceable)"]
    F --> G{"¿Ya está cacheado en el WeakMap?"}
    G -- Sí --> H["Devuelve el valor cacheado"]
    G -- No --> I["Ejecuta la callback y guarda en caché"]
```

## Onceable — cálculo del hash que identifica el lugar de llamada

La clase `Onceable` calcula, a partir del backtrace, un hash que identifica de forma única «desde qué lugar de llamada» se ha invocado.

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

La información base del hash es: `ruta del archivo + nombre de clase + nombre de la función + número de línea + valores de las variables capturadas con use por el closure`. Es decir, **aunque el `once()` se llame desde la misma línea, si cambian los valores capturados con `use`, se tratará como una caché distinta**.

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

greet('Alice'); // Calcula y cachea "Hello, Alice!"
greet('Bob');   // Como $name es distinto, el hash cambia y se recalcula
```

<Warning>
  Si llamas a `once()` desde código ejecutado con `eval()`, `hashFromTrace()` devuelve `null` y no se hace ninguna caché. Es una medida de seguridad pensada para las vistas Blade compiladas y otros casos que pasan por eval.
</Warning>

`object` indica «si el llamador es un método de instancia». `$trace[1]['object']` es el objeto de un nivel arriba en `debug_backtrace()` (es decir, el que llamó a `once()`), por lo que si es un método de instancia contiene `$this`, y si es un método estático o una función global es `null`.

## Once — la caché en sí basada en WeakMap

El almacenamiento real de la caché lo asume `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 tecnología clave es `WeakMap`. Se usa `$onceable->object` (la instancia del llamador) como clave y, como valor, un array asociativo hash → resultado. Cuando no hay `object` (función global o método estático), la clave pasa a ser el propio `$this` de `Once`, lo que en la práctica implica una única zona de caché compartida en todo el proceso.

<Info>
  Como se utiliza `WeakMap`, en cuanto no queden referencias al objeto usado como clave, la caché asociada a ese objeto también será candidata al recolector de basura. Es un diseño que dificulta las fugas de memoria aunque `once()` se llame desde muchos objetos.
</Info>

### Uso en pruebas — enable / disable / flush

Con `Once::disable()` la caché se desactiva por completo y `once()` ejecuta la callback en cada llamada. Es útil cuando en las pruebas quieres «un valor nuevo cada vez».

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

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

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

`Once::flush()` descarta la caché entera y en el próximo acceso vuelve a crear un `WeakMap` nuevo. Se usa en pruebas de comandos Artisan o en entornos como Octane donde los procesos se reutilizan y no quieres arrastrar la caché entre peticiones.

## PreventsCircularRecursion — una aplicación de Onceable

`Onceable::tryFromTrace()` no es exclusivo de `once()`: también lo reutiliza el trait `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` de Eloquent. Este trait sirve para «impedir que el mismo lugar de llamada del mismo objeto vuelva a entrar dentro de la pila de llamadas».

```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() están implementados sobre WeakMap
}
```

La diferencia decisiva con `Once` es que **la caché se limpia una vez terminada la llamada, en el bloque `finally`**. Mientras que `Once` mantiene la caché de forma permanente durante la petición, `PreventsCircularRecursion` reutiliza el mismo mecanismo de `Onceable` para cachear (en realidad, «bloquear») únicamente durante la pila de llamadas en curso.

Un uso típico en modelos Eloquent es evitar que el propio `toArray()` o un accessor se refiera recursivamente a sí mismo.

```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 () {
            // Aunque el acceso al path del parent
            // produzca una referencia circular, aquí se evita el bucle infinito
            return $this->parent
                ? $this->parent->path.' > '.$this->name
                : $this->name;
        }, default: $this->name);
    }
}
```

## Aplicaciones en el desarrollo de paquetes

Si en tu propio paquete quieres «ejecutar una sola vez por petición la misma llamada al mismo método de la misma instancia», usar directamente `once()` es lo más sencillo. Rara vez tendrás que usar `Onceable`/`Once` directamente, pero entender la implementación interna resulta útil en situaciones como estas:

* Cuando quieras controlar el comportamiento de la caché en comandos Artisan o pruebas con `Once::disable()` / `Once::flush()`.
* Cuando quieras implementar en un trait propio una caché «por lugar de llamada» o una protección contra reentradas (puedes seguir el mismo patrón que `PreventsCircularRecursion`).
* Cuando el resultado de `once()` no sea el esperado y necesites depurar: conocer los elementos que forman parte del hash (archivo, clase, función, línea, variables use) te ayuda a identificar la causa.

<Warning>
  Como `once()` incluye en el hash también las variables capturadas con `use` por el closure, si dentro de un bucle pasas un closure que captura valores distintos en cada iteración, cada iteración usará una caché distinta y, en la práctica, no se cacheará nada. Cuando lo uses dentro de un bucle, comprueba que la granularidad de la clave de caché es la que esperas.
</Warning>

## Páginas relacionadas

<Columns cols={2}>
  <Card title="Helper tap() y trait Tappable" icon="hand-point-right" href="/es/advanced/tap">
    Patrón de implementación del helper tap() que devuelve el valor a la vez que intercala efectos colaterales.
  </Card>

  <Card title="Observers de Eloquent y eventos del modelo" icon="eye" href="/es/advanced/eloquent-observers">
    Gestión centralizada mediante eventos y observers de los modelos Eloquent.
  </Card>
</Columns>


## Related topics

- [Temas avanzados](/es/advanced/index.md)
- [進階主題](/zh-TW/advanced/index.md)
- [Helper tap() y trait Tappable](/es/advanced/tap.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Implementación de un guard de autenticación personalizado](/es/advanced/custom-auth-guard.md)
