Laravel 專案從『玩具』變『航母』!資深工程師的『可演化架構』實戰藍圖 (2025 版)

2025/12/28 | Laravel技術分享, 全端與程式開發, 架構與效能優化

告別技術債!Laravel 專案的航母級架構演化指南

你的 Laravel 專案是否正走向「技術債地雷」?資深工程師 Eric 分享一套實戰可演化架構藍圖,解決 Controller 過肥、難以維護的困境。我們將從簡單的 MVC 出發,隨著專案複雜度逐步演化,引入 Service Layer、Repository 模式、乃至於 DTO 與 Action 模式。這份藍圖教你判斷「何時」升級,告別過度工程化。立即掌握從 MVP 到企業級系統的升級秘訣,讓你的程式碼乾淨、可測試,並確保你的專案能持續穩健航行!

需要專業協助?

聯絡浪花專案團隊 →

Laravel 專案從『玩具』變『航母』!資深工程師的『可演化架構』實戰藍圖 (2025 版)

嗨,我是浪花科技的 Eric。身為一個天天在 Code 海裡打滾的工程師,我看過太多專案從一開始的意氣風發,到最後變成誰都不想碰的「技術債地雷」。最常見的慘案現場?就是那個塞了幾千行程式碼、肥得跟豬一樣的 Controller。每次要加個小功能,都像在拆炸彈,改 A 壞 B,最後只能雙手一攤,跟 PM 說:「這塊動不了,要重寫。」

聽起來很熟悉嗎?別擔心,你不是一個人。尤其在 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。

記住,沒有所謂「最好」的架構,只有「最適合當下」的架構。一個好的工程師,不是一開始就蓋出一座羅馬,而是在專案的每個階段,都能做出最明智的架構決策。寫程式就像蓋房子,地基歪了,蓋再高都會倒。別為了趕工而犧牲架構,未來的你會感謝現在這個謹慎的你。

相關閱讀

希望這份藍圖對你有幫助。如果你正在為你的 Laravel 專案架構感到頭痛,或是正在規劃一個即將起飛的專案,卻不知道如何打好地基,歡迎與浪花科技的團隊聊聊。我們樂於分享更多實戰經驗,幫助你的專案航向偉大的航道!

常見問題 (FAQ)

Q1: 我應該一開始就用 Repository + Service + Action 的「完全體」架構嗎?

絕對不要!這是一個典型的過度工程化陷阱。在專案初期,過早引入複雜的抽象層只會拖慢開發速度,並帶來不必要的複雜性。請遵循「YAGNI (You Ain’t Gonna Need It)」原則,從最簡單的 MVC 開始,只有當專案的複雜度確實達到需要它們的程度時,再逐步重構、引入更進階的模式。

Q2: Laravel 11 的架構變得很精簡,這篇文章的建議還適用嗎?

當然,而且可能比以前更重要。Laravel 11 的極簡化骨架就像一塊乾淨的畫布,它給了開發者最大的自由度。這篇文章提供的正是在這塊畫布上,隨著專案成長,如何有條不紊地添加 `Services`、`Repositories`、`Actions` 等結構的指南。它教你「何時」以及「為何」要增加這些層次,而不是一開始就給你一個臃腫的樣板。

Q3: Service 和 Repository 到底有什麼差別?我常常搞混。

這是一個經典問題。一個簡單的區分方法:
Service (服務層) 處理的是「商業邏輯」或「應用程式的特定流程」。它回答的是「做什麼?」的問題。例如,一個 `OrderService` 可能會協調庫存檢查、計算總價、產生訂單、並發送通知,這是一個完整的業務流程。
Repository (倉儲層) 處理的是「資料存取邏輯」。它回答的是「如何存取資料?」的問題。它負責將你的應用程式與資料庫(或任何資料來源)解耦。一個 `OrderRepository` 只會提供如 `find($id)`、`create(array $data)`、`getPaidOrders()` 等與資料庫互動的方法,它不應該包含任何商業規則。

Q4: 什麼是 DTO (Data Transfer Object)?為什麼我需要它?

DTO 是一個純粹用來傳遞資料的物件。想像一下,你是不是常常在程式碼裡傳遞一個巨大的 `array` 或 `$request` 物件?這樣做的壞處是,你永遠不確定裡面到底有哪些鍵、值的型別是什麼,非常容易出錯且難以維護。
DTO 就是為了解決這個問題。它是一個強型別的類別,明確定義了需要傳遞的資料結構。使用 DTO 能讓你的程式碼更乾淨、更具可讀性、更容易測試,並且能像一份「契約」一樣,確保各層之間溝通的資料是標準化且可靠的。

 
立即諮詢,索取免費1年網站保固