Laravel 與後端開發

搜尋條件長到塞不進網址?Laravel 13.35 送上 Route::query() 和一整籃維運小禮物

Laravel News 於 2026/10/07 整理 Laravel 13.35:新增 Route::query() 支援 HTTP QUERY 方法、queue:work 的 --memory 可用 PHP memory_limit 的百分比、Schedule::alwaysOnOneServer() 讓所有排程只在一台主機執行、長時間排程指令可感知 schedule:interrupt,以及 response()->markdown() 等。本文說明它們解決的實際問題與升級注意事項。

暖橘色調扁平插畫:帶笑臉的放大鏡抱著一個包裹,旁邊是三台伺服器,最上面一台戴著皇冠,右上方有記憶體儀表,下方輸送帶載著佇列任務方塊。
搜尋條件長到塞不進網址?Laravel 13.35 送上 Route::query() 和一整籃維運小禮物

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()。兩個細節要記得:

  1. 沒有名稱的 closure 任務會被略過,因為 onOneServer() 需要名稱來建立鎖。
  2. 所有主機必須共用同一個快取儲存(例如 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 以可選用的 HasDefaultAttributes trait 帶入。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(),篩選條件放進請求本文,網址長度問題自然消失。

每一項都不是大改,但加起來,就是一個週一少掉四次救火。

升級前的檢查清單

  1. 全域搜尋 --memory=,決定哪些 worker 改用百分比,並確認 CLI 的 memory_limit 不是 -1。
  2. 多主機部署先盤點排程:確認共用快取、替 closure 任務命名,再啟用 alwaysOnOneServer()。
  3. 長批次指令加上 hasBeenInterruptedSince() 檢查點,並把 schedule:interrupt 寫進部署腳本。
  4. 想用 QUERY 路由,先確認 CSRF 與基礎設施:前端帶 token,CDN/WAF 放行該方法。
  5. 跑完整測試:這版修了不少 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)

← 返回技術文章