Laravel 專案長不大?資深工程師的『可演化架構』指南,告別義大利麵程式碼!

2025/07/15 | Laravel技術分享

Laravel 專案長不大?資深工程師的『可演化架構』指南,告別義大利麵程式碼!

嗨,我是浪花科技的 Eric。身為一個天天在 Code 海裡打滾的工程師,我看過太多專案從一開始的清爽 HelloWorld,隨著功能迭代,一路「歪」成一盤誰也不想碰的義大利麵。最常聽到的藉口就是:「哎呀,一開始沒想那麼多嘛!」這句話我聽了耳朵都快長繭了。其實,好的專案架構不是一開始就要搞得像蓋核電廠一樣複雜,而是要具備「演化」的能力。今天,我就來跟大家聊聊我心中理想的 Laravel 10 專案架構最佳實務,這套方法論能讓你的專案從一個小小的 MVP,順利成長為一頭能應付複雜業務的巨獸。

第一階段:純粹的 MVC – 快速驗證想法的利器

專案初期,尤其是 MVP (Minimum Viable Product) 階段,速度就是一切。這時候,Laravel 原生的 MVC 架構就是你最好的朋友。別聽信那些架構魔人說什麼一開始就要上 DDD、CQRS… 那叫過度設計(Over-engineering),是專案殺手。

在 MVC 階段,我們的邏輯很單純:

  • Model: 對應資料庫的資料表,處理 Eloquent 相關操作。
  • View: 就是你的 Blade 模板,負責呈現畫面。
  • Controller: 扮演交通警察的角色,接收 Request,呼叫 Model 拿資料,再把資料餵給 View。

這個階段,把商業邏輯直接寫在 Controller 裡是完全可以接受的。為什麼?因為業務邏輯還很簡單,團隊成員少,溝通成本低,快速實現功能、驗證市場才是首要目標。硬要分層只會拖慢開發速度。但,身為一個有遠見的工程師,你要知道這只是暫時的。

第二階段:Controller 肥胖症候群 – 災難的前兆

隨著專案功能越來越多,你會發現 Controller 開始「發福」。一個 `store` 方法可能要處理:

  • 複雜的 Request 驗證。
  • 上傳圖片並裁切、加上浮水印。
  • 建立主要訂單資料。
  • 更新商品庫存。
  • 寫入交易紀錄。
  • 發送 Email 通知給使用者。
  • 發送 Slack 通知給管理員。
  • 觸發一個 Job 進行後續分析。

當一個 Controller 方法超過一個螢幕的高度時,警報就該響起了。這就是典型的「肥 Controller」(Fat Controller),也是技術債開始滾雪球的徵兆。程式碼難以閱讀、難以維護、難以測試,而且商業邏輯散落在各個 Controller 中,無法複用。一個小小的需求變更,你可能要改好幾個地方,改完還沒信心,因為你不知道會不會改 A 壞 B。

第三階段:導入 Service Layer – 為商業邏輯找個家

為了解決 Controller 肥胖症,我們要引入第一個重要的概念:Service Layer (服務層)

Service Layer 的核心思想很簡單:將特定領域的商業邏輯,從 Controller 中抽離出來,封裝成一個獨立的類別。 這個類別不關心 HTTP Request 或 Response,它只專注於完成一項或多項相關的商業任務。

我們來把上面那個噁心的 `store` 方法動個手術。首先,在 `app` 目錄下建立一個 `Services` 資料夾,然後建立一個 `OrderService.php`。

改造前:肥胖的 `OrderController.php`

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreOrderRequest;
use App\Models\Order;
use App\Models\Product;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;

class OrderController extends Controller
{
    public function store(StoreOrderRequest $request)
    {
        // 1. 驗證 (由 FormRequest 處理了)
        $validated = $request->validated();

        // 2. 處理圖片 (假設有)
        $path = $request->file('product_image')->store('images');

        // 3. 核心商業邏輯
        $product = Product::findOrFail($validated['product_id']);
        if ($product->stock < $validated['quantity']) {
            return back()->withErrors(['msg' => '庫存不足']);
        }

        $order = Order::create([
            'user_id' => auth()->id(),
            'product_id' => $validated['product_id'],
            'quantity' => $validated['quantity'],
            'total_price' => $product->price * $validated['quantity'],
        ]);

        // 4. 更新庫存
        $product->decrement('stock', $validated['quantity']);

        // 5. 發送通知
        Mail::to(auth()->user())->send(new OrderPlaced($order));
        Notification::route('slack', '#orders')->notify(new NewOrderNotification($order));

        return redirect()->route('orders.show', $order)->with('success', '訂單已成立!');
    }
}

