1. 程式人生 > >Laravel框架中如何使用 Repository 模式

Laravel框架中如何使用 Repository 模式

源:http://www.sangeng.org/blog/index/detail/id/518.html

若將資料庫邏輯都寫在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.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歲以上的資料。
    在中大型專案,會有幾個問題:

    1. 將資料庫邏輯寫在controller,造成controller的肥大難以維護。
    2. 違反SOLID的單一職責原則:資料庫邏輯不應該寫在controller。
    3. controller直接相依於model,使得我們無法對controller做單元測試。
      比較好的方式是使用repository:
    4. 將model依賴注入到repository。
    5. 將資料庫邏輯寫在repository。
    6. 將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();
      }
      }
      

      第 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.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'));
      }
      }
      

      第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好很多了。

不過若在中大型專案,仍有以下問題:

  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
完整的示例可以在我的GitHub上找到。