~/blog/laravel-admin-architecture-design-guide.md
Laravel 與後端開發 · 2025 / 12 / 25

Laravel Admin 後台架構設計指南:從義大利麵重構成積木城堡

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
Laravel Admin 後台架構設計指南:從義大利麵重構成積木城堡
目錄 table-of-contents.md

寫了這麼多年的 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 的職責就只剩下:

  1. 接收 Request(已經被 FormRequest 清理乾淨了)。
  2. 呼叫對應的 Service 方法來處理業務。
  3. 根據 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 的完整旅程:

  1. HTTP Request 進入路由(Route)。
  2. 路由導向 `ProductController` 的 `store` 方法。
  3. `StoreProductRequest` 攔截請求,進行權限和資料驗證。
  4. 驗證通過後,`ProductController` 呼叫 `ProductService` 的 `createProduct` 方法,並傳入驗證過的資料。
  5. `ProductService` 處理核心商業邏輯(如處理圖片、設定預設值),然後呼叫 `ProductRepository` 來將資料存入資料庫。
  6. `ProductRepository` 執行 Eloquent 操作,將資料寫入 `products` 資料表。
  7. 資料庫操作完成後,`ProductService` 可能會再呼叫其他 Service(如 `NotificationService`)。
  8. `ProductService` 將建立好的 Product Model 回傳給 `ProductController`。
  9. `ProductController` 根據結果,回傳一個重導向的 HTTP Response。

這個結構帶來的好處是巨大的:

  • 高可維護性:想修改驗證規則?去 `FormRequest`。想修改商業邏輯?去 `Service`。想修改資料庫查詢?去 `Repository`。一切都井井有條。
  • 高可測試性:每一層都可以獨立進行單元測試,確保程式碼的品質。
  • 高可擴展性:未來新增功能時,你只需要組合或擴展現有的 Service 和 Repository,而不用去動到那坨巨大的 Controller。
  • 團隊協作:職責清晰,團隊成員可以分工負責不同的層級,減少互相干擾。

相關閱讀

結論:不只是寫 Code,更是對未來的投資

從一團混亂的義大利麵,到結構清晰的積木城堡,這趟旅程的核心精神在於「拆解」與「分層」。好的軟體架構,就像蓋房子前畫好的藍圖,它決定了這棟建築能蓋多高、能用多久。

或許一開始你會覺得多寫了幾個檔案、多了幾層抽象很麻煩,但請相信我,這是在為你的未來投資。當專案長大、需求變更時,你會感謝當初那個願意多花一點時間把地基打穩的自己。

如果你正在為雜亂的後台架構所苦,或是準備打造一個能支撐未來業務擴展的強大系統,卻不知從何下手,浪花科技的團隊擁有豐富的 Laravel 專案架構經驗。我們樂於協助你打造出堅固、優雅且可傳承的數位產品。歡迎點擊這裡與我們聯繫,讓我們聊聊你的專案吧!

// FAQ

常見問題

什麼是 Laravel 的「肥控制器(Fat Controller)」,它有什麼問題?
肥控制器是指把資料驗證、商業邏輯、資料庫操作、發送通知等職責全部塞進 Controller 的方法裡,使其不斷膨脹。它違反單一職責原則(SRP),導致程式碼難以閱讀、難以撰寫單元測試、邏輯重複,且職責不清。Controller 的核心職責應該只是接收 HTTP Request 並回傳 HTTP Response。
如何把驗證邏輯從 Laravel Controller 中抽離?
使用 Laravel 的 Form Request。透過 php artisan make:request 建立一個 FormRequest 類別,把驗證規則寫進它的 rules() 方法,並可在 authorize() 處理權限。接著在 Controller 方法中把型別提示換成這個 FormRequest,驗證就會自動執行,驗證失敗時 Laravel 會自動跳轉,並可用 $request->validated() 取得已驗證資料。
Service Layer 在 Laravel 架構中扮演什麼角色?
Service Layer 負責把核心商業邏輯從 Controller 抽離,封裝到獨立的 Service Class 中。導入後 Controller 的職責只剩三件事:接收已被 Form Request 清理過的 Request、呼叫對應的 Service 方法處理業務、再依 Service 回傳結果決定回傳哪個 Response。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?