where 條件寫在 Controller 裡,跟寫在別的地方,差在哪?「查詢條件不就是一串 where 嗎?寫在 Controller 裡,寫在 Model 裡,結果不都一樣?」
結果確實一樣,但**「同一段查詢邏輯要不要在第二個地方重複用到」才是關鍵**。今天要看這個系統怎麼把「已發佈」「排序規則」這類會被多處引用的查詢條件,收斂成獨立的、可以互相組合的 class。
scopeXxx() 方法,還有 invokable class 這種寫法Laravel 官方文件教的 Query Scope 寫法,通常是在 Model 裡定義一個 scopeXxx() 方法:
// 傳統寫法
public function scopePublished(Builder $query): void
{
$query->where('published_at', '<=', now());
}
// 呼叫時
News::published()->get();
這個系統選擇了另一種寫法——把每個查詢條件寫成一個獨立的 invokable class(實作 __invoke() 方法),透過 tap() 套用到查詢上:
// app/Models/Scopes/News/Published.php
class Published
{
private Carbon $now;
public function __construct(?Carbon $now = null)
{
$this->now = $now ?? now();
}
public function __invoke(Builder $query): void
{
$query
->where('published_at', '<=', $this->now)
->where(function (Builder $query) {
$query
->where('published_until', '>=', $this->now)
->orWhereNull('published_until');
})
->where('published', true);
}
}
// 呼叫時
News::query()->tap(new Published)->get();
這個「已發佈」判斷邏輯,其實同時處理了三個條件:開始發佈時間到了、結束發佈時間還沒到(或沒有設結束時間)、發佈狀態旗標開著——不是單純一個布林欄位就能表達的邏輯。把它寫成獨立 class,換來幾個好處:
Published 的建構子接受一個 ?Carbon $now,測試時可以傳入固定時間,不用依賴系統當下時間,這是 scopeXxx() 方法比較難做到的。Models/Scopes/News/Published.php、Models/Scopes/Page/Published.php 各自獨立,即使兩個 Model 的「已發佈」定義不一樣,也不會互相干擾或混淆。真正的威力在於多個 scope 可以自由組合,不需要在 Model 裡預先定義好所有排列組合:
public function publishedAncestors(): AncestorsRelation
{
return $this->ancestors()
->tap(new Alias)
->tap(new Published)
->defaultOrder();
}
這裡同時疊了 Alias(處理別名頁面轉換)跟 Published(只要已發佈的)兩個 scope,兩者互不依賴、可以獨立測試,也可以在別的查詢裡只用其中一個。另一個常見的組合是排序:
// app/Models/Scopes/News/DefaultSort.php
class DefaultSort
{
public function __invoke(Builder $query): void
{
$query->orderByDesc('on_top')
->orderByDesc('sort_order')
->orderByDesc('published_at')
->orderByDesc('id');
}
}
「置頂優先、自訂排序優先、發佈時間新的優先」這組排序規則,只要 ->tap(new DefaultSort) 就能套用,不用在每個查詢新聞列表的地方重複寫一次四層 orderBy。
// Controller A
News::where('published_at', '<=', now())
->where('published', true)
->orderByDesc('on_top')
->orderByDesc('published_at')
->get();
// Controller B(同樣的條件,複製貼上一次)
News::where('published_at', '<=', now())
->where('published', true)
->orderByDesc('on_top')
->orderByDesc('published_at')
->get();
兩處查詢條件要是有一天邏輯要改(例如加入結束時間判斷),必須記得兩處都要改。
News::query()->tap(new Published)->tap(new DefaultSort)->get();
你的專案裡有沒有一段查詢條件(不只是單一 where,而是一組組合條件),在超過一個地方各自寫了一次?如果現在要幫它加一個新條件,你有把握每個地方都會記得改嗎?
scopeXxx() 方法,也可以寫成 invokable class,透過 tap() 套用Published class明天要細看 Page 身兼多職的其中一個關鍵技巧——用一個 backed enum,把資料庫欄位存的字串值轉成型別安全的查找表。