本篇介紹如何在 Laravel 中導入 Repository Pattern 與 Service Layer,將商業邏輯從 Controller 分離,提升可測試性與可維護性。
為什麼需要分層架構?
大多數 Laravel 專案初期都是 Fat Controller,所有邏輯堆在一起:
// 典型的 Fat Controller(難以維護、無法測試)
public function store(Request $request)
{
$user = User::where('email', $request->email)->first();
if ($user) {
return response()->json(['message' => 'Email 已存在'], 422);
}
$user = User::create([...]);
$user->roles()->attach(Role::where('name', 'member')->first());
Mail::to($user)->send(new WelcomeMail($user));
Cache::forget('users:list');
Log::info('新用戶註冊', ['user_id' => $user->id]);
return response()->json(['message' => '註冊成功']);
}
問題:Controller 同時負責驗證、資料庫操作、商業邏輯、快取、通知,一旦需求改變就要動到 Controller,無法單元測試。
三層架構設計
| 層次 | 職責 | 不應該做的事 |
|---|---|---|
| Controller | 接收 Request、呼叫 Service、回傳 Response | 直接操作 DB、寫商業邏輯 |
| Service | 商業邏輯、流程控制、跨 Repository 協調 | 直接操作 DB(透過 Repository) |
| Repository | 所有資料庫查詢與寫入 | 寫商業邏輯 |
實作流程(以 User 模組為例)
Step 1:建立 Repository Interface
# app/Repositories/Contracts/UserRepositoryInterface.php
<?php
namespace App\Repositories\Contracts;
use App\Models\User;
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
public function create(array $data): User;
public function update(int $id, array $data): User;
public function delete(int $id): bool;
}
Step 2:建立 Eloquent 實作
# app/Repositories/EloquentUserRepository.php
<?php
namespace App\Repositories;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class EloquentUserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?User
{
return User::find($id);
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function create(array $data): User
{
return User::create($data);
}
public function update(int $id, array $data): User
{
$user = User::findOrFail($id);
$user->update($data);
return $user->fresh();
}
public function delete(int $id): bool
{
return User::destroy($id) > 0;
}
}
Step 3:綁定 Interface → 實作
# app/Providers/AppServiceProvider.php
use App\Repositories\Contracts\UserRepositoryInterface;
use App\Repositories\EloquentUserRepository;
public function register(): void
{
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
}
Step 4:建立 Service Layer
# app/Services/UserService.php <?php namespace App\Services; use App\Models\User; use App\Repositories\Contracts\UserRepositoryInterface; use App\Jobs\SendWelcomeEmail; use Illuminate\Support\Facades\Cache; class UserService { public function __construct( private UserRepositoryInterface $userRepository, ) {} public function register(array $data): User { // 商業邏輯:檢查 Email 是否重複 if ($this->userRepository->findByEmail($data['email'])) { throw new \Exception('Email 已存在'); } $user = $this->userRepository->create($data); // 派發歡迎信 Job SendWelcomeEmail::dispatch($user); // 清除快取 Cache::forget('users:list'); return $user; } public function updateProfile(int $id, array $data): User { return $this->userRepository->update($id, $data); } }
Step 5:Controller 只注入 Service
# app/Http/Controllers/UserController.php
<?php
namespace App\Http\Controllers;
use App\Services\UserService;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function __construct(
private UserService $userService,
) {}
public function store(Request $request)
{
$user = $this->userService->register($request->validated());
return response()->json(['message' => '註冊成功', 'user' => $user], 201);
}
}
單元測試的好處
導入分層後,Service 可以 Mock Repository,完全不需要碰真實資料庫:
class UserServiceTest extends TestCase
{
public function test_register_throws_exception_when_email_exists()
{
// Mock Repository 回傳已存在的 User
$mockRepo = Mockery::mock(UserRepositoryInterface::class);
$mockRepo->shouldReceive('findByEmail')
->once()
->andReturn(new User());
$service = new UserService($mockRepo);
$this->expectException(\Exception::class);
$service->register(['email' => '[email protected]']);
}
}
目錄結構
app/ ├── Http/ │ └── Controllers/ │ └── UserController.php # 只管 Request / Response ├── Services/ │ └── UserService.php # 商業邏輯 ├── Repositories/ │ ├── Contracts/ │ │ └── UserRepositoryInterface.php │ └── EloquentUserRepository.php # 資料庫操作 └── Providers/ └── AppServiceProvider.php # Interface 綁定
什麼時候不需要用?
分層架構是有成本的,以下情境可以斟酌:
- CRUD 簡單的小型專案:直接用 Controller + Model 就夠了
- 原型開發、快速驗證:先求跑通,之後有需要再重構
- 團隊規模小、沒有測試需求:分層反而增加溝通成本
分層的核心價值在於:商業邏輯複雜、需要單元測試、多人協作的中大型專案。
參考文獻: