跳轉到主要內容
當你想將查詢到的模型整理成可以用 ID 查找的形式,或想把結果處理集中到區域作用域時,可以使用 afterQuery()。它不只能新增 SQL 條件,還能向查詢建構器註冊一個對查詢結果執行的 Closure。 不過,它並不是「每執行一次 SQL 就會收到相同型別結果的鉤子」。依照查詢方法的不同,傳入的 Collection 內容與呼叫次數都會改變。
本頁面以 Laravel 13 為對象。以官方文件中的 After Query Callbacks為出發點,透過 Eloquent 建構器與官方測試確認使用時的邊界。

將結果處理集中到區域作用域

若需要反覆以 ID 參照模型,可以準備一個統一結果鍵的作用域。以下範例定義在 app/Models/Post.php 中。
呼叫端可以像一般作用域一樣進行鏈式呼叫。
$posts 仍然是 Eloquent Collection,元素也仍然是 Post。get(42) 會回傳鍵為 42 的模型,不存在時回傳 null。若使用 select() 限定查詢欄位,請一併包含作為鍵使用的 id。 如果只在一個地方處理,get()->keyBy('id') 會更單純。當你希望在作用域或重複使用的查詢中也包含查詢後的處理時,再選擇 afterQuery()。作用域本身的定義方式請參考 Eloquent 作用域。

解讀 get 的執行順序

Eloquent\Builder::get() 會套用全域作用域、產生模型、對指定的關聯進行預先載入,最後執行 afterQuery 的回呼。 在一般的 get() 中,回呼可以使用已預先載入的關聯。存取未指定的關聯可能會產生額外的 SQL,因此請搭配預先載入進行設計。 它與 retrieved 模型事件的時機不同。retrieved 會在 Model::newFromBuilder() 產生每個模型時觸發,早於 get() 的預先載入。處理一批查詢結果模型的 afterQuery() 與各模型的事件並不負責相同的工作。
這是 Eloquent\Builder::get() 的流程。cursor() 不會預先載入關聯,回呼也會針對每個模型個別執行。請不要期待它與 get() 有相同的批次處理行為。

回傳值會傳遞給下一個回呼

afterQuery() 會依註冊順序新增 Closure。負責執行的 applyAfterQueryCallbacks() 實作如下。
這裡使用的不是 ??,而是依據 PHP truthy 判斷的 ?:。 也可以使用直接修改 Collection 而不回傳任何內容的 void 回呼。此時會保留被修改的同一個物件。另一方面,若建立了新的 Collection,就必須回傳該 Collection。 內部的參數與回傳值都是 mixed,但應避免把任意型別轉換當成公開 API。例如,將 get() 的結果替換成字串或陣列,會破壞 first() 接著呼叫的 ->first() 等處理。在作用域中,請以保持模型、Collection 型別、筆數與排序的處理為基本原則。

各查詢方法的差異

下表是註冊在 Eloquent 建構器上的 afterQuery() 的處理方式。 first() 在內部會呼叫 limit(1)->get()->first()。不能因為使用了 first() 就把 Closure 的參數改成 Post。
本頁的 keyedById() 是用來查詢模型的作用域。不能用於 Post::keyedById()->pluck('id')。pluck() 傳入的是 Illuminate\Support\Collection,與範例中的 Illuminate\Database\Eloquent\Collection 型別不一致。僅放寬型別宣告,並不能讓從值參照模型屬性的處理變得安全。
若只需要值,請不要使用作用域,直接寫 Post::query()->pluck('id')。若要在處理模型後再取出值,可以寫 Post::keyedById()->get()->pluck('id')。後者的 pluck() 是對已查詢 Collection 的操作,而不是對查詢建構器的操作。

不要改變每頁筆數或下一次查詢位置

paginate() 的總筆數由 SQL 統計,之後再查詢該頁的模型。即使在查詢後篩選 Collection,總筆數也不會重新計算。 另外,simplePaginate() 與 cursorPaginate() 會使用處理後的結果判斷是否有下一頁。chunkById() 會使用區塊末尾的 ID 作為下一次查詢的位置。若在回呼中改變筆數或順序,分頁判斷與接續位置可能會與原本的查詢不一致。 篩選、排序與查詢欄位應在 SQL 端指定,afterQuery() 只用於不改變查詢結果含義的處理。若只是想改變 API 的顯示格式,也可以考慮使用 API 資源進行轉換。 官方文件中有一個為模型加入搜尋結果摘要屬性的範例。對模型屬性的指派也可能改變要儲存的內容。請避免將帶有資料庫中不存在的顯示用屬性的模型直接 save() 的設計。

註冊會保留在查詢建構器上

每次呼叫 afterQuery() 都會新增一個 Closure,執行後註冊也不會消失。使用同一個建構器再次查詢時會再次執行,同一個作用域呼叫兩次,回呼也會被註冊兩次。複製(clone)建構器時,也會繼承已註冊的回呼。 在需要多次查詢的處理或長時間執行的 worker 中,請為每個用途建立新的查詢,並讓回呼只做沒有副作用的結果處理。若在其中放入寄送郵件或持久化操作,很容易把區塊或游標造成的多次執行誤認為業務上的一次處理。 在套件中提供可重複使用的作用域時,也請將支援的查詢方法明確列為公開 API,並測試型別與呼叫次數的契約。

測試不同查詢路徑的差異

在使用端的 Laravel 13 應用程式中,不只要確認鍵的處理,也要確認 first()、cursor() 與空頁面時的呼叫情況。 以下範例假設已有前面的 Post 模型,以及包含 id、title、body、created_at、updated_at 的 posts 資料表遷移。使用 RefreshDatabase 隔離測試之間的紀錄。 在 tests/Feature/AfterQueryTest.php 中建立測試。
可以只執行該測試。

維護時的檢查清單

  • 明確說明作用域支援的查詢方法,以及 Closure 的參數與回傳值型別。
  • 區分驗證 get() 的空結果與總筆數為 0 的 paginate()。
  • 對於區塊與游標,確認呼叫次數與查詢順序不會改變。
  • 設計時確保回呼重複註冊或建構器重複使用不會產生業務上的副作用。
  • 更新 Laravel 時,重新確認 get()、pluck()、cursor() 與 BuildsQueries 的呼叫路徑。

相關頁面

Eloquent 作用域

了解將 SQL 條件集中到區域作用域與全域作用域的基礎。

Eloquent 集合

了解處理模型的 Collection 與 Support Collection 的差異。

Eloquent Observers 與模型事件

了解與查詢結果處理不同、以模型為單位的事件。

API 資源

將回傳給 API 的顯示資料轉換與模型的儲存處理分離。

參考的一手資料

最後修改於 2026年11月6日