1、概述 首先需要声明的是设计模式和使用的框架以及语言是无关的,关键是要理解设计模式背后的原则,这样才能不管你用的是什么技术,都能够在实践中实现相应的设计模式。
按照最初提出者的介绍,repository 是衔接数据映射层和领域层之间的一个纽带,作用相当于一个在内存中的域对象集合。客户端对象把查询的一些实体进行组合,并把它 们提交给 repository。对象能够从 repository 中移除或者添加,就好比这些对象在一个 collection 对象上进行数据操作,同时映射层的代码会对应的从数据库中取出相应的数据。
从概念上讲,repository 是把一个数据存储区的数据给封装成对象的集合并提供了对这些集合的操作。
repository 模式将业务逻辑和数据访问分离开,两者之间通过 repository 接口进行通信,通俗点说,可以把 repository 看做仓库管理员,我们要从仓库取东西(业务逻辑),只需要找管理员要就是了(repository),不需要自己去找(数据访问),具体流程如下图所示:
这种将数据访问从业务逻辑中分离出来的模式有很多好处:
集中数据访问逻辑使代码易于维护 业务和数据访问逻辑完全分离 减少重复代码 使程序出错的几率降低 2、只是接口而已 要实现 repository 模式,首先需要定义接口,这些接口就像laravel 中的契约一样,需要具体类去实现。现在我们假定有两个数据对象 actor 和 film。这两个数据对象上可以进行哪些操作呢?一般情况下,我们会做这些事情:
获取所有记录 获取分页记录 创建一条新的记录 通过主键获取指定记录 通过属性获取相应记录 更新一条记录 删除一条记录 现在你已经意识到如果我们为每个数据对象实现这些操作要编写多少重复代码!当然,对小型项目而言,这不是什么大问题,但如果对大型应用而言,这显然是个坏主意。
现在,如果我们要定义这些操作,需要创建一个 repository 接口:
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('*'));}
3、目录结构 在我们继续创建具体的 repository 实现类之前,让我们先想想要如何组织我们要编写的代码,通常,当我要创建一些类的时候,我喜欢以组件的方式来组织代码,因为我希望这些代码可以很方便地在其他项目中被复用。我为 repository 组件定义的目录结构如下:
但是这也不是一成不变的,要视具体情况来定。比如如果组件包括配置项,或者迁移之类的话,目录结构会有所不同。
在 src 目录下,我创建了三个子目录: contracts 、 eloquent 和 exceptions 。这样命令的原因是显而易见的,一眼就能看出里面存放的是什么类。我们会将接口放在 contracts 目录下, eloquent 目录用于存放实现 repository 接口的抽象类和具体类,而 exceptions 目录用于存放异常处理类。
由于我们创建的是一个扩展包,需要创建一个 composer.json 文件用于定义命名空间映射目录,包依赖以及其它的元数据。下面是我的 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}
正如你所看到的,我们将 bosnadev\repository 映射到了 src 目录。另外,在我们实现 repositoryinterface 之前,由于其位于 contracts 目录下,我们需要为其设置正确的命名空间:
model->get($columns);}
其中 $this->model 是 bosnadev\models\actor 的实例,这样的话,我们还需要定义设置该实例的方法:
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() ,强制在实现类中实现该方法已获取当前实现类对应的模型:
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(); }}
很简单,是吧?现在剩下唯一要做的就是在 actorscontroller 中依赖注入 actorrepository :
actor = $actor; } public function index() { return \response::json($this->actor->all()); }}
5、criteria 查询实现 上面的实现对简单查询而言已经足够了,但是对大型项目而言,有时候需要通过 criteria 创建一些自定义查询获取一些更加复杂的查询结果集。
要实现这一功能,我们首先定义如下这个抽象类:
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; }}
创建一个新的 criteria
有了 criteria 查询,你现在可以更简单的组织 repository 代码:
你可以这样定义 criteria 类:
where('length', '>', 120); return $query; }}
在控制器中使用 criteria
现在我们已经定义了一些简单的 criteria,现在我们来看看如何使用它们。在 repository 中有两种方式使用 criteria,第一种个是使用 pushcriteria 方法:
film = $film; } public function index() { $this->film->pushcriteria(new lengthovertwohours()); return \response::json($this->film->all()); }}
这个方法在你需要多个 criteria 时很有用。然而,如果你只想使用一个 criteria,可以使用 getbycriteria() 方法:
film = $film; } public function index() { $criteria = new lengthovertwohours(); return \response::json($this->film->getbycriteria($criteria)->all()); }}
6、安装依赖包 本教程提及的 repository 实现在 github 上有对应扩展包: https://github.com/bosnadev/repositories 。
你可以通过在项目根目录下的 composer.json 中添加如下这行依赖:
bosnadev/repositories: 0.*
然后运行 composer update 来安装这个 repository 包。