若将数据库逻辑都写在model,会造成model的肥大而难以维护,基于SOLID原则,我们应该使用Repository模式辅助model,将相关的数据库逻辑封装在不同的repository,方便中大型项目的维护。
Version:Laravel 5.1.22
在CRUD中,CUD比较稳定,但R的部分则千变万化,大部分的数据库逻辑都在描述R的部分,若将数据库逻辑写在controller或model都不适当,会造成controller与model肥大,造成日后难以维护。
使用repository之后,model仅当成Eloquent class即可,不要包含数据库逻辑,仅保留以下部分:
注释:因为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
初学者常会在controller直接调用model写数据库逻辑:
public function index()
{
$users = User::where('age', '>', 20)
->orderBy('age')
->get();
return view('users.index', compact('users'));
}
数据库逻辑是要抓20岁以上的数据。
在中大型项目,会有几个问题:
将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仅依赖注入于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好很多了。
不过若在中大型项目,仍有以下问题:
Conclusion
实务上可以一开始1个repository对应1个model,但不用太执着于1个repository一定要对应1个model,可将repository视为逻辑上的数据库逻辑类别即可,可以横跨多个model处理,也可以1个model拆成多个repository,端看需求而定。
Repository使得数据库逻辑从controller或model中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。
Sample Code
完整的示例可以在我的GitHub上找到。