仓库就像是业务内部的数据对象集合, 负责协调业务和数据映射层之间的关系, 客户端对象只需要构造一个清晰的查询请求, 然后提交给仓库就行了. 仓库里的对象可以通过客户端的请求进行增删改查, 客户端可以在某个场景下, 通过一个简单的对象集合或者仓库中映射的代码实现合适的操作
这样做有几个好处:
- 把数据处理逻辑分离使得代码更容易维护
- 数据处理逻辑和业务逻辑分离,可以对这两个代码分别进行测试
- 减少代码重复
- 降低代码出错的几率
- 让controller代码的可读性大大提高
仓库模式把数据访问逻辑和业务逻辑中实体访问分开了, 数据访问逻辑和业务逻辑只能通过接口来进行数据操作.

简单来说, 仓库模式就是一种存放数据访问逻辑的容器, 它向业务层屏蔽了数据访问逻辑的细节, 也可以这样理解, 在不清楚数据层设计结构的情况下, 我们也能按照业务逻辑来访问数据层.
关于接口
仓库模式就是接口, 接口就是一个协议, 协议说明某一个具体的类必须被实现. 我们仔细想一下, 现在我们有两个数据对象Actor和Film, 通常我们对这些对象都有哪些操作呢?
- 获取所有记录
- 获取分页记录
- 添加一条记录
- 根据主键获取记录
- 根据其他属性获取记录
- 更新记录
- 删除记录
现在我们定义这些通用操作, 并且创建一个接口:
目录结构
- interface RepositoryInterface {
- public function all($columns = array('*'));
- public function paginate($perPage = 15, $columns = array('*'));
- public function create(array $data);
- public function update(array $data, $id);
- public function delete($id);
- public function find($id, $columns = array('*'));
- public function findBy($field, $value, $columns = array('*'));
- }
在我们准备设计实现这个接口的仓库之前, 我们先考虑一下如何组织我们的代码, 通常, 我习惯把新功能做成一个component组件, 这样我以后就能重复使用这些代码, 例如这样的文件夹结构:
当然, 你随便, 怎么着都行. 下面我按照我这个组织形式继续讲.
src 文件夹包含了 Contracts, Eloquent, Exceptions 这几个文件夹, 一看文件夹名就知道这几个文件夹放啥了. Contracts 放协议(接口文件), Eloquent 放实现了接口的仓库类, Exceptions 存放异常类
顺便看一下 composer.json 文件的内容吧,
- {
- "name": "bosnadev/repositories",
- "description": "Laravel Repositories",
- "keywords": [
- "laravel",
- "repository",
- "repositories",
- "eloquent",
- "database"
- ],
- "licence": "MIT",
- "authors": [
- {
- "name": "Mirza Pasic",
- "email": "mirza.pasic@edu.fit.ba"
- }
- ],
- "require": {
- "php": ">=5.4.0",
- "illuminate/support": "5.*",
- "illuminate/database": "5.*"
- },
- "autoload": {
- "psr-4": {
- "Bosnadev\\Repositories\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Bosnadev\\Tests\\Repositories\\": "tests/"
- }
- },
- "extra": {
- "branch-alias": {
- "dev-master": "0.x-dev"
- }
- },
- "minimum-stability": "dev",
- "prefer-stable": true
- }
通过 composer.json 我们映射了 src的类到 命名空间Bosnadev\Repository, 所以接口文件的标准格式为:
- <?php namespace Bosnadev\Repositories\Contracts;
- interface RepositoryInterface {
- ...
- }
现在我们开始实现接口
实现接口
使用仓库可以让我们查询数据, 建立业务实体和数据源之间的持续映射关系
当然, 每一个子仓库都应该继承这个抽象的仓库(实现了接口).
现在从接口第一个方法all()开始. 这个方法负责取出实体中的所有记录, 参数$columns是一个数组, 如果发送这个参数的话, 我们会取出$columns中指定的字段, 不传的话默认取出所有字段.
- public function all($columns = array('*')) {
- return $this->model->get($columns);
- }
然后我们设置$this->model属性为我们想要操作的数据对象, 来个抽象仓库类的完整案例:
- <?php namespace Bosnadev\Repositories\Eloquent;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- use Bosnadev\Repositories\Exceptions\RepositoryException;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Container\Container as App;
- /**
- * Class Repository
- * @package Bosnadev\Repositories\Eloquent
- */
- abstract class Repository implements RepositoryInterface {
- /**
- * @var App
- */
- private $app;
- /**
- * @var
- */
- protected $model;
- /**
- * @param App $app
- * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
- */
- public function __construct(App $app) {
- $this->app = $app;
- $this->makeModel();
- }
- /**
- * Specify Model class name
- *
- * @return mixed
- */
- abstract function model();
- /**
- * @return Model
- * @throws RepositoryException
- */
- public function makeModel() {
- $model = $this->app->make($this->model());
- if (!$model instanceof Model)
- throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
- return $this->model = $model;
- }
- }
我们定一个这个抽象类之后, 现在我们实现一个具体的仓库类, 只需要实现 model()方法就行了
- <?php namespace App\Repositories;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- use Bosnadev\Repositories\Eloquent\Repository;
- class ActorRepository extends Repository {
- /**
- * Specify Model class name
- *
- * @return mixed
- */
- function model()
- {
- return 'Bosnadev\Models\Actor';
- }
- }
现在实现接口中的其他方法:
- <?php namespace Bosnadev\Repositories\Eloquent;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- use Bosnadev\Repositories\Exceptions\RepositoryException;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Container\Container as App;
- /**
- * Class Repository
- * @package Bosnadev\Repositories\Eloquent
- */
- abstract class Repository implements RepositoryInterface {
- /**
- * @var App
- */
- private $app;
- /**
- * @var
- */
- protected $model;
- /**
- * @param App $app
- * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
- */
- public function __construct(App $app) {
- $this->app = $app;
- $this->makeModel();
- }
- /**
- * Specify Model class name
- *
- * @return mixed
- */
- abstract function model();
- /**
- * @param array $columns
- * @return mixed
- */
- public function all($columns = array('*')) {
- return $this->model->get($columns);
- }
- /**
- * @param int $perPage
- * @param array $columns
- * @return mixed
- */
- public function paginate($perPage = 15, $columns = array('*')) {
- return $this->model->paginate($perPage, $columns);
- }
- /**
- * @param array $data
- * @return mixed
- */
- public function create(array $data) {
- return $this->model->create($data);
- }
- /**
- * @param array $data
- * @param $id
- * @param string $attribute
- * @return mixed
- */
- public function update(array $data, $id, $attribute="id") {
- return $this->model->where($attribute, '=', $id)->update($data);
- }
- /**
- * @param $id
- * @return mixed
- */
- public function delete($id) {
- return $this->model->destroy($id);
- }
- /**
- * @param $id
- * @param array $columns
- * @return mixed
- */
- public function find($id, $columns = array('*')) {
- return $this->model->find($id, $columns);
- }
- /**
- * @param $attribute
- * @param $value
- * @param array $columns
- * @return mixed
- */
- public function findBy($attribute, $value, $columns = array('*')) {
- return $this->model->where($attribute, '=', $value)->first($columns);
- }
- /**
- * @return \Illuminate\Database\Eloquent\Builder
- * @throws RepositoryException
- */
- public function makeModel() {
- $model = $this->app->make($this->model());
- if (!$model instanceof Model)
- throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
- return $this->model = $model->newQuery();
- }
- }
现在我们实现控制器试试
- <?php namespace App\Http\Controllers;
- use App\Repositories\ActorRepository as Actor;
- class ActorsController extends Controller {
- /**
- * @var Actor
- */
- private $actor;
- public function __construct(Actor $actor) {
- $this->actor = $actor;
- }
- public function index() {
- return \Response::json($this->actor->all());
- }
- }
标准查询方式
刚刚只是一些简单的操作, 在大型项目中, 我们有可能会定义一些其他的复杂操作.
为了实现这个, 我们要先顶一个抽象类, 用来扩展 repository 的查询能力.
- <?php namespace Bosnadev\Repositories\Criteria;
- use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- abstract class Criteria {
- /**
- * @param $model
- * @param RepositoryInterface $repository
- * @return mixed
- */
- public abstract function apply($model, Repository $repository);
- }
这个方法是用来处理查询请求的, 现在我们要扩展 repository, 所以我们再定义一个接口文件:
- <?php namespace Bosnadev\Repositories\Contracts;
- use Bosnadev\Repositories\Criteria\Criteria;
- /**
- * Interface CriteriaInterface
- * @package Bosnadev\Repositories\Contracts
- */
- interface CriteriaInterface {
- /**
- * @param bool $status
- * @return $this
- */
- public function skipCriteria($status = true);
- /**
- * @return mixed
- */
- public function getCriteria();
- /**
- * @param Criteria $criteria
- * @return $this
- */
- public function getByCriteria(Criteria $criteria);
- /**
- * @param Criteria $criteria
- * @return $this
- */
- public function pushCriteria(Criteria $criteria);
- /**
- * @return $this
- */
- public function applyCriteria();
- }
现在让 repository 类实现这个接口:
- <?php namespace Bosnadev\Repositories\Eloquent;
- use Bosnadev\Repositories\Contracts\CriteriaInterface;
- use Bosnadev\Repositories\Criteria\Criteria;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- use Bosnadev\Repositories\Exceptions\RepositoryException;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Support\Collection;
- use Illuminate\Container\Container as App;
- /**
- * Class Repository
- * @package Bosnadev\Repositories\Eloquent
- */
- abstract class Repository implements RepositoryInterface, CriteriaInterface {
- /**
- * @var App
- */
- private $app;
- /**
- * @var
- */
- protected $model;
- /**
- * @var Collection
- */
- protected $criteria;
- /**
- * @var bool
- */
- protected $skipCriteria = false;
- /**
- * @param App $app
- * @param Collection $collection
- * @throws \Bosnadev\Repositories\Exceptions\RepositoryException
- */
- public function __construct(App $app, Collection $collection) {
- $this->app = $app;
- $this->criteria = $collection;
- $this->resetScope();
- $this->makeModel();
- }
- /**
- * Specify Model class name
- *
- * @return mixed
- */
- public abstract function model();
- /**
- * @param array $columns
- * @return mixed
- */
- public function all($columns = array('*')) {
- $this->applyCriteria();
- return $this->model->get($columns);
- }
- /**
- * @param int $perPage
- * @param array $columns
- * @return mixed
- */
- public function paginate($perPage = 1, $columns = array('*')) {
- $this->applyCriteria();
- return $this->model->paginate($perPage, $columns);
- }
- /**
- * @param array $data
- * @return mixed
- */
- public function create(array $data) {
- return $this->model->create($data);
- }
- /**
- * @param array $data
- * @param $id
- * @param string $attribute
- * @return mixed
- */
- public function update(array $data, $id, $attribute="id") {
- return $this->model->where($attribute, '=', $id)->update($data);
- }
- /**
- * @param $id
- * @return mixed
- */
- public function delete($id) {
- return $this->model->destroy($id);
- }
- /**
- * @param $id
- * @param array $columns
- * @return mixed
- */
- public function find($id, $columns = array('*')) {
- $this->applyCriteria();
- return $this->model->find($id, $columns);
- }
- /**
- * @param $attribute
- * @param $value
- * @param array $columns
- * @return mixed
- */
- public function findBy($attribute, $value, $columns = array('*')) {
- $this->applyCriteria();
- return $this->model->where($attribute, '=', $value)->first($columns);
- }
- /**
- * @return \Illuminate\Database\Eloquent\Builder
- * @throws RepositoryException
- */
- public function makeModel() {
- $model = $this->app->make($this->model());
- if (!$model instanceof Model)
- throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
- return $this->model = $model->newQuery();
- }
- /**
- * @return $this
- */
- public function resetScope() {
- $this->skipCriteria(false);
- return $this;
- }
- /**
- * @param bool $status
- * @return $this
- */
- public function skipCriteria($status = true){
- $this->skipCriteria = $status;
- return $this;
- }
- /**
- * @return mixed
- */
- public function getCriteria() {
- return $this->criteria;
- }
- /**
- * @param Criteria $criteria
- * @return $this
- */
- public function getByCriteria(Criteria $criteria) {
- $this->model = $criteria->apply($this->model, $this);
- return $this;
- }
- /**
- * @param Criteria $criteria
- * @return $this
- */
- public function pushCriteria(Criteria $criteria) {
- $this->criteria->push($criteria);
- return $this;
- }
- /**
- * @return $this
- */
- public function applyCriteria() {
- if($this->skipCriteria === true)
- return $this;
- foreach($this->getCriteria() as $criteria) {
- if($criteria instanceof Criteria)
- $this->model = $criteria->apply($this->model, $this);
- }
- return $this;
- }
- }
添加一些查询规则
通过添加的标准查询, 我们能更容易的组织我们的代码, repository 也不至于会有一堆臃肿的代码
添加的查询规则文件
- <?php namespace App\Repositories\Criteria\Films;
- use Bosnadev\Repositories\Contracts\CriteriaInterface;
- use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
- use Bosnadev\Repositories\Contracts\RepositoryInterface;
- class LengthOverTwoHours implements CriteriaInterface {
- /**
- * @param $model
- * @param RepositoryInterface $repository
- * @return mixed
- */
- public function apply($model, Repository $repository)
- {
- $query = $model->where('length', '>', 120);
- return $query;
- }
- }
在控制器中使用这些标准查询
有两种方式使用, 第一种:
- <?php namespace App\Http\Controllers;
- use App\Repositories\Criteria\Films\LengthOverTwoHours;
- use App\Repositories\FilmRepository as Film;
- class FilmsController extends Controller {
- /**
- * @var Film
- */
- private $film;
- public function __construct(Film $film) {
- $this->film = $film;
- }
- public function index() {
- $this->film->pushCriteria(new LengthOverTwoHours());
- return \Response::json($this->film->all());
- }
- }
使用这种方法你能添加任意数量的标准查询规则, 但是如果你只想添加一个, 那么你可以使用这个方法getByCriteria():
- <?php namespace App\Http\Controllers;
- use App\Repositories\Criteria\Films\LengthOverTwoHours;
- use App\Repositories\FilmRepository as Film;
- class FilmsController extends Controller {
- /**
- * @var Film
- */
- private $film;
- public function __construct(Film $film) {
- $this->film = $film;
- }
- public function index() {
- $criteria = new LengthOverTwoHours();
- return \Response::json($this->film->getByCriteria($criteria)->all());
- }
- }
安装这个包
在你的composer.json 文件中添加"bosnadev/repositories": "0.*", 然后运行composer update.
总结
在应用中使用 repository 设计模式有很多好处, 最基本的就是能减少你的代码量, 是你的代码更容易维护, 测试和扩展.
从程序的设计架构角度来说, 你的控制器不需要知道你在哪儿存储的数据, 这些数据怎么来的, 这样是非常漂亮的结构.
有一个 laravel 实现的包推荐一下, 可能大多数人都知道吧
https://github.com/andersao/l5-repository