php语言repository,PHP 设计模式 - 资源库模式(Repository)

1. 模式定义

Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。

它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。

Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。

Repository 模式是架构模式,在设计架构时,才有参考价值。应用 Repository 模式所带来的好处,远高于实现这个模式所增加的代码。只要项目分层,都应当使用这个模式。

2. UML类图

444b262b79b1

image.png

3. 示例代码

Storage 接口类

namespace DesignPattern\Other\Repository;

/**

* Storage接口

*

* 该接口定义了访问数据存储器的方法

* 具体的实现可以是多样化的,比如内存、关系型数据库、NoSQL数据库等等

*

* @package DesignPatterns\Repository

*/

interface Storage

{

/**

* 持久化数据方法

* 返回新创建的对象ID

*

* @param array() $data

* @return int

*/

public function persist($data);

/**

* 通过指定id返回数据

* 如果为空返回null

*

* @param int $id

* @return array|null

*/

public function retrieve($id);

/**

* 通过指定id删除数据

* 如果数据不存在返回false,否则如果删除成功返回true

*

* @param int $id

* @return bool

*/

public function delete($id);

}

MemoryStorage.php

namespace DesignPattern\Other\Repository;

/**

* MemoryStorage类

* @package DesignPatterns\Repository

*/

class MemoryStorage implements Storage

{

private $data;

private $lastId;

public function __construct()

{

$this->data = array();

$this->lastId = 0;

}

/**

* {@inheritdoc}

*/

public function persist($data)

{

$this->data[++$this->lastId] = $data;

return $this->lastId;

}

/**

* {@inheritdoc}

*/

public function retrieve($id)

{

return isset($this->data[$id]) ? $this->data[$id] : null;

}

/**

* {@inheritdoc}

*/

public function delete($id)

{

if (!isset($this->data[$id])) {

return false;

}

$this->data[$id] = null;

unset($this->data[$id]);

return true;

}

}

Post.php

namespace DesignPattern\Other\Repository;

/**

* Post 类

* @package DesignPatterns\Repository

*/

class Post

{

/**

* @var int

*/

private $id;

/**

* @var string 标题

*/

private $title;

/**

* @var string 具体文本

*/

private $text;

/**

* @var string 作者

*/

private $author;

/**

* @var \DateTime 创建时间

*/

private $created;

/**

* @param int $id

*/

public function setId($id)

{

$this->id = $id;

}

/**

* @return int

*/

public function getId()

{

return $this->id;

}

/**

* @param string $author

*/

public function setAuthor($author)

{

$this->author = $author;

}

/**

* @return string

*/

public function getAuthor()

{

return $this->author;

}

/**

* @param \DateTime $created

*/

public function setCreated($created)

{

$this->created = $created;

}

/**

* @return \DateTime

*/

public function getCreated()

{

return $this->created;

}

/**

* @param string $text

*/

public function setText($text)

{

$this->text = $text;

}

/**

* @return string

*/

public function getText()

{

return $this->text;

}

/**

* @param string $title

*/

public function setTitle($title)

{

$this->title = $title;

}

/**

* @return string

*/

public function getTitle()

{

return $this->title;

}

}

PostRepository.php

namespace DesignPattern\Other\Repository;

/**

* Post 对应的 Repository

* 该类介于数据实体层(Post) 和访问对象层(Storage)之间

*

* Repository 封装了持久化对象到数据存储器以及在展示层显示面向对象的视图操作

*

* Repository 还实现了领域层和数据映射层的分离和单向依赖

*

* PostRepository 类

* @package DesignPatterns\Repository

*/

class PostRepository

