若将数据库逻辑都写在 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.php
app/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']; }
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.php
app/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(); } }
/** @var User 注入的User model */
protected $user;
/**
* UserRepository constructor.
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
将相依的 User model 依赖注入到 UserRepository。
/** * 回传大于?年纪的资料 * @param integer $age * @return Collection */ public function getAgeLargerThan($age) { return $this->user ->where('age', '>', $age) ->orderBy('age') ->get(); }
将抓 20 岁以上的数据的数据库逻辑写在 getAgeLargerThan()。
不是使用User facade,而是使用注入的$this->user。3
3这里也可以使用User facade 的方式,并不会影响可测试性,因为实务上在测试 repository 时,会真的去读写数据库,而不会去 mock User model,因此可以依可测试性决定要用依赖注入还是 Facade。
UserController.php
app/Http/Controllers/UserController.php namespace 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')); } }
/** @var UserRepository 注入的UserRepository */
protected $userRepository;
/**
* UserController constructor.
*
* @param UserRepository $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
将相依的 UserRepository 依赖注入到 UserController。
/** * 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'); } } /** * 回传大于?年纪的资料 * @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
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 上找到。