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.
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.La llamada en helpers.php
El cuerpo de la funciónonce() en Illuminate\Support\helpers.php es muy sencillo.
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.
Onceable — cálculo del hash que identifica el lugar de llamada
La claseOnceable calcula, a partir del backtrace, un hash que identifica de forma única «desde qué lugar de llamada» se ha invocado.
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.
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 asumeIlluminate\Support\Once.
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.
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.Uso en pruebas — enable / disable / flush
ConOnce::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».
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».
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.
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 directamenteonce() 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.
Páginas relacionadas
Helper tap() y trait Tappable
Patrón de implementación del helper tap() que devuelve el valor a la vez que intercala efectos colaterales.
Observers de Eloquent y eventos del modelo
Gestión centralizada mediante eventos y observers de los modelos Eloquent.