肥 Controller 瘦不下來?Laravel 後台架構終極對決:Repository vs. Service vs. Action 模式,資深工程師帶你選對屠龍刀!

2025/12/27 | Laravel技術分享, 全端與程式開發

告別義大利麵:Laravel 專案的 Controller 瘦身戰略與架構選擇

您的 Laravel Controller 是否已成為無所不包的「義大利麵」?肥大的 Controller 是技術債利息最高的債主,嚴重違反了單一職責原則。本文將帶您解析資深工程師用於拯救專案的三把「屠龍刀」:專責資料庫溝通的 Repository 模式、協調複雜業務流程的 Service Layer,以及追求單一職責極致的 Action 模式。透過實戰重構,我們展示了如何將驗證、商業邏輯、資料庫操作完美分層,打造出清爽且高度可測試的程式碼。別再讓難以維護的程式碼拖垮您的開發速度!立即掌握這些架構精髓,為您的專案選對武器,徹底根除 Controller 臃腫的惡夢!

需要專業協助?

聯絡浪花專案團隊 →

肥 Controller 瘦不下來?Laravel 後台架構終極對決:Repository vs. Service vs. Action 模式,資深工程師帶你選對屠龍刀!

嗨,我是浪花科技的 Eric。身為一個整天跟程式碼打交道的工程師,我看過太多 Laravel 專案從一開始的清爽簡潔,到幾個月後變成一團難以收拾的『義大利麵』。而這場災難的起點,通常都來自一個地方:一個越來越肥、無所不包的 Controller。

一開始,你可能只是在 Controller 裡加個幾行驗證、存個資料庫,感覺很方便。但隨著功能越來越複雜,你會發現 Controller 裡開始出現商業邏輯、資料庫查詢、發送 Email、觸發事件…等等。很快地,一個方法就長達數百行,修改一個小功能,就像在拆炸彈,深怕動到哪裡就引爆了整個系統。這就是所謂的「技術債」,而肥 Controller 正是利息最高的那個債主。

今天,我不想跟你談太多空泛的理論,我想直接帶你上戰場,解析三種在 Laravel 世界裡最常被用來『減肥』的架構模式:Repository、Service 和 Action。它們不是互斥的,而是可以協同作戰的武器。搞懂它們各自的守備範圍,你才能為你的專案選對『屠龍刀』,打造出可維護、可擴展、甚至可以傳承的程式碼藝術品。

肥 Controller 的原罪:當交通警察開始兼差當法官

想像一下,Controller 的原始職責應該像個交通警察。它站在路口,接收請求(Request),確認證件(Validation),然後指揮車輛(Data)到該去的地方(Model 或下一個處理層),最後給予回應(Response)。它的工作很單純:『協調與轉發』。

但肥 Controller 的問題在於,這個交通警察不但指揮交通,還當場審判起案件(執行商業邏輯)、調查戶口(複雜的資料庫查詢)、甚至親自送信(發送通知)。這嚴重違反了軟體設計中的『單一職責原則』(Single Responsibility Principle, SRP),導致程式碼的職責混亂、高度耦合,最後變成一場維護惡夢。

災難現場:一個典型的『義大利麵』Controller

讓我們來看一個血淋淋的例子。這是一個建立訂單的 store 方法,是不是很眼熟?

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Events\OrderCreated;

class OrderController extends Controller
{
    public function store(Request $request)
    {
        // 1. 驗證
        $validated = $request->validate([
            'user_id' => 'required|exists:users,id',
            'products' => 'required|array',
            'products.*.id' => 'required|exists:products,id',
            'products.*.quantity' => 'required|integer|min:1',
        ]);

        $totalPrice = 0;
        $orderItemsData = [];

        // 2. 商業邏輯:計算總價與檢查庫存
        foreach ($validated['products'] as $productData) {
            $product = Product::find($productData['id']);
            if ($product->stock < $productData['quantity']) {
                return response()->json(['message' => $product->name . ' 庫存不足'], 400);
            }
            $totalPrice += $product->price * $productData['quantity'];
            $orderItemsData[] = [
                'product_id' => $product->id,
                'quantity' => $productData['quantity'],
                'price' => $product->price,
            ];
        }

        // 3. 資料庫操作:建立訂單與訂單項目、更新庫存
        $order = Order::create([
            'user_id' => $validated['user_id'],
            'total_price' => $totalPrice,
            'status' => 'pending',
        ]);

        $order->items()->createMany($orderItemsData);

        foreach ($validated['products'] as $productData) {
            $product = Product::find($productData['id']);
            $product->decrement('stock', $productData['quantity']);
        }

        // 4. 額外操作:發送 Email & 觸發事件
        Mail::to($request->user())->send(new \App\Mail\OrderConfirmation($order));
        event(new OrderCreated($order));

        return response()->json($order, 201);
    }
}

