國內外的 Laravel 社群,主流的架構建議是使用 Service 類別和 Repository 類別來組織原始碼。
然而,我發現許多團隊在實施這個架構時會遇到兩個問題。
第一個問題是,隨著時間推移,程式碼會散落各處,無法明確區分哪些應該放在 Service、哪些應該放在 Repository、哪些應該放在 Controller。邊界變得模糊,導致整體可維護性下降。
第二個問題是,「檔案數量」的成長速度趕不上「程式碼數量」的成長速度。
如果你的 Service、Repository 數量是每個月增加兩三個,但是你程式碼行數是每個星期增加兩三百行,那開發兩三年之後當然會有檔案肥大的問題。
舉例來說,如果你們公司有串金流功能的話,那我猜你的 OrderService 類別應該就超過一千行了,然後跟這個類別有關的程式碼也變得很難維護。
所以我認為 Service 加 Repository 這個架構並不適合用在 Laravel 開發。
最近在設計新架構時,我受到 GraphQL 的啟發,試著把每件事單獨放在一個 Mutation 或 Query 中,結果出乎意料地好。
今天想與大家分享這種「扁平式」新架構:Laravel Flat Architecture
(注意:此架構受到 GraphQL Query/Mutation 概念的啟發,但它不是 GraphQL)
這個架構非常簡單,我們只需要安裝 https://github.com/lorisleiva/laravel-actions
接著在 app 資料夾建立 Mutations 和 Queries 資料夾。
在開發新功能時:
我直接把我一個專案(藝文活動搜集行事曆)的部分檔案開源給大家參考:
當你這樣整理程式碼,Controller 就會變得非常簡潔優雅:
class MyEventController extends Controller
{
function index(Request $request)
{
$events = \App\Queries\GetUserEventsWithPagination::run(Auth::user()->id);
return view('my-events.index', compact('events'));
}
function edit($id, Request $request)
{
$event = \App\Queries\GetEvent::run($id);
if ($event->user->id != Auth::user()->id) {
abort(403);
}
return view('add-event', compact('event'));
}
function update($id, Request $request)
{
$event = \App\Queries\GetEvent::run($id);
if ($event->user->id != Auth::user()->id) {
abort(403);
}
$imageFileName = null;
if ($request->hasFile('image')) {
$imageFileName = handleImage($request);
}
$event = \App\Mutations\UpdateEvent::run(
event: $event,
title: $request->input('name'),
description: $request->input('description'),
city: $request->input('city'),
url: $request->input('url'),
placeKey: $request->input('place_name'),
category: $request->input('category'),
fromDate: $request->input('from'),
toDate: $request->input('to'),
inDates: explode(',', $request->get('in_dates')),
imageFileName: $imageFileName,
);
return redirect()->to('/my-events')->with('status', '活動修改成功');
}
function destroy($id, Request $request)
{
$event = \App\Queries\GetEvent::run($id);
if ($event->user->id != Auth::user()->id) {
abort(403);
}
\App\Mutations\DeleteEvent::run($event);
return redirect()->to('/my-events')->with('status', '活動刪除成功');
}
}
更多範例檔案與說明,可以參考這邊:https://github.com/howtomakeaturn/laravel-flat-architecture
這個架構雖然看起來簡單,但其實有很多好處。
團隊有初級開發者時,他們通常會有過度抽象化、做出錯誤抽象化的情況出現,讓專案越來越難維護。
Laravel Flat Architecture 會讓專案呈現扁平,Mutations 與 Queries 內可能有數百個檔案,但每個檔案不會超過兩三百行。
這樣的架構避免了 God Object 這種反模式,開發速度快且抽象化簡單。
在審查同事的 PR 做 Code Review 時也很簡單,只要看看 Controller 內容是否單純優雅即可。
如果 Controller 呼叫了 Eloquent 預設的 save()、delete()、where() 等邏輯,就表示邏輯洩漏到 Controller 裡了。
這時請對方重構到 Mutations/Queries 類別即可。
採用這個扁平架構,會讓自動化測試的檔案結構同時變得很簡單。
一個 Mutation 對應一個測試、一個 Query 對應一個測試即可!
此外,你還可以寫一個簡單的覆蓋率測試。
無論使用 PHPUnit 還是任何工具都可以輕鬆實現。
只需要掃描 Mutations 與 Queries 資料夾,確認每個檔案在 tests 資料夾中是否有對應的測試檔案即可。
同事發的 PR 就算能用,通常在抽象化設計與架構方面需要跟大家一起討論、沒辦法自動化架構檢查。
然而 Laravel Flat Architecure 這個架構因為非常簡單,因此你可以在本地端或 CI/CD 環境中使用工具檢查它。
舉例來說,我這邊附上一份 PHPStan 的設定檔,裡面會檢查你的 Controller 是否出現 save、delete、where,如果有就會擋掉。
includes:
- vendor/larastan/larastan/extension.neon
- vendor/spaze/phpstan-disallowed-calls/extension.neon
parameters:
level: 0
paths:
- app/Http/Controllers
disallowedMethodCalls:
- method: Illuminate\Database\Eloquent\Model::save
message: "❌ 禁止在 Controller 內直接使用 save(),請使用 Mutations!"
- method: Illuminate\Database\Eloquent\Model::update
message: "❌ 禁止在 Controller 內直接使用 update(),請使用 Mutations!"
- method: Illuminate\Database\Eloquent\Model::delete
message: "❌ 禁止在 Controller 內直接使用 delete(),請使用 Mutations!"
- method: Illuminate\Database\Eloquent\Builder::where
message: "❌ 禁止在 Controller 內直接使用 where(),請使用 Queries!"
- method: Illuminate\Database\Eloquent\Builder::find
message: "❌ 禁止在 Controller 內直接使用 find(),請使用 Queries!"
- method: Illuminate\Database\Eloquent\Builder::first
message: "❌ 禁止在 Controller 內直接使用 first(),請使用 Queries!"
- method: Illuminate\Database\Eloquent\Builder::get
message: "❌ 禁止在 Controller 內直接使用 get(),請使用 Queries!"
如果新同事不清楚架構,可以將這個工具裝到 CI/CD 裡,這樣他們在 push 時就會亮紅燈,無法 merge。
為了避免檔案過於肥大,我認為採用扁平化的架構比較好,而且這種架構不會妨礙進一步的抽象化。
例如,如果多個 Mutations 有共用邏輯,可以使用 OOP 方式建一個 Parent 類別,讓其他 Mutation 繼承它。若喜歡 Composition,也可以使用 Trait 將某些動作單獨成檔案,讓其他 Mutation 直接 use 也可以。
總而言之,在規格不斷變化、急著開發新功能、沒時間深入設計抽象化時,我鼓勵使用扁平的架構來當第一步。然後在規格明確、心有餘力的時候,再好好設計更進階的抽象化,這樣更有利於長期開發。
我之後預計會分享關於 Laravel Flat Architecture 的更多內容,有興趣者可以參考:https://codelove.tw/@howtomakeaturn/post/qBgOdx