Laravel 專案從『玩具』變『航母』!『可演化架構』實戰藍圖 (2025 版)
☰ 目錄 table-of-contents.md
一個塞了三千行的 Controller,到底是怎麼長出來的?沒有人一開始就打算寫爛,它是在一次次「先動再說」的需求追加中慢慢肥大,最後變成改 A 壞 B、誰都不敢碰的技術債地雷。這篇我想把這幾年整理出的「可演化架構」藍圖攤開來談:怎麼讓 Laravel 專案從小玩具一路長成航母,而不是長成炸彈。
聽起來很熟悉嗎?別擔心,你不是一個人。尤其在 Laravel 10 之後,特別是 Laravel 11 帶來了更精簡的預設架構,這既是祝福也是詛咒。對於新手或小型專案,它清爽、快速;但對於需要長期維護、功能會不斷擴增的專案來說,這個「極簡風」的起點,很可能就是未來義大利麵程式碼的溫床。
所以今天,我不想跟你談那些虛無飄渺的理論。我想給你一份實戰藍圖,一份關於 Laravel 10 專案架構最佳實務 的演化指南。我們的核心思想很簡單:架構不是一次到位,而是隨著專案需求『演化』的生命體。我們會從一個最簡單的 Laravel 專案開始,一步步看著它『長大』,並在對的時機,為它引入對的架構模式。
階段一:『輕裝上陣』- Laravel 預設 MVC 架構的黃金時期
當你剛開始一個新專案、做一個 MVP (Minimum Viable Product),或是功能單純的後台時,請務必克制你想「秀一手」高超架構設計的衝動。這個階段,過度工程化 (Over-engineering) 才是你最大的敵人。
簡單的美好:為什麼你該擁抱預設 MVC
直接在 Controller 裡面寫商業邏輯、用 Eloquent ORM 處理資料庫操作,這在專案初期完全沒問題!它的好處顯而易見:
- 開發快速: 不用建立一堆檔案和目錄,直奔主題,快速實現功能。
- 直觀易懂: 程式碼的流動路徑很清晰,從 Route 到 Controller 再到 View,一目了然。
- 學習曲線平緩: 對於團隊新成員來說,這是最容易上手的結構。
舉個例子,一個簡單的建立文章功能,在 Controller 裡可能是這樣:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// 業務邏輯:檢查是否有敏感詞 (假設有個 helper function)
if (contains_sensitive_words($validated['body'])) {
return back()->withErrors(['body' => '內容包含敏感詞彙']);
}
$post = Post::create([
'title' => $validated['title'],
'body' => $validated['body'],
'user_id' => auth()->id(),
]);
// 觸發一個通知
// NotifiableUser::find(1)->notify(new NewPostNotification($post));
return redirect()->route('posts.show', $post);
}
}
你看,很乾淨,不是嗎?但魔鬼藏在細節裡。當業務邏輯開始膨脹時,警訊就來了。
警訊:何時該告別『輕裝』時代?
當你發現以下情況時,就代表你的 Controller 快要「過勞」了,是時候進行第一次架構升級了:
- 一個 Controller method 超過 50 行,你需要捲動好幾次才看得完。
- 同樣的資料庫查詢邏輯(例如,撈取熱門文章)在好幾個不同的 Controller 裡重複出現。
- 一個動作(例如建立文章)牽涉到的不只是儲存資料,還包含發送通知、清除快取、呼叫外部 API 等等。
- 商業邏輯變得複雜,充滿了 if-else 判斷,難以閱讀和測試。
階段二:『職責分離』- 導入 Service Layer 與 Form Request
這是專案成長的第一個關鍵轉捩點。我們要做的,就是把「商業邏輯」和「HTTP 層的處理」分開。Controller 的工作應該很單純:接收 Request,呼叫對應的服務,然後回傳 Response。其他的,它一概不管。
為什麼是 Service Layer,而不是 Repository?
很多工程師在這裡會糾結,到底該用 Service 還是 Repository?我的建議是:優先導入 Service。因為你當前面臨最大的痛點是「商業邏輯無處安放」,而不是「資料存取方式複雜」。
- Service 層: 負責處理應用程式的商業邏輯、業務流程。它可以協調多個 Model、呼叫外部服務,完成一個特定的「使用案例」(Use Case)。
- Repository 層: 負責將資料存取邏輯(如 Eloquent 查詢)抽象化。它的主要目的是隔絕商業邏輯與資料來源的細節。
我們先解決最痛的問題。建立一個 `app/Services` 資料夾,然後把剛剛的邏輯搬進去。
實戰:打造你的第一個 Service
<?php
namespace App\Services;
use App\Models\Post;
use Illuminate\Support\Facades\Notification;
class PostService
{
public function createPost(array $data, int $userId): Post
{
// 業務邏輯:檢查是否有敏感詞
if (contains_sensitive_words($data['body'])) {
// 在 Service 層可以拋出更明確的 Exception
throw new \InvalidArgumentException('內容包含敏感詞彙');
}
$post = Post::create([
'title' => $data['title'],
'body' => $data['body'],
'user_id' => $userId,
]);
// 觸發一個通知
// NotifiableUser::find(1)->notify(new NewPostNotification($post));
return $post;
}
}
表單驗證的守門員:Form Request
接著,我們用 Laravel 內建的 Form Request 功能,把驗證邏輯也從 Controller 抽離。
php artisan make:request StorePostRequest
然後 Controller 就會變得非常清爽:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest; // <-- 使用 Form Request
use App\Services\PostService; // <-- 注入 Service
class PostController extends Controller
{
protected $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function store(StorePostRequest $request) // <-- 驗證交給 Form Request
{
try {
$post = $this->postService->createPost(
$request->validated(),
$request->user()->id
);
} catch (\InvalidArgumentException $e) {
return back()->withErrors(['body' => $e->getMessage()]);
}
return redirect()->route('posts.show', $post);
}
}
是不是舒服多了?Controller 現在只做三件事:驗證輸入、呼叫 Service、導向頁面。這就是「關注點分離」(Separation of Concerns)。
階段三:『數據抽象』- Repository 模式的登場時機
當你的專案持續擴大,你可能會發現,某些複雜的資料庫查詢開始在不同的 Service 裡重複出現。或者,你開始需要從不同的來源(例如資料庫、快取、外部 API)獲取同類型的資料。這時候,Repository 模式就該登場了。
Repository 不是萬靈丹!你真的需要它嗎?
囉嗦一下,千萬不要為了用 Repository 而用。如果你的專案只是簡單的 CRUD,直接在 Service 裡用 Eloquent 就好。引入 Repository 的最佳時機是:
- 複雜查詢的複用: 例如,「獲取過去 7 天內,留言數超過 50 且至少有 1 個精選留言的熱門文章」。這種查詢邏輯如果散落在各處,會是維護的惡夢。
- 多重資料來源: 需要整合從資料庫、Redis 快取、甚至是 Algolia 搜尋服務來的資料。Repository 可以作為一個統一的介面,隱藏背後的複雜性。
- 為了可測試性: 想要在測試 Service 時,能夠輕易地模擬 (Mock) 資料來源,而不是真的去讀寫資料庫。
實戰:從 Service 呼叫 Repository
我們會先定義一個 Interface (合約),這是 Repository 模式的精髓,它讓我們的程式碼依賴於「抽象」而非「實作」。
<?php
namespace App\Repositories\Contracts;
interface PostRepositoryInterface
{
public function create(array $data): \App\Models\Post;
public function findHotPosts(int $days, int $commentThreshold);
}
然後是 Eloquent 的具體實作:
<?php
namespace App\Repositories\Eloquent;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
class EloquentPostRepository implements PostRepositoryInterface
{
public function create(array $data): Post
{
return Post::create($data);
}
// ... 其他實作
}
最後,在 `PostService` 中,我們注入 `PostRepositoryInterface`,而不是直接使用 `Post` Model。
class PostService
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function createPost(array $data, int $userId): Post
{
// ... 業務邏輯 ...
$postData = array_merge($data, ['user_id' => $userId]);
$post = $this->postRepository->create($postData);
// ... 其他業務邏輯 ...
return $post;
}
}
如此一來,我們的 Service 完全不知道資料是怎麼存的,它只知道呼叫 `create` 方法。未來如果我們要換成用 MongoDB 存,只需要寫一個 `MongoPostRepository` 並在 Service Provider 裡綁定新的實作,Service 層的程式碼完全不用動!
階段四:『終極解耦』- Action 模式與 DTOs 的威力
對於大型、複雜的企業級應用,有時候連 Service 都會變得臃腫。一個 `UserService` 可能要處理註冊、登入、更新個人資料、上傳頭像、重設密碼... 最後變成另一個上帝物件 (God Object)。
當 Service 也變胖時:Action 模式來救駕
Action 模式的核心思想是:一個類別只做一件事。每個 Action 都是一個獨立、可執行的單元。
使用 Action 的好處:
- 高內聚、低耦合: 每個 Action 的職責都非常單一,易於理解、修改和測試。
- 可重用性: 你可以在 Controller、Command、甚至是 Job 裡重複使用同一個 Action。
- 程式碼結構清晰: `app/Actions` 目錄下一目了然,直接反映了系統具備的所有功能。
數據的標準契約:DTO (Data Transfer Objects)
在進入 Action 之前,我強烈建議搭配使用 DTO。DTO 是一個簡單的物件,專門用來在不同層級之間傳遞資料。它能確保傳遞的資料結構清晰、型別正確,告別噁心的巨大陣列 (array)。`spatie/laravel-data` 這個套件是實現 DTO 的絕佳選擇。
實戰:將『建立文章』重構為 Action
首先,定義一個 DTO:
<?php
namespace App\Data;
use Spatie\LaravelData\Data;
class PostData extends Data
{
public function __construct(
public string $title,
public string $body,
public int $user_id
) {}
}
然後是我們的 Action:
<?php
namespace App\Actions\Posts;
use App\Data\PostData;
use App\Models\Post;
use Lorisleiva\Actions\Concerns\AsAction;
class CreatePostAction
{
use AsAction;
public function handle(PostData $postData): Post
{
// 這裡可以注入 Repository 或直接用 Model
return Post::create($postData->toArray());
}
}
最後,Controller 瘦到只剩一行核心程式碼:
<?php
namespace App\Http\Controllers;
use App\Actions\Posts\CreatePostAction;
use App\Http\Requests\StorePostRequest;
use App\Data\PostData;
class PostController extends Controller
{
public function store(StorePostRequest $request, CreatePostAction $createPost)
{
// 從 Request 建立 DTO
$postData = PostData::from(array_merge($request->validated(), [
'user_id' => $request->user()->id
]));
// 執行 Action
$post = $createPost->handle($postData);
return redirect()->route('posts.show', $post);
}
}
至此,我們的架構已經高度模組化、可測試且易於維護了。從最初的幾十行程式碼在一個 Controller 裡,演化成各司其職的 Form Request, DTO, Action, Repository,這就是一個專案從『玩具』走向『航母』的過程。
總結:你的專案在哪個階段?
我們回顧一下這趟演化之旅:
- 階段一: 快速迭代的 MVP,使用預設 MVC。
- 階段二: 業務邏輯變複雜,導入 Service Layer 和 Form Request。
- 階段三: 資料存取邏輯變複雜,導入 Repository 模式。
- 階段四: 應用程式規模龐大,導入 Action 模式和 DTOs。
記住,沒有所謂「最好」的架構,只有「最適合當下」的架構。一個好的工程師,不是一開始就蓋出一座羅馬,而是在專案的每個階段,都能做出最明智的架構決策。寫程式就像蓋房子,地基歪了,蓋再高都會倒。別為了趕工而犧牲架構,未來的你會感謝現在這個謹慎的你。
相關閱讀
- 肥 Controller 瘦不下來?Laravel 後台架構終極對決:Repository vs. Service vs. Action 模式,資深工程師帶你選對屠龍刀!
- Laravel 專案長不大?資深工程師的『可演化架構』指南,告別義大利麵程式碼!
- 你的 Laravel 專案是技術債炸彈還是傳世藝術品?Laravel 10 專案架構最佳實務指南
希望這份藍圖對你有幫助。如果你正在為你的 Laravel 專案架構感到頭痛,或是正在規劃一個即將起飛的專案,卻不知道如何打好地基,歡迎與浪花科技的團隊聊聊。我們樂於分享更多實戰經驗,幫助你的專案航向偉大的航道!
常見問題
小型 Laravel 專案或 MVP 該用什麼架構?
Laravel 專案出現哪些警訊時該開始做架構升級?
Laravel 架構升級時,該先導入 Service 還是 Repository?
導入 Service 層和 Form Request 後,Controller 應該只做什麼?
訂閱免費電子報
把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。