Laravel框架中如何使用 Repository 模式
源:http://www.sangeng.org/blog/index/detail/id/518.html
若將資料庫邏輯都寫在model,會造成model的肥大而難以維護,基於SOLID原則,我們應該使用Repository模式輔助model,將相關的資料庫邏輯封裝在不同的repository,方便中大型專案的維護。
資料庫邏輯
在CRUD中,CUD比較穩定,但R的部分則千變萬化,大部分的資料庫邏輯都在描述R的部分,若將資料庫邏輯寫在controller或model都不適當,會造成controller與model肥大,造成日後難以維護。
Model
使用repository之後,model僅當成Eloquent class即可,不要包含資料庫邏輯,僅保留以下部分:
- Property:如$table,$fillable…等。
- Mutator:包括mutator與accessor。
- Method:relation類的method,如使用hasMany()與belongsTo()。
-
註釋:因為Eloquent會根據資料庫欄位動態產生property與method,等。若使用Laravel IDE Helper,會直接在model加上@property與@method描述model的動態property與method。
User.phpapp/User.php namespace MyBlog; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; /** * MyBlog\User * * @property integer $id * @property string $name * @property string $email * @property string $password * @property string $remember_token * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value) */ class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { use Authenticatable, Authorizable, CanResetPassword; /** * The database table used by the model. * * @var string */ protected $table = 'users'; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name', 'email', 'password']; /** * The attributes excluded from the model's JSON form. * * @var array */ protected $hidden = ['password', 'remember_token']; }
12行
/** * MyBlog\User * * @property integer $id * @property string $name * @property string $email * @property string $password * @property string $remember_token * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value) * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value) */
IDE-Helper幫我們替model加上註釋,讓我們可以在PhpStorm的語法提示使用model的property與method
Repository
初學者常會在controller直接呼叫model寫資料庫邏輯:
public function index() { $users = User::where('age', '>', 20) ->orderBy('age') ->get(); return view('users.index', compact('users')); }
資料庫邏輯是要抓20歲以上的資料。
在中大型專案,會有幾個問題:- 將資料庫邏輯寫在controller,造成controller的肥大難以維護。
- 違反SOLID的單一職責原則:資料庫邏輯不應該寫在controller。
- controller直接相依於model,使得我們無法對controller做單元測試。
比較好的方式是使用repository: - 將model依賴注入到repository。
- 將資料庫邏輯寫在repository。
-
將repository依賴注入到service。
UserRepository.phpapp/Repositories/UserRepository.php namespace MyBlog\Repositories; use Doctrine\Common\Collections\Collection; use MyBlog\User; class UserRepository { /** @var User 注入的User model */ protected $user; /** * UserRepository constructor. * @param User $user */ public function __construct(User $user) { $this->user = $user; } /** * 回傳大於?年紀的資料 * @param integer $age * @return Collection */ public function getAgeLargerThan($age) { return $this->user ->where('age', '>', $age) ->orderBy('age') ->get(); } }
第 8 行
/** @var User 注入的User model */ protected $user; /** * UserRepository constructor. * @param User $user */ public function __construct(User $user) { $this->user = $user; }
將相依的User model依賴注入到UserRepository。
21 行/** * 回傳大於?年紀的資料 * @param integer $age * @return Collection */ public function getAgeLargerThan($age) { return $this->user ->where('age', '>', $age) ->orderBy('age') ->get(); }
將抓20歲以上的資料的資料庫邏輯寫在getAgeLargerThan()。
不是使用User facade,而是使用注入的$this->user
UserController.php
app/Http/Controllers/UserController.phpnamespace App\Http\Controllers; use App\Http\Requests; use MyBlog\Repositories\UserRepository; class UserController extends Controller { /** @var UserRepository 注入的UserRepository */ protected $userRepository; /** * UserController constructor. * * @param UserRepository $userRepository */ public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $users = $this->userRepository ->getAgeLargerThan(20); return view('users.index', compact('users')); } }
第8行
/** @var UserRepository 注入的UserRepository */ protected $userRepository; /** * UserController constructor. * * @param UserRepository $userRepository */ public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; }
將相依的UserRepository依賴注入到UserController。
26行/** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $users = $this->userRepository ->getAgeLargerThan(20); return view('users.index', compact('users')); }
從原本直接相依的User model,改成依賴注入的UserRepository。
改用這種寫法,有幾個優點:
- 將資料庫邏輯寫在repository,解決controller肥大問題。
- 符合SOLID的單一職責原則:資料庫邏輯寫在repository,沒寫在controller。
- 符合SOLID的依賴反轉原則:controller並非直接相依於repository,而是將repository依賴注入進controller。
實務上建議repository僅依賴注入於service,而不要直接注入在controller,本示例因為還沒介紹到servie模式,為了簡化起見,所以直接注入於controller。
是否該建立Repository Interface?
理論上使用依賴注入時,應該使用interface,不過interface目的在於抽象化方便抽換,讓程式碼達到開放封閉的要求,但是實務上要抽換repository的機會不高,除非你有抽換資料庫的需求,如從MySQL抽換到MongoDB,此時就該建立repository interface。
不過由於我們使用了依賴注入,將來要從class改成interface也很方便,只要在constructor的type hint改成interface即可,維護成本很低,所以在此大可使用repository class即可,不一定得用interface而造成over design,等真正需求來時再重構成interface即可。
是否該使用Query Scope?
Laravel 4.2就有query scope,到5.1都還留著,它讓我們可以將商業邏輯寫在model,解決了維護與重複使用的問題。
User.php
app/User.php
namespace MyBlog;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
/**
* (註解:略)
*/
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'password'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
/**
* 回傳大於?年紀的資料
* @param Builder $query
* @param integer $age
* @return Builder
*/
public function scopeGetAgerLargerThan($query, $age)
{
return $query->where('age', '>', $age)
->orderBy('age');
}
}
42行
/**
* 回傳大於?年紀的資料
* @param Builder $query
* @param integer $age
* @return Builder
*/
public function scopeGetAgerLargerThan($query, $age)
{
return $query->where('age', '>', $age)
->orderBy('age');
}
Query scope必須以scope為prefix,第1個引數為query builder,一定要加,是Laravel要用的。
第2個引數以後為自己要傳入的引數。
由於回傳也必須是一個query builder,因此不加上get()。
UserController.php
app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use MyBlog\User;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$users = User::getAgerLargerThan(20)->get();
return view('users.index', compact('users'));
}
}
在controller呼叫query scope時,不要加上prefix,由於其本質是query builder,所以還要加上get()才能抓到Collection。
由於query scope是寫在model,不是寫在controller,所以基本上解決了controller肥大與違反SOLID的單一職責原則的問題,controller也可以重複使用query scope,已經比直接將資料庫邏輯寫在controller好很多了。
不過若在中大型專案,仍有以下問題:
- Model已經有原來的責任,若再加上query scope,造成model過於肥大難以維護。
- 若資料庫邏輯很多,可以拆成多repository,可是卻很難拆成多model。
- 單元測試困難,必須面臨mock Eloquent的問題。
Conclusion
實務上可以一開始1個repository對應1個model,但不用太執著於1個repository一定要對應1個model,可將repository視為邏輯上的資料庫邏輯類別即可,可以橫跨多個model處理,也可以1個model拆成多個repository,端看需求而定。
Repository使得資料庫邏輯從controller或model中解放,不僅更容易維護、更容易擴充套件、更容易重複使用,且更容易測試。
Sample Code
完整的示例可以在我的GitHub上找到。