跳轉到主要內容
即使更新了套件,也要確保更新前分派的 Job 仍能被處理。Queue 中儲存的不是要執行的程式碼,而是 Job 類別的名稱與狀態。除了新程式碼的單元測試之外,還需要以新程式碼還原舊程式碼所儲存狀態的測試。
本頁以 Laravel 13 為對象,確認了 Framework v13.42.0 的實作。前提是已了解 Queue 的基礎與套件的測試。以下的相容性設計是套件端的方針,並不是 Laravel 自動遷移自訂 payload 格式的功能。

儲存與執行之間程式碼會改變

對於物件 Job,Queue::createObjectPayload() 會執行 serialize(clone $job),並將結果放入 payload 的 data.command。即使啟用加密,在那之前也會進行同樣的序列化。 worker 端的 CallQueuedHandler::getCommand() 會對已儲存的資料執行 unserialize(),並將還原後的 Job 交給 Dispatcher。一般的 handle() 會以當下 worker 正在執行的程式碼與服務容器來執行。 若沒有重新啟動長時間執行的 worker,更新後舊程式碼仍會持續運作。在滾動部署中,不只要考慮「舊分派端與新 worker」,也要考慮「新分派端與舊 worker」的組合。 判斷類別的重新命名或刪除時,不能只看使用者是否直接呼叫該類別。等待中、延遲中、等待重試的 Job,以及失敗 Job 的保存期間,也都屬於相容性的範圍。

SerializesModels 儲存的內容

Illuminate\Foundation\Queue\Queueable 在內部使用了 SerializesModels。它不只負責替換 model,也提供列舉 Job 本身屬性的 __serialize() 與還原值的 __unserialize()。

與宣告預設值相等的值不會被儲存

__serialize() 會排除 static、未初始化等屬性,並且當屬性目前的值與宣告預設值嚴格相等時也會省略儲存。
若這個屬性仍是 'csv',儲存的陣列中就不會包含 format。若之後的版本將宣告預設值改為 'json',舊 Job 也可能套用 'json'。這不是單純外觀上的預設值變更,而是讀取舊資料方式的變更。

還原時不會重新呼叫建構子

__unserialize() 會為目前類別中存在、且儲存陣列中也有對應鍵的屬性指派值。它不會執行透過建構子進行的輸入補全或驗證。
這個範例中的 'csv' 是建構子參數的預設值。它不是屬性本身的宣告預設值,因此在還原不含 format 的舊 payload 時不會補上這個值。若新增的具型別屬性維持未初始化狀態,之後存取時就會失敗。 此外,public 屬性的鍵就是名稱本身,protected 屬性的鍵帶有表示可見性的前綴,private 屬性的鍵還包含類別名稱。將屬性從 public 改為 private 的變更也要用舊 payload 驗證。
SerializesModels 列舉的是 Job 的屬性。不要認為任意 DTO 的內部或陣列深處的 model 也會被遞迴地進行同樣的轉換。傳遞 model 以外的物件時,這些類別及其儲存格式也屬於相容性的範圍。

將識別碼與執行服務分開

將執行所需的識別碼與選項儲存在 Job 中,而 API 用戶端或 Exporter 等執行服務則注入到 handle()。若將服務傳給建構子並保留下來,其內部狀態也可能成為儲存對象。 下面的範例為舊版中只持有 reportId 的同名 Job 加入格式選擇與 payload 版本。ReportExporter 是套件提供並 bind 到容器的契約,假設它擁有 export(string $reportId, string $format): void。
這個設計將缺少的值的意義與新分派的值分開。
  • 舊 payload 中沒有 format 時,使用宣告預設值 'csv'。
  • 舊 payload 中沒有 payloadVersion 時,視為宣告預設值 1。
  • 新建立 Job 時,建構子會將 payloadVersion 設為 2。由於與宣告預設值不同,這個值會被儲存。
  • 新 Job 的 format 為 'json' 時,該值也會被儲存。為 'csv' 時會被省略,因此今後也要維持宣告預設值 'csv'。
若將來只想變更新 Job 的預設格式,可以修改建構子參數的預設值與指派,並與用於舊資料的屬性預設值分開考量。payload 版本不是套件的發行版本號,而是作為儲存格式及其解讀的世代來管理。
只加入版本號並不能獲得相容性。本範例的新版也能處理舊格式,但無法保證舊版的 worker 能理解新的 format 或 payloadVersion。還需要後文所述的分派端與 worker 的更新順序。

