Laravel框架中使用 Repository 模式

    若将数据库逻辑都写在model,会造成model的肥大而难以维护,基于SOLID原则,我们应该使用Repository模式辅助model,将相关的数据库逻辑封装在不同的repository,方便中大型项目的维护。

Version:Laravel 5.1.22


数据库逻辑

在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.phpnamespace 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岁以上的数据。
在中大型项目,会有几个问题:

  1. 将数据库逻辑写在controller,造成controller的肥大难以维护。

  2. 违反SOLID的单一职责原则:数据库逻辑不应该写在controller。

  3. controller直接相依于model,使得我们无法对controller做单元测试。
    比较好的方式是使用repository:

  4. 将model依赖注入到repository。

  5. 将数据库逻辑写在repository。

  6. 将repository依赖注入到service。
    UserRepository.php


    第 8 行


    将相依的User model依赖注入到UserRepository。
    21 行


    将抓20岁以上的数据的数据库逻辑写在getAgeLargerThan()。
    不是使用User facade,而是使用注入的$this->user
    UserController.php
    app/Http/Controllers/UserController.php


    第8行


    将相依的UserRepository依赖注入到UserController。
    26行


    从原本直接相依的User model,改成依赖注入的UserRepository。
    改用这种写法,有几个优点:

    1. /**

    2. * Display a listing of the resource.

    3. *

    4. * @return \Illuminate\Http\Response

    5. */

    6. public function index()

    7. {

    8. $users = $this->userRepository

    9. ->getAgeLargerThan(20);


    10. return view('users.index', compact('users'));

    11. }

    12. /** @var  UserRepository 注入的UserRepository */

    13. protected $userRepository;

    14. /**

    15. * UserController constructor.

    16. *

    17. * @param UserRepository $userRepository

    18. */

    19. public function __construct(UserRepository $userRepository)

    20. {

    21. $this->userRepository = $userRepository;

    22. }

    23. namespace App\Http\Controllers;

    24. use App\Http\Requests;

    25. use MyBlog\Repositories\UserRepository;

    26. class UserController extends Controller

    27. {

    28. /** @var  UserRepository 注入的UserRepository */

    29. protected $userRepository;


    30. /**

    31. * UserController constructor.

    32. *

    33. * @param UserRepository $userRepository

    34. */

    35. public function __construct(UserRepository $userRepository)

    36. {

    37. $this->userRepository = $userRepository;

    38. }


    39. /**

    40. * Display a listing of the resource.

    41. *

    42. * @return \Illuminate\Http\Response

    43. */

    44. public function index()

    45. {

    46. $users = $this->userRepository

    47. ->getAgeLargerThan(20);


    48. return view('users.index', compact('users'));

    49. }

    50. }

    51. /**

    52. * 回传大于?年纪的数据

    53. * @param integer $age

    54. * @return Collection

    55. */

    56. public function getAgeLargerThan($age)

    57. {

    58. return $this->user

    59. ->where('age', '>', $age)

    60. ->orderBy('age')

    61. ->get();

    62. }

    63. /** @var User 注入的User model */

    64. protected $user;

    65. /**

    66. * UserRepository constructor.

    67. * @param User $user

    68. */

    69. public function __construct(User $user)

    70. {

    71. $this->user = $user;

    72. }

    73. app/Repositories/UserRepository.php

    74. namespace MyBlog\Repositories;

    75. use Doctrine\Common\Collections\Collection;

    76. use MyBlog\User;

    77. class UserRepository

    78. {

    79. /** @var User 注入的User model */

    80. protected $user;

    81. /**

    82. * UserRepository constructor.

    83. * @param User $user

    84. */

    85. public function __construct(User $user)

    86. {

    87. $this->user = $user;

    88. }

    89. /**

    90. * 回传大于?年纪的数据

    91. * @param integer $age

    92. * @return Collection

    93. */

    94. public function getAgeLargerThan($age)

    95. {

    96. return $this->user

    97. ->where('age', '>', $age)

    98. ->orderBy('age')

    99. ->get();

    100. }

    101. }

  • 将数据库逻辑写在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.phpnamespace 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好很多了。

不过若在中大型项目,仍有以下问题:

  1. Model已经有原来的责任,若再加上query scope,造成model过于肥大难以维护。

  2. 若数据库逻辑很多,可以拆成多repository,可是却很难拆成多model。

  3. 单元测试困难,必须面临mock Eloquent的问题。

Conclusion

实务上可以一开始1个repository对应1个model,但不用太执着于1个repository一定要对应1个model,可将repository视为逻辑上的数据库逻辑类别即可,可以横跨多个model处理,也可以1个model拆成多个repository,端看需求而定。
Repository使得数据库逻辑从controller或model中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。
Sample Code


评论 (0)

发表评论