{

private $persistence;

public function __construct(Storage $persistence)

{

$this->persistence = $persistence;

}

/**

* 通过指定id返回Post对象

*

* @param int $id

* @return Post|null

*/

public function getById($id)

{

$arrayData = $this->persistence->retrieve($id);

if (is_null($arrayData)) {

return null;

}

$post = new Post();

$post->setId($id);

$post->setAuthor($arrayData['author']);

$post->setCreated($arrayData['created']);

$post->setText($arrayData['text']);

$post->setTitle($arrayData['title']);

return $post;

}

/**

* 保存指定对象并返回

*

* @param Post $post

* @return Post

*/

public function save(Post $post)

{

$id = $this->persistence->persist(array(

'author' => $post->getAuthor(),

'created' => $post->getCreated(),

'text' => $post->getText(),

'title' => $post->getTitle()

));

$post->setId($id);

return $post;

}

/**

* 删除指定的 Post 对象

*

* @param Post $post

* @return bool

*/

public function delete(Post $post)

{

return $this->persistence->delete($post->getId());

}

}

单元测试

namespace DesignPattern\Tests;

use DesignPattern\Other\Repository\MemoryStorage;

use DesignPattern\Other\Repository\Post;

use DesignPattern\Other\Repository\PostRepository;

use PHPUnit\Framework\TestCase;

/**

* 测试资源库模式

* Class DataMapperTest

* @package Creational\Singleton\Tests

*/

class RepositoryTest extends TestCase

{

/** @var PostRepository */

protected $postRepository;

protected function setUp(): void

{

parent::setUp();

$this->postRepository = new PostRepository(new MemoryStorage());

}

public function getPost($i)

{

$post = new Post();

$post->setTitle("博文" . $i);

$post->setText("博文内容博文内容" . $i);

$post->setAuthor("Sylvia");

$post->setCreated(date('Y-m-d H:i:s'));

return $post;

}

public function getNewPost()

{

return array(array(self::getPost(1), self::getPost(2)));

}

/**

* @param Post $post

*

* @dataProvider getNewPost

*

*/

public function testSave(Post $post)

{

$result1 = $this->postRepository->save($post);

$this->assertIsObject($result1);

$result2 = $this->postRepository->getById($result1->getId());

$this->assertEquals($result1,$result2);

}

}

在laravel 5 中使用资源库模式

前言

最近,关于软件设计模式的讨论越来越多,最常见的问题之一是“如何在某种技术上使用某种模式”。就Laravel 与 资源库模式(Repository pattern)来说,我经常看到诸如:“我如何在Laravel 4 中使用资源库模式?” 或如今的“... laravel 5”中。你必须记住的重要的一点是:设计模式不依赖与特定的技术、框架或者编程语言。

介绍

如果你真的理解了资源库模式,那么你将使用哪种框架或者编程语言都没有关系。重要的是你理解了资源库模式背后的原理。你就能够用任何所需的技术来实现它。考虑到这一点,让我们开始学习资源库模式 的定义:

资源库介于领域(??)与数据映射层(数据访问层)之间,就像内存中域对象集合一样。

“A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes.”

Repository pattern separates the data access logic and maps it to the business entities in the business logic. Communication between the data access logic and the business logic is done through interfaces.

444b262b79b1

image.png

To put it simply, Repository pattern is a kind of container where data access logic is stored. It hides the details of data access logic from business logic. In other words, we allow business logic to access the data object without having knowledge of underlying data access architecture.

The separation of data access from business logic have many benefits. Some of them are:

Centralization of the data access logic makes code easier to maintain

Business and data access logic can be tested separately

Reduces duplication of code

A lower chance for making programming errors

It’s all about interfaces

Repository pattern is all about interfaces. An interface acts like a contract which specify what an concrete class must implement. Let’s think a little bit. If we have two data objects Actor and Film, what are common set of operations that can be applied to these two data objects? In most situations we want to have the following operations:

Get all records

Get paginated set of records

Create a new record

Get record by it’s primary key

Get record by some other attribute

Update a record

Delete a record

Can you see now how much duplicated code would we have if we implement this for each data object? Sure, for small projects it’s not a big problem, but for large scale applications it’s a bad news.

Now when we have defined common operations, we can create an interface:

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('*'));

}

Directory structure

Before we continue with creating concrete repository class that will implement this interface, let’s think a bit how we want to organise our code. Usually, when I create something, I like to think component way since I want to be able to reuse that code in other projects. My simple directory structure for the repositories component looks like this:

