什麼是 once()
once() 是執行 callback 並在該次請求期間將結果快取於記憶體的全域 helper。從相同呼叫位置以相同 callback 再次呼叫時,會回傳已快取的結果。
這種「依呼叫位置、依實例」獨立的快取機制,與
memoize 這類簡單的記憶化 helper 不同。要理解其原理,需檢視 Illuminate\Support\Once 與 Illuminate\Support\Onceable 這兩個類別。helpers.php 中的呼叫
Illuminate\Support\helpers.php 中 once() 函式本體極為簡單。
once() 本身並不持有快取,而是以 debug_backtrace() 取得呼叫端資訊,組出 Onceable 實例後,將處理委派給 Once 類別。
Onceable — 用來鎖定呼叫位置的 hash 計算
Onceable 類別負責從 backtrace 計算出可唯一鎖定「哪個呼叫位置」的 hash。
檔案路徑 + 類別名稱 + 函式名稱 + 行號 + closure use 到的變數值。也就是說,即使是從同一行呼叫的 once(),只要 use 的變數值改變,就會被視為不同的快取。
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() 是最簡單的方式。直接使用 Onceable/Once 的機會不多,但在下列情境中,理解其內部實作會有幫助。
- 在 Artisan 指令或測試中,想以
Once::disable()/Once::flush()控制快取行為時 - 想在自製 trait 中實作「以呼叫位置為單位」的快取或防止重入(可套用與
PreventsCircularRecursion相同的模式) once()的結果與預期不符而需除錯時,理解構成 hash 的元素(檔案、類別、函式、行號、use 變數)將更容易鎖定原因
相關頁面
tap() helper 與 Tappable trait
在插入副作用的同時仍回傳值的 tap() helper 實作模式。
Eloquent Observers 與 Model 事件
以 Eloquent Model 事件與 observer 進行集中管理。