改造後:清爽的 `OrderController` 與專注的 `OrderService`

首先是 `OrderService.php`,它才是真正做事的人:

<?php

namespace App\Services;

use App\Events\OrderPlacedEvent;
use App\Models\Product;
use App\Models\User;

class OrderService
{
    public function placeOrder(User $user, array $data)
    {
        $product = Product::findOrFail($data['product_id']);

        // 檢查庫存
        if ($product->stock < $data['quantity']) {
            // 這裡可以拋出自訂的 Exception,讓 Controller 去接
            throw new \Exception('商品庫存不足');
        }

        // 使用資料庫交易確保資料一致性
        return \DB::transaction(function () use ($user, $product, $data) {
            $order = $user->orders()->create([
                'product_id' => $data['product_id'],
                'quantity' => $data['quantity'],
                'total_price' => $product->price * $data['quantity'],
            ]);

            $product->decrement('stock', $data['quantity']);

            // 使用事件來解耦通知邏輯
            event(new OrderPlacedEvent($order));

            return $order;
        });
    }
}

然後看看我們的主角 `OrderController.php`,是不是瘦身成功,看起來神清氣爽?

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreOrderRequest;
use App\Services\OrderService;

class OrderController extends Controller
{
    protected $orderService;

    // 使用依賴注入,讓 Controller 更具彈性
    public function __construct(OrderService $orderService)
    {
        $this->orderService = $orderService;
    }

    public function store(StoreOrderRequest $request)
    {
        try {
            $order = $this->orderService->placeOrder(
                $request->user(),
                $request->validated()
            );

            return redirect()->route('orders.show', $order)->with('success', '訂單已成立!');

        } catch (\Exception $e) {
            return back()->withInput()->withErrors(['msg' => $e->getMessage()]);
        }
    }
}

看到了嗎?Controller 現在只做三件事:接收請求、呼叫 Service、回傳響應。所有的商業邏輯都被封裝在 `OrderService` 中。未來如果 App 或 CLI 也需要下訂單,我們可以直接複用 `OrderService`,而不用重寫一遍邏輯。這就是分離關注點(Separation of Concerns)的威力!

第四階段:Repository Pattern – 讓資料庫操作更單純

當專案越來越複雜,你可能會發現 Service 裡面也混雜了大量的 Eloquent 查詢,特別是一些複雜的 `join` 或 `where` 條件。這時候,我們可以再往下拆分,引入 Repository Pattern (倉儲模式)

Repository 的職責非常單一:作為應用程式與資料來源(通常是資料庫)之間的中介層,專門處理資料的存取邏輯。 Service 不需要知道資料是從 MySQL、PostgreSQL 還是 Redis 來的,它只需要跟 Repository 說:「嘿,幫我找 ID 是 5 的訂單」,或是「幫我建立這筆新資料」。

為什麼需要 Repository?

  • 邏輯分離: 商業邏輯 (Service) 和資料存取邏輯 (Repository) 分開,程式碼更清晰。
  • 可測試性: 在測試 Service 時,你可以輕易地 Mock 一個 Repository,而不需要真的去碰資料庫。
  • 更換底層: 雖然不常見,但如果哪天你想把 Eloquent 換成其他的 ORM 或查詢建構器,你只需要改寫 Repository 的實作,而 Service 層完全不用動。

我們來建立一個 `OrderRepository`。在 `app` 下建立 `Repositories` 資料夾。

<?php

namespace App\Repositories;

use App\Models\Order;
use App\Models\User;

class OrderRepository
{
    public function createForUser(User $user, array $data): Order
    {
        return $user->orders()->create($data);
    }

    public function findById(int $id): ?Order
    {
        return Order::find($id);
    }

    // 甚至可以有更複雜的查詢
    public function getRecentOrdersWithProducts(User $user, int $limit = 10)
    {
        return $user->orders()
            ->with('product') // Eager Loading
            ->latest()
            ->take($limit)
            ->get();
    }
}