傳遞 model 時要與快照區分

將 model 直接儲存在屬性中時,SerializesAndRestoresModelIdentifiers 會將其替換為包含類別、ID、連線名稱、已載入關聯名稱等資訊的 ModelIdentifier。還原單一 model 時,會透過 firstOrFail() 從資料庫重新取得,並載入目標關聯。 這並不是固定分派當下屬性或關聯資料的機制。
  • 若記錄在執行前被更新,還原後參照的是更新後的內容。
  • 事先套用在關聯上的篩選條件不會被儲存,因此需要在執行時重新加上必要的條件。
  • 不需要的關聯可以透過 withoutRelations() 或 #[WithoutRelations] 排除在儲存對象之外。
  • 若 model 已被刪除,Job 可能會在 handle() 之前的還原階段失敗。
若可以捨棄已刪除 model 的 Job,可以考慮使用官方的 #[DeleteWhenMissingModels]。不過,套件不應擅自將捨棄設為預設行為,而應向使用者公開失敗方針。若採用只傳遞 ID 並在 handle() 內查詢的設計,則由自己控制該查詢的失敗或略過。 若需要分派當下的內容,就明確儲存所需的值,或將不可變的報表請求等記錄到資料庫並傳遞其 ID。要區分單純傳遞 model 與儲存業務資料快照這兩件事。

提交後分派是另一個問題

處理在交易中建立的記錄時,要確保 worker 不會在提交之前開始讀取。
afterCommit() 會等到開啟中的父交易提交後再分派,回滾時則捨棄該分派。它並不是解決類別名稱、屬性相容性或外部處理重複執行的功能。

將 fake 與舊 payload 的測試分開

一般的 Queue::fake() 用於記錄分派。光是 assertPushed() 通過,並不代表已確認儲存、還原,或新版對舊 payload 的處理。 目前類別的往返可以這樣驗證。
serializeAndRestore() 會在 fake 內執行 serialize() 與 unserialize()。但它不會驗證對實際後端的分派、worker 的執行以及舊版程式碼的產生。

保留以舊版產生的 fixture

在舊版的 GenerateReport 只持有 reportId 的狀態下,產生不含實際資料的固定 Job。以下是在舊版的測試環境中執行一次的準備程式碼。tests/fixtures/ 需事先準備好。
將 fixture 納入版本控制,並在新版的測試中讀取。若每次測試都用新版重新產生,就不再是向後相容性的驗證,因此要保持固定。
本範例中的 TestCase 是在套件的測試環境中準備的基底類別。傳給 unserialize() 的只能是自己管理的 fixture,不要傳入外部輸入。 此外,也要以測試用的 Exporter 執行還原後舊 Job 的 handle(),驗證到輸出與儲存結果為止。傳遞 model 時,要另外驗證更新、刪除、連線與關聯的重新取得;在實際使用的 Queue 連線上,也要另外驗證提交後分派與重試。

在分派新格式之前先更新讀取端

1

準備能讀取舊格式的版本

先發佈維持舊類別名稱與舊值解讀、同時也能讀取新格式的程式碼。用 fixture 不只確認還原,也確認處理結果。
2

確認沒有殘留的舊 worker

在使用端應用程式的部署流程中加入 worker 的替換。對於一般的 queue:work,使用 php artisan queue:restart 要求在處理完目前的 Job 後結束,並確認行程管理器啟動了新版。重新啟動訊號會使用快取。
3

啟用新格式的分派

在讀取端完成支援後,再透過功能旗標等切換到新格式的分派。若光是發佈同一份程式碼就會開始產生新格式,就需要暫停分派,或將新舊 Queue 與對應的 worker 分開等,確保不會交給舊 worker 的流程。
4

在重試與回滾期間也要持續維護

也要確認失敗 Job 的重試,以及在 Job 鏈中稍後分派的 Job。若在儲存新格式後回退到舊程式碼,要驗證舊程式碼也能讀取。不要只因為 Queue 曾經清空過一次就移除對舊類別或舊格式的支援。
queue:restart 並不是改寫已儲存 payload 的指令。在套件的更新說明中,要寫明支援的格式、所需的 worker 替換以及廢除舊格式的條件。同樣的變更也要記錄在版本相容性管理與 Changelog 的運用中。

參考的第一手資料

最後修改於 2026年11月12日