告別義大利麵: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:
- 你需要處理非常複雜、且會在多處重複使用的查詢。
- 你的應用程式可能需要支援多種資料庫(例如 MySQL 和 MongoDB)。
- 你對測試覆蓋率有極高的要求,需要徹底隔離資料庫層。
<?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 原則的極致實踐。它主張將每一個獨立的業務操作,都封裝成一個小巧、單一、可執行的類別。例如,CreateOrderAction、CancelOrderAction、SendOrderConfirmationEmailAction。
每個 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 應用!
相關閱讀
- 你的 Laravel 後台是『義大利麵』還是『積木城堡』?終極 Admin 架構設計指南,打造可傳承的程式碼藝術品
- SaaS 帝國不是夢!Laravel 多租戶 (Multi-tenant) 系統架構終極對決:從資料庫策略到實戰設計
- Eloquent 是蜜糖還是毒藥?資深工程師的 Laravel ORM 實戰心法,避開效能地雷區
在浪花科技,我們專注於打造高效、可擴展的系統架構,無論是 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)是「過度設計」,反而會增加不必要的複雜性。原則是:從簡單開始,隨著專案的成長,在遇到痛點時逐步引入更合適的架構模式來重構。