444b262b79b1

image.png

But it can be different, for example if component have configuration options, or migrations, etc.

Inside src directory I have three other directories: Contracts, Eloquent and Exceptions. As you can see, the folder names are pretty convenient for what we want to put there. In Contracts folder we put interfaces, or contracts as we call them earlier. Eloquent folder contains abstract and concrete repository class that implements contract. In Exceptions folder we put exceptions classes.

Since we are creating a package we need to create composer.json file where we define a mapping for namespaces to specific directories, package dependencies and other package metadata. Here is the content of composer.json for this package:

{

"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

}

As you can see, we mapped namespace Bosnadev\Repository to the src directory. Another thing, before we start to implement RepositoryInterface, since it is located in the Contracts folder, we need to set correct namespace for it:

interface RepositoryInterface {

...

}

We are now ready to start with the implementation of this contract.

A Repository Implementation

Using repositories enables us to query the data source for the data, map the data to a business entity and persist changes in the business entity to the data source:

444b262b79b1

image.png

Of course, each concrete child repository should extend our abstract repository, which implements RepositoryInterface contract. Now, how would you implement this contract? Take a look at first method. What can you tell about it just by looking at it?

First method in our contract is conveniently named all() . It’s duty is to fetch all records for the concrete entity. It accepts only one parameter $columns which must be an array. This parameter is used, as its name suggests, to specify what columns we want to fetch from the data source, and by default we fetch them all.

For specific entity, this method could look like this:

public function all($columns = array('*')) {

return Bosnadev\Models\Actor::get($columns);

}

But we want to make it generic, so we can use it wherever we want:

public function all($columns = array('*')) {

return $this->model->get($columns);

}

In this case $this->model is an instance of Bosnadev\Models\Actor . Thus, somewhere in the repository we need to create a new instance of the given model. Here is one solution how you can implement this:

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;

}

}

Since we declared class as abstract, it means it must be extended by concrete child class. By declaring model() method as abstract we force the user to implement this method in the concrete child class. For example:

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';

}

}

Now we can implement the rest of the contract methods:

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();

}

}

Pretty easy, right? Only thing left now is to inject ActorRepository in the ActorsController, or our business side of application:

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());

}

}

Criteria Queries

As you can imagine, these basic actions are just enough for simple querying. For larger applications you’ll most definitely need to make some custom queries to fetch more specific data set defined by some criteria.

To achieve this, we begin with defining what child (clients) criteria must implement. In other words, we’ll create an abstract non instantiable class with just one method in it:

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);

}

This method will hold criteria query which will be applied in the Repository class on the concrete entity. We also need to extend our Repository class a bit to cover criteria queries. But first, let’s create a new contract for the Repository class:

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();

}

Now we can extend functionality of our Repository class by implementing CriteriaInterface contract:

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;

}

}

Creating A New Criteria

With criteria queries, you can now organise your repositories more easily. Your repositories do not need to be thousands of lines long.

444b262b79b1

image.png

Your criteria class can look like this:

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;

}

}

Using Criteria In The Controller

Now when we have simple criteria, let’s see how we can use it. There is a two ways how you can apply the criteria on the repository. First is by using pushCriteria() method:

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());

}

}

This method is useful if you need to apply multiple criteria, you can stack them as you wish. However, if you need to apply just one criteria, you can use getByCriteria() method:

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());

}

}

Package Installation

You can install this package by adding this dependency in your composer require section:

"bosnadev/repositories": "0.*"

and just run composer update afterwards.

Conclusion

Using repositories in your application have multiple benefits. From basic things like reducing code duplication and preventing you to make programming errors to making you application easier to extend, test and maintain.

From architectural point of view you managed to separate concerns. Your controller doesn’t need to know how and where you store the data. Simple and beautiful. Abstract.

You can find this package on Github, where you can check for latest updates and bug fixes. I also plan to add new features like eager loading, caching and some configs so stay tuned by staring the repository. However, if you want to contribute in the development just fork the repository and send PR.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值