用户/aaronjan / housekeeper
强大的、简单的Laravel(>=5.1)存储库模式实现,附带测试。
Requires
- php: >=5.5.0
Requires (Dev)
- mockery/mockery: ^0.9.4
- phpunit/phpunit: ~4.0
This package is not auto-updated.
Last update: 2020-01-16 23:20:57 UTC
README
Housekeeper - Laravel
经过近六个月的开发、测试和打磨,Housekeeper 2的第一个稳定版本终于发布!
Housekeeper
旨在成为最酷、最实用的Repository Pattern
实现,欢迎任何有用的建议和PR。
提高单元测试代码覆盖率是一个持续进行中的工作(还有很多工作要做),但是有一套本地运行的集成测试覆盖了大部分代码。
简介
Housekeeper
是Laravel的一个灵活而强大的Repository Pattern
实现。除了基本的Repository Pattern
和优雅的语法外,Housekeeper
还具有如注入系统
、自动启动方法等功能,将为您带来无限可能。Housekeeper
的目标是让您摆脱繁琐的DAL
代码,更直观地编写代码。
章节
Repository Pattern和Housekeeper
Repository Pattern
是一种软件设计模式。简单来说,它意味着将您的数据交互代码封装为属于不同类的(基于数据域)方法,我们将此类称为Repository
。当您的业务逻辑层需要访问数据,如数据库中的文章条目时,它应该向Article Repository
请求,而不是编写直接处理数据库的查询。
好吧,但是...我已经有了Eloquent,为什么不用它呢?
当然可以!但也有人不喜欢Active Record,它对他们来说并不合适,对于这些人来说,Repository Pattern更有意义。此外,您可以在您的仓库类中编写更易于表达的方法,如getActivatedUsers(),并且您可以很容易地对它们进行测试。
更重要的是,Housekeeper
是Repository Pattern
的更好版本(在某些方面),您可以在下面的内容中了解更多。
Housekeeper
喜欢Eloquent。大多数查询API与Eloquent的相同,因此您可以在不需要学习任何东西的情况下使用它们,并且返回值也像Eloquent一样。
安装
要求
PHP
>= 5.5
和 Laravel
>= 5.1
通过Composer安装
$ composer require aaronjan/housekeeper ~2.3
或在您的composer.json
文件中添加以下内容
"require": {
"aaronjan/housekeeper": "~2.3"
}
然后执行控制台命令
$ composer install
在Composer
运行完成后,将HousekeeperServiceProvider
添加到config/app.php
中的提供者。
<?php // ... 'providers' => [ // ... // Add this: \Housekeeper\Providers\HousekeeperServiceProvider::class, // ... ], // ...
为Housekeeper
创建一个配置文件可以让你调整一些设置
$ artisan vendor:publish --provider=Housekeeper\\Providers\\HousekeeperServiceProvider --tag=config
完成了!现在你可以创建一个仓库
$ artisan housekeeper:make UserRepository
TL;DR
如果你有独到的见解,本节将用最简单的话告诉你如何使用Housekeeper
。
-
不要写
类构造函数
,使用boot
方法代替,支持类型提示
。 -
任何以
boot
开头且后面跟一个大写字母(例如,bootForInject
)的公共方法,在类初始化期间将被调用,也支持类型提示
。 -
如果你想在不同仓库中的某些方法之前/之后执行某些操作,将这些逻辑封装为
注入
,然后将其注入到你的仓库中。 -
通过使用上述两个功能,你可以编写
Trait
来注入Injection
,并在你的仓库中使用它。
看一个例子
<?php namespace App\Repositories\Injections; use Housekeeper\Contracts\Injection\Basic as BasicInjectionContract; use Housekeeper\Contracts\Injection\Before as BeforeInjectionContract; use Housekeeper\Contracts\Flow\Before as BeforeFlowContract; class LogTimeBefore implements BasicInjectionContract, BeforeInjectionContract { public function handle(BeforeFlowContract $beforeFlow) { \Log::alert('wow'); } }
<?php namespace App\Repositories\Abilities; use App\Repositories\Injections\LogTimeBefore; trait TimeLogger { public function bootTimeLogger() { $this->injectIntoBefore(new LogTimeBefore()); } }
<?php namespace App\Repositories; use Housekeeper\Repository; use App\Repositories\Abilities\TimeLogger; class MyRepository extends Repository { use TimeLogger; // ... }
<?php class ArticleRepository extends \Housekeeper\Repository { protected function model() { return Article::class; } public function getByName($name) { return $this->simpleWrap(Action::READ, [$this, '_getByName']); } protected function _getByName($name) { return $this->getModel() // this function give you an Eloquent / Builder instance ->where('name', '=', $name) ->get(); } }
就是这样,看看能力
的代码了解更多用法,祝你好玩!
特性
无扩展 & 流
通常,仓库模式
与装饰器模式
一起使用。例如,你有一个与数据源直接交互的仓库类,后来你决定在它上面添加缓存逻辑,因此你不必修改仓库类本身,而是创建一个新的扩展它的类,整个过程可能看起来像这样
<?php class Repository { public function findBook($id) { return Book::find($id); } }
<?php use Cache; class CachedRepository extends Repository { public function findBook($id) { return Cache::remember("book_{$id}", 60, function () use ($id) { return parent::findBook($id); }); } }
Repository类
是底层,CachedRepository类
是基于前者的另一层,因此每一层只做一件事(SRP:单一职责原则)。
这是一个很好的方法。但是Housekeeper
希望通过使用流
来以更少的代码解决这个问题。
流
是每个方法执行过程中的三个阶段,它们是:Before
、After
和Reset
。在Housekeeper仓库
中的每个方法都应该被包装,以便它可以通过这些流
。这里是一个例子
<?php class ArticleRepository extends \Housekeeper\Repository { protected function model() { return Article::class; } public function getByName($name) { return $this->simpleWrap(Action::READ, [$this, '_getByName']); } protected function _getByName($name) { return $this->getModel() // this function give you an Eloquent / Builder instance ->where('name', '=', $name) ->get(); } }
为什么有两个具有类似名称的方法?嗯,getByName
方法基本上是核心方法_getByName
的一个配置和API提示,它通过调用带有Callable
的simpleWrap
来包装核心方法,Callable
是[$this, '_getByName']
,它说明这个方法所做的操作是读取
数据(Action::READ
),整个读取逻辑都在_getByName
方法中。
你不需要担心方法参数,Housekeeper
会处理这些。实际上,你甚至不需要写[$this, '_getByName']
,因为在Housekeeper
中有一个约定(方法名前有一个下划线)。
<?php public function getByName($name) { return $this->simpleWrap(Action::READ); }
让我们回到缓存逻辑
这个话题。在Housekeeper
中,如果你像上面那样包装了你的方法,那么要添加缓存过程,你只需要写一行代码,就像这样
<?php class ArticleRepository extends \Housekeeper\Repository { use \Housekeeper\Abilities\CacheStatically; // Adding this //... }
现在你的方法返回将自动缓存,就是这样。
酷吗?
注入 & 启动
以下是Housekeeper
中方法执行的一个序列图(核心流程是实际的方法逻辑)
Housekeeper
允许你将逻辑(称为注入
)注入到任何流
中,在每个流
中,属于该流
的注入
将被执行。注入
就像中间件
,但有三种类型:Before
、After
和Reset
(对应三种不同的可注入流
)。这里是一个例子
<?php class MyBeforeInjection implements \Housekeeper\Contracts\Injection\Before { public function priority() { return 30; // Smaller first } // main method public function handle(\Housekeeper\Contracts\Flow\Before $beforeFlow) { // In here you can get the `Action` object $action = $beforeFlow->getAction(); // Or get the `Repository` $repository = $beforeFlow->getRepository(); // And you can set the returns (Only in `Before Flow`) $beforeFlow->setReturnValue(1); } }
Injection
中的handle
方法接受一个Flow
对象,根据注入的Flow
类型,该对象的接口可能不同,例如,Before Flow
提供了setReturnValue
方法,您可以通过向其传递值来调用它,然后Housekeeper
将使用此值作为返回值并跳过实际方法。
您可以通过使用这些方法注入Injection
:injectIntoBefore
、injectIntoAfter
和injectIntoReset
。
<?php class ArticleRepository extends \Housekeeper\Repository { // `Housekeeper` will call the `boot` method automatically with `Dependency Injection` process public function boot() { $this->injectIntoBefore(new MyBeforeInjection()); } // ... }
以下是Before Flow
执行的流程图
当创建Repository
实例时,Housekeeper
也会调用以boot
开头的所有Repository
类方法(在调用boot
方法之前),Housekeeper
中的一些内置Abilities
利用了这一点,如在Adjustable
特质中
<?php trait Adjustable { // ... public function bootAdjustable() { $this->injectIntoBefore(new ApplyCriteriasBefore()); } // ... }
包装层
假设有人编写了以下代码
<?php use Housekeeper\Action; class ArticleRepository extends \Housekeeper\Repository { public function getArticlesByAuthorId($authorId) { return $this->simpleWrap(Action::READ); } protected function _getArticlesByAuthorId($authorId) { return $this ->applyWheres([ ['author_id', '=', $authorId], ]) ->get(); } public function getArticlesBySameAuthor($articleId) { return $this->simpleWrap(Action::READ); } protected function _getArticlesBySameAuthor($articleId) { $article = $this->getModel()->find($articleId, ['id', 'author_id']); return $this->getArticlesByAuthorId($article->author_id); } // ... }
<?php class ArticleController { public function getRecommendForArticle(ArticleRepository $articleRepository, $articleId) { $articles = $articleRepository ->applyWheres([ ['language', '=', 'chinese'], ]) ->getArticlesBySameAuthor($articleId); return view('article.recommend-for-article', compact('articles')); } // ... }
在这个例子中,applyWheres
方法被使用了两次,一次在Controller
中,另一次在Repository
中,第一个会影响_getArticlesByAuthorId
方法吗?不会。它只会影响_getArticlesBySameAuthor
方法,并且更准确地说,它影响的是这一行
$article = $this->getModel()->find($articleId, ['id', 'author_id']);
Housekeeper
中每个包装方法都有自己的Scope
,这意味着它们有自己的Eloquent Model
(或Builder
),因此它们不会相互影响。如果您在仓库外部调用applyWheres
或ApplyOrderBy
,它们只会影响您调用的第一个包装方法。
另一个包装选择
有两个方法可能会很烦人,您可以在simpleWrap
之前编写一个匿名函数
,以传递一个Callable
<?php public function getByName($name) { return $this->simpleWrap(Action::READ, function (name) { return $this->getModel() ->where('name', '=', $name) ->get(); }); }
API
whereAre(array $wheres)
向查询添加一个where子句数组。
参数
$wheres
- 一个where条件的数组。
示例
<?php $userRepository ->whereAre([ ['age', '>', 40], ['area', 'west'] ]) ->all();
<?php $userRepository ->whereAre([ ['area', 'east'], function ($query) { $query->whereHas('posts', function ($hasQuery) { $hasQuery->where('type', 1); }); $query->whereNull('has_membership'); }, ]) ->paginate(12);
applyWheres(array $wheres)
whereAre
方法的别名。
参数
$wheres
- 一个where条件的数组。
orderBy($column, $direction = 'asc')
向查询添加一个"order by"子句。
参数
$column
$direction
示例
<?php $UserRepository ->orderBy('age', 'desc') ->all();
applyOrderBy($column, $direction = 'asc')
orderBy
方法的别名。
参数
$column
$direction
offset($value)
设置查询的"offset"值。
参数
$value
- 要返回的第一行的指定偏移量。
示例
<?php $UserRepository ->limit(10) ->all();
limit($value)
设置查询的"limit"值。
参数
$value
- 要返回的最大行数。
示例
<?php $UserRepository ->limit(10) ->all();
exists($id, $column = null)
使用其主键确定记录是否存在。
参数
$id
- 记录的主键。$column
- 您还可以指定一个非主键的列,并相应地更改$id
的值。
示例
<?php $userRepository->exists(3);
<?php $userRepository->exists('name', 'John');
您也可以使用此方法与自定义查询条件一起使用
<?php $userRepository->whereAre(['gender' => 'female'])->exists(1);
count($columns = '*')
获取查询的 "count" 结果。
参数
$columns
find($id, $columns = array('*'))
通过主键查找模型。
参数
$id
$columns
- 指定要检索的列。
示例
<?php $userRepository->find(1, ['id', 'name', 'gender', 'age']);
findMany($ids, $columns = array('*'))
通过主键查找模型的集合。
参数
$ids
$columns
- 指定要检索的列。
update($id, array $attributes)
更新数据库中的记录。
参数
$id
$attributes
示例
<?php $userRepository->update(24, [ 'name' => 'Kobe Bryant' ]);
create(array $attributes)
使用 $attributes
创建模型。
参数
$attributes
delete($id)
通过主键从数据库中删除记录。
参数
$id
first($columns = ['*'])
执行查询并检索第一个结果。
参数
$columns
all($columns = ['*'])
将查询作为 "select" 语句执行。
参数
$columns
paginate($limit = null, $columns = ['*'], $pageName = 'page', $page = null)
分页给定查询。
参数
$limit
$columns
$pageName
$page
getByField($field, $value = null, $columns = ['*'])
通过简单等式查询检索模型。
参数
$field
$value
$columns
with($relations)
设置应该预加载的关系,如 Eloquent
。
示例
<?php $users = $userRepository->with('posts')->paginate(10);
可调整的
对于更复杂的查询,您可以将其放入一个更语义化的 Criteria
类中,并在任何需要的地方重用它,为此,使用 Adjustable
能力。
示例
<?php namespace App\Repositories\Criterias; class ActiveUserCriteria implements Housekeeper\Abilities\Adjustable\Contracts\Criteria { public function apply(Housekeeper\Contracts\Repository $repository) { $repository->whereAre([ ['paid', '=', 1], ['logged_recently', '=', 1], ]); } }
然后在您的 控制器
<?php $activeUserCriteria = new ActiveUserCriteria(); // UserRepository must used the `Adjustable` trait $activeUsers = $userRepository->applyCriteria($activeUserCriteria)->all(); // Or you can remember this Criteria: $userRepository->rememberCriteria($activeUserCriteria); $activeUsers = $userRepository->all(); $femaleActiveUsers = $userRepository->where('gender', '=', 'female')->all();
API
applyCriteria(\Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria)
只应用此 Criteria
一次。
参数
$criteria
-Criteria
对象。
rememberCriteria(\Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria)
记住此 Criteria
,它将在每个包装方法被调用时应用(只有第一个,内部方法调用将被忽略)。
参数
$criteria
-Criteria
对象。
forgetCriterias()
删除所有已记住的 Criterias
(未应用)。
getCriterias()
获取所有已记住的 Criterias
。
优雅地
此 Abilitiy
提供了许多您非常熟悉的 Eloquent
风格查询 API。
API
where($column, $operator = null, $value = null, $boolean = 'and')
orWhere($column, $operator = null, $value = null)
has($relation, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null)
whereHas($relation, Closure $callback, $operator = '>=', $count = 1)
whereDoesntHave($relation, Closure $callback = null)
orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1)
whereIn($column, $values, $boolean = 'and', $not = false)
whereNotIn($column, $values, $boolean = 'and')
orWhereNotIn($column, $values)
whereNull($column, $boolean = 'and', $not = false)
orWhereNull($column)
whereNotNull($column, $boolean = 'and')
orWhereNotNull($column)
CacheStatically
此Ability
实现了一个非常简单的缓存系统:缓存所有方法返回值,在创建/更新/删除时删除所有缓存,也可以手动清除缓存。
一旦使用此Ability
,所有操作都将自动完成。 all()
、find()
、paginate()
等将通过缓存逻辑,如果找到缓存返回值,则不会执行数据库查询。不同的方法有不同的缓存键,即使应用查询也会更改缓存键。
此Ability
可能在大型项目中不太实用,但它展示了Housekeeper
的灵活性,其他缓存系统也在计划中。
示例
<?php // Cache is disabled by default, you have to enable it first. $userRepository->enableCache()->all(); // This also will be cached! $userRepository->where('age', '<', '30')->orderBy('age', 'desc')->all();
包装的方法有自己的缓存
<?php class UserRepository extends Housekeeper\Repository { use Housekeeper\Abilities\CacheStatically; public function getOnlyActive() // Cached { return $this->simpleWrap(Housekeeper\Action::READ); } protected function _getOnlyActive() { // Every wrapped method has it's own scope, they don't interfere with each other return $this->whereAre([ ['paid', '=', 1], ['logged_recently', '=', 1], ]) ->all(); // Cached too } }
API
enableCache()
启用缓存系统。
disableCache()
禁用缓存系统。
isCacheEnabled()
指示缓存系统是否启用。
clearCache()
删除此存储库的所有缓存。
Guardable
Housekeeper
默认会忽略 大量赋值保护
,如果你需要它,请使用此 能力
。
Guardable
默认也禁用了 大量赋值保护
,你需要手动开启。
示例
<?php // For inputs that we can't trust $userRepository->guardUp()->create($request->all()); // But we can trust our internal process $userRepository->guardDown()->create($attributes);
API
guardUp()
启用 大量赋值保护
。
guardDown()
禁用 大量赋值保护
。
isGuarded()
是否启用了 大量赋值保护
。
软删除
要使用 Eloquent
的 软删除
特性,你应该在你的仓库中使用此 能力
。
API
startWithTrashed()
包含软删除。
startWithTrashedOnly()
仅包含软删除。
forceDelete($id)
通过主键硬删除一条记录。
参数
$id
restore($id)
通过主键恢复一条软删除的记录。
参数
$id
控制台命令
创建一个新的仓库:
php artisan housekeeper:make MyRepository
创建一个新的仓库和一个新的模型:
php artisan housekeeper:make MyRepository --create=Models\\Student
创建一个新的仓库并添加一些 能力
:
php artisan housekeeper:make MyRepository --cache=statically --eloquently --adjustable --sd
问题
如果你对 Housekeeper
有任何疑问,请随时创建一个问题,我会尽快回复你。
任何有用的拉取请求都欢迎。
许可证
根据 Apache许可证 2.0 许可。
鸣谢
感谢 prettus/l5-repository 的启发。
感谢 sunkey 提供了棒棒的LOGO!
感谢 @DarKDinDoN、@Bruce Peng、@FelipeUmpierre、@rsdev000 的贡献!
感谢 Laravel 让我们的生活更轻松!