從義大利麵到積木城堡:Laravel Admin 終極架構重構指南
你的 Laravel 後台是不是也陷入「肥控制器」的泥沼,變成一團難以收拾的「義大利麵」程式碼?資深工程師 Eric 帶領你啟動一場關鍵的架構重構之旅!我們將運用 Form Requests 進行驗證瘦身、導入 Service Layer 集中業務核心,並透過 Repository Pattern 實現資料庫解耦。這不只是修補 Bug,更是將你的專案升級為結構清晰、高度可測試且易於維護的「積木城堡」。別讓雜亂的架構成為未來擴展的負擔,立即學習這份專業指南,打造一份能讓你和團隊感到驕傲的程式碼藝術品!
你的 Laravel 後台是『義大利麵』還是『積木城堡』?終極 Admin 架構設計指南,打造可傳承的程式碼藝術品
哈囉,我是浪花科技的資深工程師 Eric。寫了這麼多年的 Code,我看過太多一開始光鮮亮麗,後來卻變成『義大利麵』的專案。程式碼全部攪和在一起,動一髮而牽全身,別說交接了,有時候連幾個月後的自己都看不懂。尤其在 Laravel Admin 後台的開發中,這種情況更是屢見不鮮。
很多開發者(包括年輕時的我)剛接觸 Laravel 時,都會愛上它的優雅與快速。一個 `artisan make:controller` 指令下去,一個 Resource Controller 就建好了,然後就把所有的商業邏輯、資料庫操作、資料驗證…通通塞進 Controller 的七個方法裡。專案小的時候,這樣做很快,很爽。但隨著功能越來越複雜,你的 Controller 就會像吹氣球一樣,迅速膨脹成一隻難以駕馭的怪獸——我們稱之為「肥控制器」(Fat Controller)。
今天,我不是要來嚇唬你,而是想帶你走一趟 Laravel 後台架構的重構之旅。我們會從失控的義大利麵程式碼開始,一步步地拆解、重組,最終打造出一個像樂高積木一樣,結構清晰、易於維護、可擴展的『積木城堡』。這不只是寫出能動的 Code,更是打造一份能讓你(和你的團隊)在未來感到驕傲的藝術品。
第一站:MVC 的美麗與哀愁 – 為什麼 Controller 會失控?
我們先來看看一個典型的『肥控制器』案例。假設我們在開發一個商品管理的後台,`ProductController` 的 `store` 方法可能會長這樣:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Services\NotificationService; // 假設有個通知服務
class ProductController extends Controller
{
public function store(Request $request)
{
// 1. 資料驗證
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'sku' => 'required|string|unique:products,sku',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// 2. 核心商業邏輯
$product = new Product();
$product->name = $request->input('name');
$product->sku = $request->input('sku');
$product->price = $request->input('price');
$product->stock = $request->input('stock');
$product->is_active = true; // 預設為啟用
// 如果有上傳圖片,處理圖片
if ($request->hasFile('image')) {
$path = $request->file('image')->store('products', 'public');
$product->image_url = $path;
}
$product->save();
// 3. 觸發其他商業行為 (例如:發送通知)
NotificationService::sendAdminNotification('新商品已建立:' . $product->name);
// 4. 回傳 HTTP Response
return redirect()->route('products.index')->with('success', '商品建立成功!');
}
}
看起來好像沒什麼問題,對吧?但仔細想想,這個 `store` 方法做了太多事了。它違反了軟體設計中的「單一職責原則」(Single Responsibility Principle, SRP)。
- 難以閱讀:所有的邏輯都混在一起,你需要從頭看到尾才能理解完整的流程。
- 難以測試:你怎麼對這段程式碼寫單元測試?你必須模擬一個完整的 HTTP Request,非常麻煩。你想單獨測試「圖片上傳邏輯」或「通知邏輯」嗎?幾乎不可能。
- 程式碼重複:`update` 方法是不是也要寫一套幾乎一樣的驗證規則?如果新增一個 API 端點來建立商品,是不是又要複製貼上一次?
- 職責不清:Controller 的核心職責應該是接收 HTTP Request,並回傳 HTTP Response。它應該是個交通警察,而不是自己下去蓋房子。
這就是我們需要動手術的原因。接下來,我們一步步把它拆解開來。
第二站:減肥手術第一刀 – 善用 Form Requests 拆分驗證邏輯
Laravel 提供了一個非常優雅的工具來處理驗證:`FormRequest`。它可以把驗證邏輯從 Controller 中完全抽離出來。
我們先用 artisan 指令建立一個 `FormRequest`:
php artisan make:request StoreProductRequest
然後,把驗證邏輯搬進去 `app/Http/Requests/StoreProductRequest.php`:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize()
{
// 這裡可以寫權限驗證邏輯,例如:判斷當前使用者是否有權限建立商品
return true; // 暫時先設為 true
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'sku' => 'required|string|unique:products,sku',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
];
}
}
現在,我們的 Controller 就可以瘦身了。只需要在 `store` 方法中,將 `Illuminate\Http\Request` 型別提示換成我們剛建立的 `StoreProductRequest`。
// ...
use App\Http\Requests\StoreProductRequest;
class ProductController extends Controller
{
public function store(StoreProductRequest $request)
{
// 驗證邏輯已經自動執行!如果驗證失敗,Laravel 會自動跳轉回去。
// 你可以直接使用 $request->validated() 來取得已驗證的資料。
$validatedData = $request->validated();
// ... 剩下的商業邏輯
}
}
你看,Controller 是不是瞬間清爽多了?驗證邏輯被封裝到專屬的 class 裡,不僅 Controller 變乾淨了,這個 `StoreProductRequest` 還可以被重複使用。這就像是為你的 Controller 請了一個專業的保鑣,閒雜人等(無效資料)一律擋在門外,Controller 只需要專心接待貴賓就好。
第三站:打造你的『業務邏輯中樞』- Service Layer 的導入
驗證問題解決了,但 Controller 裡面還是有一大堆商業邏輯。這時候,就該輪到「服務層」(Service Layer)登場了。
Service Layer 的概念很簡單:將核心的商業邏輯(Application Logic)從 Controller 中抽離,封裝到獨立的 Service Class 中。Controller 的職責就只剩下:
- 接收 Request(已經被 FormRequest 清理乾淨了)。
- 呼叫對應的 Service 方法來處理業務。
- 根據 Service 的回傳結果,決定要回傳哪個 Response。
我們來建立一個 `ProductService`:
<?php
namespace App\Services;
use App\Models\Product;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Storage;
class ProductService
{
public function createProduct(array $data): Product
{
$product = new Product();
$product->fill($data); // 使用 Mass Assignment
$product->is_active = true;
if (isset($data['image'])) {
$path = $data['image']->store('products', 'public');
$product->image_url = Storage::url($path);
}
$product->save();
// 觸發通知
NotificationService::sendAdminNotification('新商品已建立:' . $product->name);
return $product;
}
}
接著,我們在 Controller 中透過「依賴注入」(Dependency Injection)來使用這個 Service:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreProductRequest;
use App\Services\ProductService;
class ProductController extends Controller
{
protected $productService;
public function __construct(ProductService $productService)
{
$this->productService = $productService;
}
public function store(StoreProductRequest $request)
{
$this->productService->createProduct($request->validated());
return redirect()->route('products.index')->with('success', '商品建立成功!');
}
}
工程師的小囉嗦時間:看到這裡,你可能會覺得「有必要搞這麼複雜嗎?」。相信我,絕對有必要。當你的業務邏輯越來越複雜,例如「建立商品後,需要同步到 ERP 系統」、「需要更新會員的購買記錄」、「需要觸發一個複雜的行銷活動」,這些邏輯都應該放在 `ProductService` 裡。Controller 始終保持乾淨,它完全不需要知道背後發生了什麼驚天動地的大事,這就是「關注點分離」(Separation of Concerns)的精髓。
第四站:解耦資料庫操作 – Repository Pattern 的優雅
我們的架構已經改善很多了,但還有優化的空間。在 `ProductService` 中,我們直接使用了 `Product` 這個 Eloquent Model。這在多數情況下沒問題,但如果我們想進一步提高程式碼的彈性和可測試性,「倉儲模式」(Repository Pattern)就是你的好朋友。
Repository Pattern 的核心思想是建立一個資料存取的中介層。Service 不再直接跟 Eloquent Model 對話,而是透過 Repository 來操作資料。這樣做的好處是:
- 易於測試:在測試 Service 時,你可以輕易地用一個「假的」(Mock)Repository 來取代真實的資料庫操作。
- 資料來源抽換:如果未來你的商品資料來源不只是 MySQL,可能還有一部分來自外部 API 或 Redis,你只需要建立一個新的 Repository 來實現同樣的介面,Service 層的程式碼完全不用動。
實作上,我們通常會先定義一個 Interface(介面),再建立一個 Class 來實現它。
1. 建立 Interface `app/Repositories/Interfaces/ProductRepositoryInterface.php`
<?php
namespace App\Repositories\Interfaces;
use App\Models\Product;
interface ProductRepositoryInterface
{
public function create(array $data): Product;
}
2. 建立實現 `app/Repositories/Eloquent/ProductRepository.php`
<?php
namespace App\Repositories\Eloquent;
use App\Models\Product;
use App\Repositories\Interfaces\ProductRepositoryInterface;
class ProductRepository implements ProductRepositoryInterface
{
public function create(array $data): Product
{
return Product::create($data);
}
}
3. 在 `AppServiceProvider` 中綁定 Interface 和實現
// app/Providers/AppServiceProvider.php
use App\Repositories\Interfaces\ProductRepositoryInterface;
use App\Repositories\Eloquent\ProductRepository;
public function register()
{
$this->app->bind(
ProductRepositoryInterface::class,
ProductRepository::class
);
}
最後,改造我們的 `ProductService`,讓它依賴 `ProductRepositoryInterface` 而不是 `Product` Model。
<?php
namespace App\Services;
use App\Models\Product;
use App\Repositories\Interfaces\ProductRepositoryInterface;
use Illuminate\Support\Facades\Storage;
class ProductService
{
protected $productRepository;
protected $notificationService; // 假設 NotificationService 也被注入
public function __construct(ProductRepositoryInterface $productRepository, NotificationService $notificationService)
{
$this->productRepository = $productRepository;
$this->notificationService = $notificationService;
}
public function createProduct(array $data): Product
{
$productData = $data;
$productData['is_active'] = true;
if (isset($data['image'])) {
$path = $data['image']->store('products', 'public');
$productData['image_url'] = Storage::url($path);
unset($productData['image']);
}
$product = $this->productRepository->create($productData);
$this->notificationService->sendAdminNotification('新商品已建立:' . $product->name);
return $product;
}
}
到這裡,我們的架構已經非常清晰了。每一層都有自己明確的職責,就像一條分工精細的工廠生產線。
終點站:組建你的『樂高城堡』- 完整的 Laravel Admin 架構
讓我們回顧一下一個 Request 的完整旅程:
- HTTP Request 進入路由(Route)。
- 路由導向 `ProductController` 的 `store` 方法。
- `StoreProductRequest` 攔截請求,進行權限和資料驗證。
- 驗證通過後,`ProductController` 呼叫 `ProductService` 的 `createProduct` 方法,並傳入驗證過的資料。
- `ProductService` 處理核心商業邏輯(如處理圖片、設定預設值),然後呼叫 `ProductRepository` 來將資料存入資料庫。
- `ProductRepository` 執行 Eloquent 操作,將資料寫入 `products` 資料表。
- 資料庫操作完成後,`ProductService` 可能會再呼叫其他 Service(如 `NotificationService`)。
- `ProductService` 將建立好的 Product Model 回傳給 `ProductController`。
- `ProductController` 根據結果,回傳一個重導向的 HTTP Response。
這個結構帶來的好處是巨大的:
- 高可維護性:想修改驗證規則?去 `FormRequest`。想修改商業邏輯?去 `Service`。想修改資料庫查詢?去 `Repository`。一切都井井有條。
- 高可測試性:每一層都可以獨立進行單元測試,確保程式碼的品質。
- 高可擴展性:未來新增功能時,你只需要組合或擴展現有的 Service 和 Repository,而不用去動到那坨巨大的 Controller。
- 團隊協作:職責清晰,團隊成員可以分工負責不同的層級,減少互相干擾。
相關閱讀
- Laravel 專案長不大?資深工程師的『可演化架構』指南,告別義大利麵程式碼!
- 肥 Controller 瘦不下來?Laravel 後台架構終極對決:Repository vs. Action 模式,資深工程師帶你選對屠龍刀!
- 你的 Controller 還在身兼多職?導入 Service Layer,打造可維護、高彈性的 Laravel Admin 後台架構!
結論:不只是寫 Code,更是對未來的投資
從一團混亂的義大利麵,到結構清晰的積木城堡,這趟旅程的核心精神在於「拆解」與「分層」。好的軟體架構,就像蓋房子前畫好的藍圖,它決定了這棟建築能蓋多高、能用多久。
或許一開始你會覺得多寫了幾個檔案、多了幾層抽象很麻煩,但請相信我,這是在為你的未來投資。當專案長大、需求變更時,你會感謝當初那個願意多花一點時間把地基打穩的自己。
如果你正在為雜亂的後台架構所苦,或是準備打造一個能支撐未來業務擴展的強大系統,卻不知從何下手,浪花科技的團隊擁有豐富的 Laravel 專案架構經驗。我們樂於協助你打造出堅固、優雅且可傳承的數位產品。歡迎點擊這裡與我們聯繫,讓我們聊聊你的專案吧!
常見問題 (FAQ)
Q1: Service Layer 和 Repository Pattern 一定要一起用嗎?
不一定。這取決於專案的複雜度。對於中小型專案,只導入 Service Layer 來分離商業邏輯,並在 Service 中直接使用 Eloquent Model 也是一個非常常見且有效的做法。當你的資料庫查詢變得非常複雜,或是有抽換資料來源的需求時,再導入 Repository Pattern 也不遲。架構是演化而來的,不需要一開始就過度設計。
Q2: 什麼時候我的專案才『需要』這麼複雜的架構?
一個好的判斷點是:當你發現 Controller 的方法超過一頁螢幕、開始出現重複的邏輯、或者你覺得要為某個功能寫測試變得很困難時,就是重構和分層的好時機。我的建議是,對於任何預期會長期維護和迭代的專案,從一開始就建立 Service Layer 和使用 FormRequest 是個好習慣。
Q3: 使用這些模式會不會讓開發速度變慢?
短期來看,建立這些檔案和分層會增加一些初始的開發時間。但長期來看,它會大大加快你的開發和維護速度。因為程式碼結構清晰,新增功能、除錯、或交接給新成員時,效率會高非常多。這是一種「先苦後甘」的投資。
Q4: Laravel 內建的 Resource Controller 和這個架構有衝突嗎?
完全沒有衝突!Resource Controller 提供了一個很好的 RESTful 方法論骨架(index, create, store, show, edit, update, destroy)。我們今天討論的架構設計,正是在這個骨架之內,將每個方法的「實作」變得更乾淨、更有組織。你可以繼續使用 Resource Controller,但把內部的邏輯拆分到 FormRequest、Service 和 Repository 中。





