~/blog/laravel-10-scalable-project-architecture-guide.md
Laravel 與後端開發 · 2025 / 12 / 27 · 3 views

打造可傳承的 Laravel 架構:薄 Controller 與職責分層實戰

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
打造可傳承的 Laravel 架構:薄 Controller 與職責分層實戰
目錄 table-of-contents.md

Laravel 可擴展架構怎麼設計?答案先講

商業邏輯一旦塞進 Controller,Laravel 專案離「技術債地雷」就不遠了。要打造可傳承的架構,核心原則只有一條:讓 Controller 變薄,把商業邏輯依「職責」拆進專屬的類別。實務上的分工是——「單一、明確的動作」用 Action;「一組相關的業務流程」用 Service;「需要抽換或封裝資料來源」才用 Repository;層與層之間的資料用 DTO 傳遞。

不需要一次全套上身。本文用一個「肥 Controller」的重構實例,帶你看清每一種模式各自解決什麼問題、什麼時候該用、什麼時候是過度設計,讓你打造出三年後回頭看依然清晰、好維護、可測試的程式碼。

為何 Laravel 預設架構是「新手村」,而非「終點站」?

先別誤會,Laravel 預設的 MVC(Model-View-Controller)架構非常優秀。它讓新手能快速上手,把 Controller、Model、View 各司其職,對於小型專案或原型開發來說,簡直是完美。但問題是,當專案規模擴大、商業邏輯變得複雜時,這個「新手村」的裝備就不夠用了。

開發者會很自然地把所有邏輯都往 Controller 裡塞,因為那是最直覺的地方。於是,你的 UserController 不只處理 HTTP 請求,還得負責:

  • 驗證使用者輸入。
  • 呼叫多個 Model 進行複雜的資料庫操作。
  • 處理圖片上傳。
  • 呼叫第三方服務發送歡迎郵件或簡訊。
  • 根據不同條件組合複雜的查詢。
  • 格式化回傳給前端的資料。

很快地,Controller 就會變得臃腫不堪,也就是我們常說的「Fat Controller」。這種程式碼不僅難以閱讀和維護,更可怕的是,它幾乎無法進行單元測試。你想單獨測試發送郵件的邏輯嗎?抱歉,你得模擬一整個 HTTP 請求。這就是災難的開端。

判斷你的 Controller 是否「過胖」

如果以下情況出現任何一項,就代表該把邏輯往外搬了:

  • 同一個方法裡同時出現驗證、資料庫寫入、寄信、呼叫外部 API。
  • 同一段業務流程被複製貼上到 Web Controller 和 API Controller 兩個地方。
  • 想寫測試時,發現非得先偽造一個 HTTP Request 才能跑得起來。
  • 新人接手時,光看一個方法看不懂「這個功能到底做了哪些事」。

後端架構三巨頭:Service、Repository、Action 該怎麼選?

為了解決 Fat Controller 的問題,社群發展出了許多優秀的設計模式。其中,Service 層、Repository 模式和 Action 模式是最常被討論的三巨頭。它們不是互斥的,而是可以在同一個專案中各司其職、相輔相成的工具。身為工程師,囉嗦一點是應該的,我們得搞清楚每把武器的適用場景。

Service 層:你的商業邏輯總管

Service 層(服務層)是專門用來放置「商業邏輯」的地方。什麼是商業邏輯?簡單來說,就是那些跟「我們的生意怎麼做」有關的程式碼,例如「使用者註冊後,要建立會員資料、發送歡迎信、並給予紅利點數」。這些流程通常會跨越多個 Model,而且不應該被綁定在任何一個 Controller 裡面。

  • 職責:協調多個 Model 或其他 Service,完成一項完整的業務功能。
  • 優點:讓 Controller 變得很乾淨,只負責接收請求和回傳響應。商業邏輯可以被多個 Controller(例如 Web Controller 和 API Controller)重複使用。
  • 範例:OrderServiceUserServicePaymentService

Repository 模式:資料庫的「翻譯官」

Repository Pattern(倉儲模式)的初衷是將資料存取邏輯(無論是從資料庫、快取還是外部 API)從應用程式的其餘部分抽離出來。它扮演著應用程式和資料來源之間的中介層。