看到了嗎?驗證、商業邏輯、多個資料庫操作、發送郵件、觸發事件…全都擠在一起。這段程式碼有幾個致命傷:

  • 難以測試: 你要怎麼測試計算總價的邏輯,而不真的去寫入資料庫或發送 Email?非常困難。
  • 難以重複使用: 如果你需要在後台手動建立訂單,或透過 API 建立訂單,你就得複製貼上這段邏輯,造成程式碼重複。
  • 難以閱讀與維護: 當需求變更,例如增加優惠券、計算運費等邏輯,你敢動這坨程式碼嗎?

架構三本柱:Repository, Service, Action 的角色與分工

為了解決這個問題,我們需要引入『分層』的概念,讓每一層的程式碼只做自己該做的事。Repository、Service、Action 就是實現分層的三種主要模式。

第一層防線:Repository Pattern – 你的資料庫口譯員

Repository 模式的核心思想是建立一個資料存取層的抽象。簡單來說,它就像一個專業的口譯員,專門負責跟資料庫溝通。你的應用程式其他部分(例如 Service)不需要知道底層是用 Eloquent、Query Builder 還是其他 ORM,只需要跟 Repository 這個口譯員溝通就好。

優點:

  • 解耦: 將資料庫查詢邏輯與商業邏輯分離。
  • 易於測試: 在測試商業邏輯時,你可以輕易地用一個假的(Mock)Repository 來取代真的資料庫操作。
  • 集中管理: 所有跟某個 Model 相關的複雜查詢,都可以集中放在它的 Repository 裡,方便重複使用。

工程師的小囉嗦: Repository 模式是個好東西,但不要濫用!對於簡單的 CRUD 專案,為每個 Model 都建立一個 Repository 往往是『過度設計』(Over-engineering)。Laravel 的 Eloquent 本身已經是個非常強大的抽象層了。我建議只在以下情況使用 Repository:

  1. 你需要處理非常複雜、且會在多處重複使用的查詢。
  2. 你的應用程式可能需要支援多種資料庫(例如 MySQL 和 MongoDB)。
  3. 你對測試覆蓋率有極高的要求,需要徹底隔離資料庫層。
<?php
// 首先定義一個介面 (Contract)
namespace App\Repositories\Contracts;

interface OrderRepositoryInterface
{
    public function create(array $data);
    public function findByUser(int $userId);
}

// 然後是 Eloquent 的實作
namespace App\Repositories\Eloquent;

use App\Models\Order;
use App\Repositories\Contracts\OrderRepositoryInterface;

class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function create(array $data)
    {
        return Order::create($data);
    }

    public function findByUser(int $userId)
    {
        return Order::where('user_id', $userId)->get();
    }
}

第二層防線:Service Layer – 商業邏輯的總指揮

如果 Repository 是口譯員,那 Service 就是商業邏輯的總指揮官。它負責編排整個業務流程(Use Case)。例如「建立訂單」這個流程,就可能包含:驗證庫存、計算價格、建立訂單資料、扣除庫存、發送通知…等一連串的步驟。這些步驟的『協調與執行』就是 Service 的職責。

Service 層會呼叫 Repository 來存取資料,但它本身不應該包含任何資料庫查詢語法。它專注於回答「做什麼?(What)」,而 Repository 專注於回答「如何取得資料?(How)」。

<?php

namespace App\Services;

use App\Repositories\Contracts\OrderRepositoryInterface;
use App\Repositories\Contracts\ProductRepositoryInterface;
use Illuminate\Support\Facades\Mail;

class OrderService
{
    protected $orderRepository;
    protected $productRepository;

    public function __construct(OrderRepositoryInterface $orderRepo, ProductRepositoryInterface $productRepo)
    {
        $this->orderRepository = $orderRepo;
        $this->productRepository = $productRepo;
    }