接著,我們的 `OrderService` 就可以注入 `OrderRepository` 來使用,讓自己變得更專注於「流程編排」,而不是實際的資料庫操作細節。

小囉嗦一下,我知道有些人會覺得 Repository 在 Laravel 中有點多餘,因為 Eloquent 本身已經很強大,像是一個 Active Record 模式的實現。我同意在中小型專案中,直接在 Service 中使用 Eloquent 是完全 OK 的。但當你的查詢邏輯變得非常複雜、需要在多個地方複用時,將它們抽到 Repository 中,絕對是個能提升程式碼品質的好習慣。

第五階段:Action/Domain – 當業務複雜到需要領域專家

對於絕大多數專案來說,`Controller -> Service -> Repository` 的分層已經非常夠用。但如果你的專案是那種業務邏輯極其複雜的企業級應用,你可能會發現 Service 變得越來越大,成為了新的「肥」點。這時候,可以考慮引入更細粒度的模式,例如 ActionDomain 的概念。

Action Class(或稱 Use Case)是一個只做「一件事」的小類別。例如,`PlaceOrderAction`、`CancelOrderAction`、`ApplyCouponAction`。每個 Action 都有一個公開的 `execute` 或 `handle` 方法。這讓你的商業意圖變得極度清晰,也更容易組合與測試。

這部分就比較進階了,算是對 Laravel 10 專案架構最佳實務 的一個延伸探討,我們點到為止。重點是理解架構演化的思想:不要一開始就用牛刀,但當牛出現時,你要知道去哪裡找牛刀。

結論:架構是演化而來,不是設計出來的

一個好的 Laravel 專案架構,就像蓋房子一樣,地基要穩,但不用一開始就把所有房間都蓋好。從簡單的 MVC 開始,快速前進;當 Controller 變胖時,用 Service 為它瘦身;當資料庫查詢變複雜時,用 Repository 隔離它們;當業務邏輯爆炸時,考慮用 Action 或更深入的 DDD 模式來拆解。這種漸進式的演化,才是應對軟體複雜性的最佳策略。

希望今天的分享,能幫助你擺脫維護義大利麵程式碼的惡夢。記住,好的架構是為了讓未來的你(和你的同事)感謝現在的你。

延伸閱讀

如果你們公司也有專案架構混亂、技術債纏身的問題,或是正在規劃新的專案,卻不知道如何設計一個能夠長久發展的穩固架構,別猶豫了!我們浪花科技團隊擁有豐富的 Laravel 專案開發與架構重構經驗,能幫助你從混亂中理出頭緒,打造出健壯、可維護、高效能的應用程式。立即填寫表單,讓我們聊聊你的專案吧!

常見問題 (FAQ)

Q1: 我的 Laravel 專案什麼時候才需要導入 Service Layer?

A1: 最簡單的判斷標準是:當你的 Controller 方法裡的程式碼開始變得冗長(例如超過 20-30 行),並且包含了不只一個商業邏輯步驟時,就是一個很好的時機。另一個指標是,當你發現某段商業邏輯需要在不同的 Controller(例如 Web Controller 和 API Controller)中重複使用時,就應該立刻將它抽離到 Service Layer。

Q2: Repository Pattern 在 Laravel 中是必須的嗎?Eloquent 不是很方便了嗎?

A2: 不是絕對必須的。對於中小型專案,直接在 Service 中使用 Eloquent 是非常常見且高效的做法。引入 Repository 的主要好處是當你的資料庫查詢變得非常複雜且需要被多個 Service 複用時,它可以幫助你統一管理這些查詢邏輯,並提高程式碼的可測試性。你可以把它當成一個「選項」,當專案複雜度達到一定程度時再考慮導入。

Q3: 一開始就把 Service 和 Repository 都建好,不是比較一勞永逸嗎?

A3: 這是一種常見的「過度設計」陷阱。在專案初期,業務需求尚未穩定,過早地建立過多的分層和抽象,反而會增加開發的複雜度和時間成本,導致開發速度變慢。最好的策略是「演化式架構」,從最簡單的 MVC 開始,隨著專案的複雜度增加,逐步引入 Service Layer、Repository 等模式,讓架構跟隨業務一同成長。

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