老實說,這年頭還在爭 Repository pattern 在 Laravel 中有沒有必要,其實有點像在爭 PHP 是不是最好的語言一樣……沒完沒了。反對者認為 Laravel 的 Eloquent ORM 本身就已經是個很強大的資料抽象層了,再加一層 Repository 只是徒增複雜度。我個人的看法是:對於多數的中小型專案,直接用 Eloquent 就夠了。但在某些特定情境下,Repository 依然非常有價值:

  • 需要切換資料來源:例如,你的產品資料可能一部分來自資料庫,一部分來自外部的 ERP API。Repository 可以將這個複雜性隱藏起來,讓 Service 層用同樣的方式取得產品資料。
  • 複雜的查詢邏輯:當你有非常複雜、需要被多處複用的查詢時,可以將它們封裝在 Repository 的方法中,例如 getActiveUsersWithRecentOrders()
  • 強迫團隊遵守規範:在大型團隊中,Repository 可以作為一個契約(介面),確保大家用同樣的方式與資料庫互動,也方便在測試時替換成假的實作。

提醒:Repository 真正的價值在於「面向介面而非實作」。如果你只是把 Eloquent 的方法原封不動換個名字包一層,卻沒有定義介面、也沒有抽換需求,那就是典型的過度設計,徒增維護負擔。

Action 模式:單一任務的「特種兵」

Action 模式是近年來在 Laravel 社群中越來越流行的一種作法。它的核心思想非常簡單:一個類別,只做一件事情。如果說 Service 像是一個負責多項事務的專案經理,那 Action 就是一個只專注於完成單一任務的特種兵。

  • 職責:執行一個單一、明確的動作。通常只有一個公開的方法,例如 execute()handle()
  • 優點:高度內聚、低耦合,非常容易理解和測試。可以避免 Service 層因為功能不斷增加而變得臃腫。
  • 範例:CreateUserActionProcessPaymentActionUploadAvatarAction

一張表看懂三者的分工

模式 解決的問題 適用時機 命名直覺
Action 把單一業務動作封裝成可重用、好測試的單元 「動詞 + 受詞」就能說清楚的功能,如「建立訂單」 動詞開頭,如 CreateUserAction
Service 組織一組相關的業務流程、協調多個步驟 同一個業務領域有多個相關操作要集中管理 名詞 + Service,如 OrderService
Repository 抽離並統一資料存取邏輯 需要抽換資料來源,或有複雜共用查詢 名詞 + Repository,如 UserRepository

實戰演練:拆解一顆「肥 Controller 炸彈」

光說不練假把戲。讓我們來看一個實際的例子,如何將一個典型的 Fat Controller 方法,重構成清晰、可維護的結構。

改造前:令人崩潰的 Fat Controller

想像一下,你有一個使用者註冊的功能,Controller 長得可能像這樣:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

class UserController extends Controller
{
    public function store(Request $request)
    {
        // 1. 驗證
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        // 2. 處理頭像上傳
        $avatarPath = null;
        if ($request->hasFile('avatar')) {
            $avatarPath = $request->file('avatar')->store('avatars', 'public');
        }

        // 3. 建立使用者
        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
            'avatar' => $avatarPath,
        ]);

        // 4. 發送歡迎信
        Mail::to($user->email)->send(new WelcomeEmail($user));

        // 5. 分配預設角色 (假設有)
        $user->assignRole('member');

        return response()->json(['message' => 'User created successfully!', 'user' => $user], 201);
    }
}

天啊,光看就頭痛。驗證、檔案處理、資料庫操作、郵件發送、權限分配……全都擠在一起。現在,我們來動手術。

重構的三個步驟

我們的目標是讓 Controller 只做它該做的事:接收請求、呼叫核心邏輯、回傳響應。重構分成三步:

  1. 把驗證搬到 Form Request——讓驗證規則有自己的家。
  2. 用 DTO 承載結構化資料——讓資料在各層之間傳遞時型別明確。
  3. 用 Action 封裝核心動作——把「建立使用者」這件事獨立成可重用、可測試的單元。

