HTTP QUERY 路由、queue:work 記憶體上限改用百分比、排程一行設定只在單台主機執行、部署時讓長任務安全收手,再加上 response()->markdown()。給企業後端與多主機部署的升級筆記。
做過商品搜尋或報表篩選的工程師,大概都遇過這種兩難:條件少的時候用 GET,網址乾淨、可以快取、語意正確;條件一多——品牌、價格區間、二十個規格勾選、排序、分頁——網址就長得像一首散文詩。改用 POST 嘛,語意又變成「我要改資料」,明明只是查詢而已。
Laravel 框架在 2026 年 10 月 6 日推出 v13.35.0,Laravel News 的 Paul Redmond 於 10 月 7 日整理重點。這一版的主角是 Route::query(),但真正讓維運團隊眼睛一亮的,是後面那一串和佇列、排程、部署有關的小改動。它們看起來不起眼,卻剛好打中多主機、容器化部署最常踩的坑。
Route::query():給「帶本文的查詢」一個正確的名字
Joel Butcher 把 Route::query() 帶進 Laravel 13。它為 HTTP QUERY 方法註冊路由。依 Laravel News 的說明,QUERY 是一種安全的方法,但可以攜帶請求本文,很適合篩選條件多到放不進 query string 的搜尋端點:
Route::query('/products/search', SearchProductsController::class);
背景是這樣的:Laravel 13.19 已經加入 Http::query() 以及測試用的 query()、queryJson() 輔助方法,但要定義路由,過去得寫成 Route::match(['QUERY'], ...)。現在 QUERY 正式進入路由器的動詞清單,所以 Route::any() 和 Route::redirect() 也會比對它。
特別注意 CSRF。 在 Laravel 13 中,CSRF 保護會用和 POST 相同的方式檢查 QUERY 請求。也就是說,放在 web 中介層群組裡的 QUERY 路由需要 CSRF token,否則就要加進 except 清單。前端呼叫時忘了帶 token,第一個回報的通常是客戶:「搜尋按了沒反應」。
實務上我們也建議:在正式採用 QUERY 之前,先確認你的 CDN、WAF、反向代理與 API 閘道是否允許這個方法通過。框架支援了,不代表路上的每一站都認得它。
queue:work --memory 終於可以用百分比
Jack Bayliss 讓 queue:work 的 --memory 選項可以接受 PHP memory_limit 的百分比,讓 worker 的上限跟著容器大小走,而不是寫死一個 MB 數字:
php artisan queue:work --memory=60%
官方舉例:如果 memory_limit 是 512M,worker 在某個 job 完成後、用量超過約 307 MB 時就會停止,交給程序管理器重啟。
這對容器化部署很實用。以前把 worker 從 1 GB 的機器搬到 512 MB 的容器,常常忘了改 --memory,結果不是太早重啟、就是撐到被系統砍掉。改成百分比之後,設定可以跟著資源規格自動縮放。
但有一個陷阱。 PHP CLI 的 memory_limit 常被設成 -1(不限制)。這時百分比會算出負數,worker 永遠不會因為記憶體而停止。所以使用百分比時,請務必為 worker 程序明確設定 memory_limit。
Schedule::alwaysOnOneServer():多台主機不再重複寄信
排程器在多台主機或多個副本上跑時,每個任務都會在每一台上執行,除非任務呼叫了 onOneServer()。這也是「客戶收到三封一模一樣的月報」的經典成因。
Jack Bayliss 的第三個 PR 新增 Schedule::alwaysOnOneServer(),一行就把 onOneServer() 套用到所有排程任務:
// AppServiceProvider::boot()
Schedule::alwaysOnOneServer();
過去的做法是把 routes/console.php 裡的每個任務包進 Schedule::onOneServer()->group()。兩個細節要記得:
- 沒有名稱的 closure 任務會被略過,因為
onOneServer()需要名稱來建立鎖。 - 所有主機必須共用同一個快取儲存(例如 Redis),鎖才有意義。
另外 schedule:list --json 的輸出現在也會顯示 onOneServer,方便用腳本檢查設定是否到位。
部署時,讓長任務在安全點收手
php artisan schedule:interrupt 可以停止 schedule:run 的子分鐘迴圈,但用 runInBackground() 啟動的指令過去根本收不到這個訊號。貢獻者 cyppe 把中斷改成儲存一個時間戳(和 queue:restart 的做法相同),並新增 Schedule::hasBeenInterruptedSince(),讓長時間指令可以在安全的檢查點自己停下來:
use Illuminate\Console\Scheduling\Schedule;
public function handle(Schedule $schedule)
{
$startedAt = now();
foreach ($this->batches() as $batch) {
if ($schedule->hasBeenInterruptedSince($startedAt)) {
return;
}
$this->process($batch);
}
}
schedule:run 也改用同樣的檢查,所以同一分鐘稍早發出的中斷,不會誤停在它之後才開始的執行。對每天跑大批次匯入、對帳或同步的企業系統來說,這代表部署時不必再賭「剛好沒有任務在跑」。
response()->markdown():給 AI 與工具一份乾淨的內容
Ashot Gharakeshishyan 為 response factory 新增 markdown() 方法,搭配既有的 $request->wantsMarkdown():
return $request->wantsMarkdown()
? response()->markdown($post->markdown)
: view('posts.show', ['post' => $post]);
它會回傳 text/markdown 內容類型的一般回應,也可以像 response()->make() 一樣帶狀態碼與標頭。對內容型網站來說,同一個網址可以依請求偏好,給人看的 HTML、給工具或 AI 代理讀的 Markdown,不必另外維護一套 API。
其他值得一提的改動
- 模型
defaults()下放到 13.x:Wes Hooper 以可選用的HasDefaultAttributestrait 帶入。defaults()的值優先於$attributes,而且是原始值,會略過 casts 與 mutators。用 trait 是為了避免影響已經有同名方法的模型。 - 假物件斷言接受屬性陣列:
Queue、Bus、Event、Notification的 fake 斷言可以直接傳入預期屬性;Eloquent 模型以is()比較,其餘用嚴格相等,屬性必須是 public。 - 本機磁碟的
lazy_root_creation:設為true時,根目錄等到第一次寫入檔案才建立。 - 一長串 Eloquent 修正:例如
chunkById()與lazyById()在主鍵有 cast 的模型上會無限迴圈的問題、schedule:list在跨越日光節約時間時的時區轉換等。
情境演練:一個電商後台的週一早上
把這些功能放進真實情境,會更容易看出價值。假設一個品牌電商後台部署在三台應用主機上,前面有負載平衡器,背後有 Redis 和資料庫:
- 早上六點,排程要產生前一天的銷售報表並寄給主管。以前如果某個任務忘了加
onOneServer(),主管會收到三封一樣的信,然後在群組裡問「系統是不是壞了」。啟用alwaysOnOneServer()後,只要快取共用、任務有名稱,就只會寄一封。 - 早上九點,行銷部要上新活動,工程師部署新版本。正好有一個商品同步指令在背景跑了二十分鐘。以前只能等它跑完,或硬生生中斷、事後補資料;現在指令在每個批次之間檢查
hasBeenInterruptedSince(),處理完手上那一批就乾淨收手;只要指令設計成可以續跑,下一輪排程就能接著做。 - 中午,促銷流量湧入,佇列裡塞滿訂單通知與發票開立。維運把 worker 容器從 1 GB 縮成 512 MB 以多開幾個副本,因為
--memory用的是百分比,不用逐一改設定。 - 下午,客服反映進階搜尋「條件選太多就壞掉」。團隊把搜尋端點改成
Route::query(),篩選條件放進請求本文,網址長度問題自然消失。
每一項都不是大改,但加起來,就是一個週一少掉四次救火。
升級前的檢查清單
- 全域搜尋
--memory=,決定哪些 worker 改用百分比,並確認 CLI 的memory_limit不是-1。 - 多主機部署先盤點排程:確認共用快取、替 closure 任務命名,再啟用
alwaysOnOneServer()。 - 長批次指令加上
hasBeenInterruptedSince()檢查點,並把schedule:interrupt寫進部署腳本。 - 想用 QUERY 路由,先確認 CSRF 與基礎設施:前端帶 token,CDN/WAF 放行該方法。
- 跑完整測試:這版修了不少 Eloquent 邊界情況,若你的程式「依賴」過舊行為,測試會告訴你。
你可能會問
Q:QUERY 可以完全取代 POST 搜尋嗎?
語意上更貼切,但請先確認前端、代理與監控工具都支援。過渡期可以兩者並存。
Q:百分比要設多少?
官方範例是 60%。實際數字要看單一 job 的記憶體峰值,建議先觀察監控數據再調整。
Q:alwaysOnOneServer() 和每個任務個別設定 onOneServer() 有什麼差別?
效果相同,差別在於不會漏掉。新增排程時不必記得每次都加,比較不容易因為人為疏忽而重複執行。
Q:response()->markdown() 適合哪些網站?
文件站、部落格、知識庫等內容型網站最適合。同一份內容可以同時服務讀者與自動化工具,維護成本最低。
Q:一定要升級嗎?
如果你有多主機排程、容器化 worker,或需要更好的部署收尾機制,這版很值得。其他專案可以排進例行的小版本更新。
結語:小版本,大大省掉半夜的電話
Laravel 13.35 沒有華麗的新架構,但它處理的都是讓維運人員半夜被叫醒的問題:重複寄出的信、被系統砍掉的 worker、部署時跑到一半的批次。再加上 QUERY 路由與 Markdown 回應,後端的語意也更乾淨了。
如果你的企業系統正在 Laravel 上跑排程、佇列與多台主機,我們很樂意協助規劃升級、調整 worker 設定與部署流程,讓下一次上線變成一件無聊的事——在維運的世界裡,無聊就是最高的讚美。
資料來源:Route::query() and Model defaults() in Laravel 13.35 — Laravel News(Paul Redmond,2026/10/07)