## 如何使用 Repository 模式
若将数据库逻辑都写在 Model 里,会造成 model 代码的臃肿难以维护,基于 SOLID 原则,我们应该使用 **Repository** 模式辅助 Model,将相关的数据库逻辑封装在不同的 Repository,方便后期项目的维护。
### Laravel 框架版本
Laravel 5.4.17
### 数据库逻辑
在 CURD 中,CUR 比较稳定,但 Read 的部分则变化万千,大部分的数据库逻辑都在描述 Read 部分,若将数据库逻辑写在 Controller 或 Model 都不合适,会造成 Controller 或 Model 代码臃肿,如后难以维护。
### Model
使用 Repository 模式之后,Model 仅仅当成 Eloquent Class 即可,不需要包含数据库逻辑,仅保留如下部分:
* Property: 如 `$table` `$fillable` ..
* Mutator: 包括 mutator 与 accessor
* Method: relation 类的方法,比如使用 `hasMany()` 与 `belongsTo()`
单一对应关系:
* hasOne
* belongsTo
* morphTo
* morphOne
多个对应关系指的是使用以下关键词定义的关联模型:
* hasMany
* belongsToMany
* morphMany
* morphToMany
* morphedByMany
> 因为 Eloquent 会根据数据库字段动态的产生 property 与 method等,若使用 [Laravel IDE Helper](https://github.com/barryvdh/laravel-ide-helper) ,会直接在Model加上 `@property` 与 `@method` 描述model的动态 proerty 与 method。 如下`app\User.php`中安装完`Laravel IDE Helper`后执行`php artisan ide-helper:models`后自动生成的内容:
```
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* App\User
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
* @mixin \Eloquent
*/
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
```
### Repository
在开发时常常会在 Controller 直接调用 Model 写数据库逻辑,如下:获取数据库中用户 `age>20`的数据。
```
public function index()
{
return User::where('age','>',20)->orderBy('age')->get();
}
```
这样写逻辑会有几个问题:
* 将数据库逻辑写在 Controller,造成 Controller 代码臃肿难以维护。
* 违反了 SOLID 的单一职责原则,数据库逻辑不应该写在 Controller 中。
* Controller 直接操作 Model,使得对 Controller 做单元测试困难。
比较好的方式是使用 Repository:
* 将 Model 依赖注入到 Repository。
* 将数据库逻辑写在 Repository。
* 将 Repository 依赖注入到 Service。
`app/Repositories/UserRepostitory.php` 中的内容:
```
<?php
namespace App\Repositories;
use App\User;
/**
* Class UserRepository
* @package App\Repositories
*/
class UserRepository
{
/**
* @var User
*/
private $user;
/**
* UserRepository constructor.
* @param $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @param $age
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAgeLargerThan($age)
{
return $this->user
->where('age', '>', $age)
->orderBy('age')
->get();
}
}
```
在控制器`app\Controllers\UserController.php`中使用依赖注入:
```
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;
/**
* Class UserController
*
* @package App\Http\Controllers
*/
class UserController extends Controller
{
/**
* @var \App\Repositories\UserRepository
*/
protected $userRepository;
/**
* UserController constructor.
* @param $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function index()
{
return $this->userRepository->getAgeLargerThan(20);
}
}
```
将相依的 `UserRepository` 依賴注入到 `UserController`,并从原本直接依赖 `User` Model改成依赖注入的 `UserRepository`
#### 优点
* 将数据库逻辑写在 Repository 里,解决了 Controller 代码臃肿的问题。
* 符合 SOLID 的单一职责原则:数据库逻辑写在 Repository 里,没写在 Controller 里。
* 符合 SOLID 的依赖反转原则:Controller 并非直接相依与 Repositroy,而是将 Repository 依赖注入进 Controller。
> 实际上建议 Repository 仅依赖注入进 Service,而不是直接注入在 Controller。
#### 是否该建立 Repository Interface?
理论上使用依赖注入时,应该使用 Interface ,不过 Interface 目的在于更换数据库,让代码达到开放封闭的要求,但是实际上要更改 Reposiroty 的机会也不多,除非是从 MySQL 更换到 MongoDB,此时就应该建立 Repository Interface。
不过由于我们使用了依赖注入,将来要从 Class 改成 Interface 也很方便,只要在 Constructor 的 type hint 改成 Interface 即可,维护成本很低,所以在此大可使用 Repository Class 即可,不一定得用Interface而造成 Over Design,等真正需要修改时,再重构 Interface 即可。
#### 是否该使用 Query Scope?
Laravel 4.2 就有 QueryScope,到 Laravel5.1 都还保留着,它让我们可以将逻辑代码写在 Model ,解决了维护与重复使用的问题。
如 `app/User.php` 里的代码:
```
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* App\User
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
* @mixin \Eloquent
*/
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @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');
}
}
```
QueryScope 必须以 `scope`开头,第一个参数为 queryBuilder,一定要加上;第二个参数以后为自己要传入的参数。
由于回传必须是一个 queryBuilder ,因此不需要加上 `get()`
`app/Controllers/UserController.php` 中使用代码:
```
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\User;
use Illuminate\Http\Request;
/**
* Class UserController
*
* @package App\Http\Controllers
*/
class UserController extends Controller
{
/**
* @var \App\Repositories\UserRepository
*/
protected $userRepository;
/**
* UserController constructor.
* @param $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function index()
{
return User::getAgerLargerThan(20)->get();
}
}
```
在 Controller 中使用 QueryScope 时,不需要加上 Prefix,由于其本质是 queryBuilder,所以还要加上 `get()` 才能获得 Conllection 数据。
由于 QueryScope 是写在 Model,不是写在 Controller,所以基本上解决了 Controller 臃肿违反 SOLID 的单一职责原则的问题, Controller 也可以重复使用 QueryScope ,已经比直接将资料库逻辑写在 Controlelr 中好很多。
不过若在中大型项目中,仍然有以下问题:
* Model 已经有原来的责任,若再加上 queryScope,造成 Model 过于臃肿难以维护。
* 若数据库逻辑很多,可能拆成多个 Repository,可是确很难拆成多个 Model。
* 单元测试困难,必须面临 mock Eloquent 的问题。
### 最后
实际开发时,可以一开始 1 个 Repository 对应 1 个 Model,但是也不必太过执着于 1 个 Repository,一定要对应 1 个 Model,可将 Repository 视为逻辑上的数据库逻辑类别即可,可以横跨多个Model处理,也可以 1 个 Model 拆成多个 Repository,视情况而定。
Repository 使得数据库逻辑从 Controller 或 Model 中解放,不仅更容易维护、更容易拓展、更容易重复使用,也更容易测试。
- 介绍
- Laravel5发送邮件使用Service隔离业务
- 如何使用Repository模式
- 如何使用Service模式
- 如何使用Presenter模式
- Laravel 5.* 执行迁移文件报错:Specified key was too long error
- EloquentORM关联关系
- EloquentORM关联关系之一对一
- EloquentORM关联关系之一对多
- EloquentORM关联关系之远层一对多
- EloquentORM关联关系之多对多
- EloquentORM关联关系之多态关联
- EloquentORM关联关系之多对多多态关联
- Laravel测试
- Laravel中涉及认证跳转地址的修改的地方
- Laravel中Collection的基本使用
- all
- avg
- chuck
- collapse
- combine
- contains
- containsStrict
- count
- diff
- diffAssoc
- diffKeys
- each
- every
- except
- filter
- first
- flatMap
- flatten
- flip
- forget
- forPage
- get
- groupBy
- has
- implode
- intersect
- intersectKey
- isEmpty
- isNotEmpty
- keyBy
- keys
- last
- map
- mapWithKeys
- max
- median
- merge
- min
- mode
- nth
- only
- partition
- pipe
- pluck
- pop
- prepend
- pull
- push
- put
- random
- reduce
- reject
- reverse
- search
- shift
- shuffle
- slice
- sort
- sortBy
- sortByDesc
- splice
- split
- sum
- take
- tap
- times
- toArray
- toJson
- transform
- union
- unique
- uniqueStrict
- values
- when
- where
- whereStrict
- whereIn
- whereInStrict
- whereNotIn
- whereNotInStrict
- zip
- Laravel中Collection的实际使用
- collection中sum求和
- collection格式化计算数据
- collection格式化计算数据计算github事件得分总和
- collection格式化markdown数据列表
- collection格式化计算两个数组的数据
- collection中reduce创建lookup数组
- TODO