改造後:清爽、可讀、可測試的「積木城堡」

我們會使用 Action 模式來封裝「建立使用者」這個核心動作,並搭配 DTO(Data Transfer Object)來傳遞結構化資料。

首先,我們建立一個 DTO 來承載使用者資料。原文範例使用 spatie/laravel-data 這個套件,它能讓 DTO 變得非常優雅,並支援從 Request 自動轉換。

1. 建立 UserData DTO(app/Data/UserData.php

<?php

namespace App\Data;

use Spatie\LaravelData\Data;
use Illuminate\Http\UploadedFile;

class UserData extends Data
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
        public ?UploadedFile $avatar = null
    ) {}
}

2. 建立 CreateUserAction(app/Actions/Users/CreateUserAction.php

我們將所有註冊邏輯都搬到這個 Action 裡。

<?php

namespace App\Actions\Users;

use App\Data\UserData;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

class CreateUserAction
{
    public function execute(UserData $data): User
    {
        $avatarPath = null;
        if ($data->avatar) {
            $avatarPath = $data->avatar->store('avatars', 'public');
        }

        $user = User::create([
            'name' => $data->name,
            'email' => $data->email,
            'password' => Hash::make($data->password),
            'avatar' => $avatarPath,
        ]);

        Mail::to($user->email)->send(new WelcomeEmail($user));

        $user->assignRole('member');

        return $user;
    }
}

3. 改造後的 Controller

現在,我們的 Controller 變得前所未有的清爽!

<?php

namespace App\Http\Controllers;

use App\Actions\Users\CreateUserAction;
use App\Http\Requests\StoreUserRequest; // 將驗證邏輯抽到 Form Request
use App\Data\UserData;

class UserController extends Controller
{
    public function store(StoreUserRequest $request, CreateUserAction $createUserAction)
    {
        // 從 Request 自動轉換為 DTO
        $userData = UserData::from($request);

        // 執行 Action
        $user = $createUserAction->execute($userData);

        return response()->json(['message' => 'User created successfully!', 'user' => $user], 201);
    }
}

看到了嗎?Controller 現在只剩下三行核心程式碼,每一行都只做一件事。它不關心密碼如何加密、頭像如何儲存、郵件如何發送。它只負責協調。這樣的程式碼,可讀性、可維護性、可測試性都提升了好幾個檔次。

抽離後,測試變得有多簡單?

重構真正的價值,在測試的時候會看得最清楚。改造後你不再需要偽造 HTTP 請求,只要直接準備一個 DTO、呼叫 Action,再驗證結果即可:

<?php

namespace Tests\Feature;

use App\Actions\Users\CreateUserAction;
use App\Data\UserData;
use Tests\TestCase;

class CreateUserActionTest extends TestCase
{
    public function test_it_creates_a_user(): void
    {
        $data = new UserData(
            name: 'Eric',
            email: 'eric@example.com',
            password: 'password123',
        );

        $user = app(CreateUserAction::class)->execute($data);

        $this->assertSame('eric@example.com', $user->email);
        $this->assertDatabaseHas('users', ['email' => 'eric@example.com']);
    }
}

沒有路由、沒有 HTTP、沒有一堆前置設定——這正是「低耦合」帶來的紅利。對於寄信、第三方呼叫這類副作用,還能搭配 Laravel 內建的假冒工具(如 Mail::fake())來斷言「該寄的信有沒有被寄出」,而完全不會真的送出郵件。

不只是好看:「可演化架構」的四大核心價值

採用這樣的架構,得到的好處遠不只是程式碼變整潔而已,它為你的專案帶來了四大核心價值:

  • 可維護性(Maintainability):當你需要修改註冊流程時(例如增加邀請碼功能),你只需要去 CreateUserAction 這個檔案修改,而不用在龐大的 Controller 裡大海撈針。
  • 可測試性(Testability):你可以輕易地對 CreateUserAction 進行單元測試,模擬各種輸入,驗證輸出的使用者資料是否正確,而完全不需要啟動 HTTP 服務。
  • 可擴展性(Scalability):如果未來你需要一個 CLI 指令來大量建立使用者,你可以在指令中直接呼叫 CreateUserAction,完美複用所有商業邏輯。
  • 團隊協作(Collaboration):職責劃分清晰,前端工程師可以專注於 View 和 Controller,後端工程師可以專注於 Action 和 Service 的商業邏輯,大家可以並行開發,減少衝突。

如何循序漸進導入,而不是一次全套上身?

架構是演進出來的,不是一開始就鋪天蓋地。建議的導入順序是「痛點驅動」:

  1. 先把驗證抽到 Form Request。這是成本最低、收益最高的一步,Controller 立刻瘦一圈。
  2. 遇到「跨多個 Model、又會被複用」的流程,抽成 Action。從最常被改、最常出錯的那個功能開始。
  3. 當同一個業務領域累積了多個相關 Action,再考慮用 Service 收攏。讓 Service 去協調這些 Action。
  4. 只有在真的需要抽換資料來源或封裝複雜共用查詢時,才導入 Repository。並且務必搭配介面(interface)。

記住一個原則:每一層的存在都要能回答「它替我擋掉了什麼複雜度」。如果答不出來,那一層多半是過度設計。

結論:你的程式碼,是你留給世界的數位遺產

我知道,一開始就導入這樣的架構,感覺會多寫一些檔案、多做一些規劃。但請相信我,這就像蓋房子前先畫好藍圖一樣,前期的投入會在專案的整個生命週期中,為你省下數不清的時間和精力。好的架構能讓你的專案走得更遠、更穩,也能讓你成為一個更專業、更有價值的開發者。

你的程式碼,不只是一行行的指令,它是你解決問題的思路、是你專業能力的體現,更是你留給下一位維護者(甚至是你自己)的數位遺產。希望今天的分享,能幫助你打造出讓自己和團隊都感到驕傲的程式碼帝國。

如果你對於如何將現有專案進行重構,或是想為新專案規劃一個穩固的架構感到困惑,浪花科技的團隊擁有豐富的實戰經驗,能為你提供專業的架構諮詢與開發服務。別讓失控的技術債拖垮你的業務,立即聯繫我們,讓我們一起打造堅不可摧的數位產品!

延伸閱讀

// FAQ

常見問題

如何設計可擴展的 Laravel 專案架構,避免變成技術債?
核心原則是讓 Controller 變薄,把商業邏輯依職責拆進專屬類別。實務分工為:單一明確的動作用 Action;一組相關的業務流程用 Service;需要抽換或封裝資料來源才用 Repository;層與層之間用 DTO 傳遞資料。不需要一次全套導入,依專案實際痛點漸進引入即可。
怎麼判斷我的 Laravel Controller 已經太肥(Fat Controller)?
出現以下任一情況就該把邏輯往外搬:同一個方法裡同時做驗證、資料庫寫入、寄信、呼叫外部 API;同一段業務流程被複製到 Web Controller 和 API Controller 兩處;想寫測試時非得先偽造一個 HTTP Request 才能跑;新人接手時光看一個方法看不懂這功能到底做了哪些事。
Laravel 的 Service、Repository、Action 三種模式各自解決什麼問題?
Action 把單一業務動作封裝成可重用、好測試的單元,適合「動詞加受詞」就能說清楚的功能(如 CreateUserAction)。Service 組織一組相關業務流程、協調多個步驟,適合同一業務領域有多個相關操作要集中管理(如 OrderService)。Repository 抽離並統一資料存取邏輯,適合需要抽換資料來源或有複雜共用查詢時(如 UserRepository)。三者並非互斥,可在同一專案中各司其職。
Laravel 一定要用 Repository 模式嗎?
不一定。對多數中小型專案,Eloquent ORM 本身已是強大的資料抽象層,直接使用就足夠。Repository 的真正價值在於「面向介面而非實作」,適用於需要切換資料來源、有複雜且多處複用的查詢,或在大型團隊中作為契約統一互動方式。若只是把 Eloquent 方法原封不動包一層、沒有定義介面也沒有抽換需求,就是典型的過度設計。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

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

$
// final.exec()

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