Skip to main content

什麼是 once()

once() 是執行 callback 並在該次請求期間將結果快取於記憶體的全域 helper。從相同呼叫位置以相同 callback 再次呼叫時,會回傳已快取的結果。
若從物件實例的方法內呼叫,快取會以該實例為單位獨立存放。
這種「依呼叫位置、依實例」獨立的快取機制,與 memoize 這類簡單的記憶化 helper 不同。要理解其原理,需檢視 Illuminate\Support\OnceIlluminate\Support\Onceable 這兩個類別。

helpers.php 中的呼叫

Illuminate\Support\helpers.phponce() 函式本體極為簡單。
重點在於:once() 本身並不持有快取,而是以 debug_backtrace() 取得呼叫端資訊,組出 Onceable 實例後,將處理委派給 Once 類別。

Onceable — 用來鎖定呼叫位置的 hash 計算

Onceable 類別負責從 backtrace 計算出可唯一鎖定「哪個呼叫位置」的 hash。
hash 的計算來源為 檔案路徑 + 類別名稱 + 函式名稱 + 行號 + closure use 到的變數值。也就是說,即使是從同一行呼叫的 once(),只要 use 的變數值改變,就會被視為不同的快取
若從 eval() 執行的程式碼中呼叫 once()hashFromTrace() 會回傳 null,完全不會進行快取。這是針對 Blade 已編譯視圖等透過 eval 執行的情境所做的安全防範。
object 表示「呼叫端是否為實例方法」。$trace[1]['object']debug_backtrace() 上一層(呼叫 once() 的一側)的物件,因此在實例方法中會是 $this,靜態方法或全域函式中則為 null

Once — 以 WeakMap 為核心的快取本體

實際的快取保存工作由 Illuminate\Support\Once 負責。
關鍵技術是 WeakMap。以 $onceable->object(呼叫端實例)為 key,保存「hash → 結果」的關聯陣列作為值。若沒有 object(全域函式或靜態方法),Once 自身的 $this 會成為 key,實質上會成為整個程序共用的單一快取區域。
由於使用 WeakMap,一旦作為 key 的物件不再有其他參照,該物件對應的快取也會成為垃圾回收的對象。這是即使從大量物件呼叫 once() 也不易造成記憶體洩漏的設計。

在測試中的應用 — enable / disable / flush

呼叫 Once::disable() 可完全停用快取,once() 每次都會執行 callback。適合測試中「每次都想取得新值」的場景。
Once::flush() 會捨棄整個快取,於下一次存取時重建全新的 WeakMap。適用於 Artisan 指令測試,或 Octane 這類程序會被重複使用、需避免快取跨請求殘留的環境。

PreventsCircularRecursion — Onceable 的應用範例

Onceable::tryFromTrace() 並非 once() 專用,Eloquent 的 Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion trait 也重複利用了它。這個 trait 的目的是「防止同一物件的同一呼叫位置在 call stack 中重入」。
Once 的關鍵差異在於:finally 區塊中,於呼叫結束後即清除快取Once 是「請求期間持續快取」,而 PreventsCircularRecursion 則是「僅在目前執行中的 call stack 期間快取(實質上為 lock)」,借用了相同的 Onceable 機制。 Eloquent Model 上典型的用途,是防止 Model 的 toArray() 或 accessor 在遞迴中參照到自己的情況。

套件開發的應用

自製套件中若想「讓同一實例的同一方法呼叫在單次請求中僅執行一次」,直接使用 once() 是最簡單的方式。直接使用 OnceableOnce 的機會不多,但在下列情境中,理解其內部實作會有幫助。
  • 在 Artisan 指令或測試中,想以 Once::disable()Once::flush() 控制快取行為時
  • 想在自製 trait 中實作「以呼叫位置為單位」的快取或防止重入(可套用與 PreventsCircularRecursion 相同的模式)
  • once() 的結果與預期不符而需除錯時,理解構成 hash 的元素(檔案、類別、函式、行號、use 變數)將更容易鎖定原因
由於 once() 會將 closure use 的變數也納入 hash,若在迴圈中傳入每次都捕獲不同值的 closure,可能會意外地每次產生不同的快取,實質上等同於未快取。在迴圈中使用時,請確認快取 key 的粒度是否符合預期。

相關頁面

tap() helper 與 Tappable trait

在插入副作用的同時仍回傳值的 tap() helper 實作模式。

Eloquent Observers 與 Model 事件

以 Eloquent Model 事件與 observer 進行集中管理。
最後修改於 2026年9月11日