    public function createOrder(array $data)
    {
        // ... 複雜的商業邏輯,例如檢查庫存、計算總價等 ...
        
        $order = $this->orderRepository->create([...]);

        // ... 扣除庫存 ...

        Mail::to($data['user'])->send(new \App\Mail\OrderConfirmation($order));

        return $order;
    }
}

新銳挑戰者:Action Pattern – 單一職責的極致信徒

當你的 Service 變得越來越大,處理的業務越來越多時(例如 OrderService 可能要處理建立、取消、退款、出貨等),Service 本身也可能變成一個新的「肥怪獸」。這時候,Action 模式就登場了。

Action 模式是 SRP 原則的極致實踐。它主張將每一個獨立的業務操作,都封裝成一個小巧、單一、可執行的類別。例如,CreateOrderActionCancelOrderActionSendOrderConfirmationEmailAction

每個 Action 類別通常只有一個公開方法,例如 execute()handle()。這讓你的程式碼變得極度模組化、可讀性高,而且非常容易被組合和重複使用。你可以將 Service 視為一個更輕量的協調者,負責依序呼叫多個 Action 來完成一個複雜的業務流程。

<?php

namespace App\Actions\Orders;

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

class CreateOrderAction
{
    public function execute(User $user, array $products)
    {
        // 1. 計算總價
        $totalPrice = $this->calculateTotalPrice($products);

        // 2. 建立訂單
        $order = Order::create([
            'user_id' => $user->id,
            'total_price' => $totalPrice,
            'status' => 'pending',
        ]);

        // 3. 處理訂單項目與庫存
        // ...

        return $order;
    }

    private function calculateTotalPrice(array $products)
    {
        // ...
    }
}

實戰重構:用三層架構拯救『肥 Controller』

好了,理論講完,我們來動手重構剛剛那個義大利麵 Controller。

Step 1: 建立 Form Request 處理驗證與授權

首先,把驗證邏輯從 Controller 中抽離,這是 Laravel 內建的優雅作法。

// app/Http/Requests/StoreOrderRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        // 可以在這裡做授權檢查,例如檢查用戶是否有權限下單
        return true;
    }

    public function rules(): array
    {
        return [
            'products' => 'required|array',
            'products.*.id' => 'required|exists:products,id',
            'products.*.quantity' => 'required|integer|min:1',
        ];
    }
}

Step 2: 建立 Action/Service 處理核心邏輯

這裡我們用 Action 模式來封裝建立訂單的核心邏輯。

// app/Actions/Orders/CreateCompleteOrderAction.php
<?php

namespace App\Actions\Orders;

use App\Models\Order;
use App\Models\Product;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Events\OrderCreated;

class CreateCompleteOrderAction
{
    public function execute(User $user, array $productsData)
    {
        return DB::transaction(function () use ($user, $productsData) {
            $totalPrice = 0;
            $orderItemsData = [];

            foreach ($productsData as $productData) {
                $product = Product::lockForUpdate()->find($productData['id']);
                if ($product->stock < $productData['quantity']) {
                    throw new \Exception($product->name . ' 庫存不足');
                }
                $totalPrice += $product->price * $productData['quantity'];
                $orderItemsData[] = [
                    'product_id' => $product->id,
                    'quantity' => $productData['quantity'],
                    'price' => $product->price,
                ];
            }

            $order = Order::create([
                'user_id' => $user->id,
                'total_price' => $totalPrice,
                'status' => 'pending',
            ]);

            $order->items()->createMany($orderItemsData);

            foreach ($productsData as $productData) {
                Product::find($productData['id'])->decrement('stock', $productData['quantity']);
            }

            Mail::to($user)->send(new \App\Mail\OrderConfirmation($order));
            event(new OrderCreated($order));

            return $order;
        });
    }
}

Step 3: 清爽的 Controller – 瘦身成功!

最後,我們來看看 Controller 變得多麽乾淨。

<?php

namespace App\Http\Controllers;

use App\Actions\Orders\CreateCompleteOrderAction;
use App\Http\Requests\StoreOrderRequest;

class OrderController extends Controller
{
    public function store(StoreOrderRequest $request, CreateCompleteOrderAction $createOrderAction)
    {
        try {
            $order = $createOrderAction->execute(
                $request->user(),
                $request->validated()['products']
            );

            return response()->json($order, 201);
        } catch (\Exception $e) {
            return response()->json(['message' => $e->getMessage()], 400);
        }
    }
}

看到了嗎?Controller 現在只做三件事:接收經過驗證的請求、呼叫 Action 執行任務、回傳結果。它完全不知道訂單是如何建立的,也不關心庫存和 Email。職責單一,清爽無比!

工程師的真心話:沒有銀彈,只有取捨

講了這麼多,你可能會問:「Eric,所以我到底該用哪一種?」我的答案是:看情況。軟體架構的世界沒有萬能的銀彈,只有針對不同場景的『取捨』(Trade-offs)。

  • 小型專案 / CRUD 系統: 保持簡單!預設的 MVC 架構搭配 Form Request 就非常夠用了,別為了架構而架構。
  • 中型專案 / 業務邏輯開始複雜: 引入 Service Layer。將相關的商業邏輯組織在 Service 裡,讓 Controller 保持乾淨。
  • 大型專案 / 複雜且多變的業務流程: 考慮在 Service 中引入 Action 模式。將大的業務流程拆解成一系列獨立、可組合的 Action,這會讓你的系統擁有極高的靈活性和可維護性。
  • 需要支援多種資料來源 / 有大量複雜查詢: 在需要的地方引入 Repository Pattern,但不要把它當成標配,除非你真的需要那層抽象。

最終的目標,是寫出讓三個月後的自己(或是接手的同事)能看得懂、改得動,而且不會邊改邊罵髒話的程式碼。選擇一個適合你團隊和專案規模的架構,並且貫徹它,遠比追求一個『完美』但沒人會用的架構來得重要。希望今天的分享,能幫助你擺脫義大利麵的詛咒,打造出更強健的 Laravel 應用!

相關閱讀

在浪花科技,我們專注於打造高效、可擴展的系統架構,無論是 WordPress 還是 Laravel。如果你正在為你的專案架構感到頭痛,或是有更複雜的系統整合需求,歡迎與我們聊聊!我們很樂意提供專業的技術諮詢,幫助你的專案走向成功的道路。

常見問題 (FAQ)

Q1: 為什麼不能把所有邏輯都寫在 Controller?

A: 將所有邏輯放在 Controller 會導致所謂的「肥控制器」(Fat Controller),這會讓程式碼變得難以閱讀、測試和維護。它違反了單一職責原則,使得商業邏輯與 HTTP 請求處理的邏輯耦合在一起。當你需要重複使用某段商業邏輯時(例如在 API 和 Web 介面中),就只能複製貼上程式碼,造成後續維護的災難。

Q2: Service Layer 和 Repository Pattern 有什麼不同?我該用哪個?

A: 兩者職責不同。Repository Pattern 是一個「資料存取層」的抽象,專門負責與資料庫溝通,封裝 SQL 查詢。而 Service Layer 是「商業邏輯層」,負責協調和執行業務流程,它會呼叫 Repository 來取得或儲存資料。你可以把 Service 想成大腦,Repository 想成手腳。對於大多數專案,Service Layer 更為常用,只有在查詢邏輯非常複雜或需要支援多資料來源時,才需要引入 Repository。

Q3: Action 模式是什麼?它和 Service 有什麼關係?

A: Action 模式是將單一業務操作封裝成一個獨立類別的方法。當 Service 處理的業務太多而變得臃腫時,可以將其拆分成多個 Actions。例如,一個大的 `OrderService` 可以被拆解成 `CreateOrderAction`、`CancelOrderAction` 等。Service 可以作為一個輕量的協調者來呼叫這些 Action。Action 模式讓你的程式碼更符合單一職責原則,更模組化且易於重用。

Q4: 每個 Laravel 專案都需要這麼複雜的架構嗎?

A: 完全不需要。架構的選擇應與專案的複雜度相匹配。對於簡單的 CRUD 應用,使用 Laravel 預設的 MVC 架構,搭配 Form Request 進行驗證就非常足夠了。過早地引入複雜的設計模式(如 Repository)是「過度設計」,反而會增加不必要的複雜性。原則是:從簡單開始,隨著專案的成長,在遇到痛點時逐步引入更合適的架構模式來重構。

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