用户/aaronjan/housekeeper

此包已被弃用且不再维护。没有建议替代包。

强大的、简单的Laravel(>=5.1)存储库模式实现,附带测试。

v2.3.4 2018-02-05 08:26 UTC

README

Housekeeper

Latest Stable Version License

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(),并且您可以很容易地对它们进行测试。

更重要的是,HousekeeperRepository Pattern的更好版本(在某些方面),您可以在下面的内容中了解更多。

Housekeeper喜欢Eloquent。大多数查询API与Eloquent的相同,因此您可以在不需要学习任何东西的情况下使用它们,并且返回值也像Eloquent一样。

安装

要求

PHP >= 5.5Laravel >= 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希望通过使用来以更少的代码解决这个问题。

是每个方法执行过程中的三个阶段,它们是:BeforeAfterReset。在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提示,它通过调用带有CallablesimpleWrap来包装核心方法,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中方法执行的一个序列图(核心流程是实际的方法逻辑)

method execution in Housekeeper

Housekeeper允许你将逻辑(称为注入)注入到任何中,在每个中,属于该注入将被执行。注入就像中间件,但有三种类型:BeforeAfterReset(对应三种不同的可注入)。这里是一个例子

<?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将使用此值作为返回值并跳过实际方法。

您可以通过使用这些方法注入InjectioninjectIntoBeforeinjectIntoAfterinjectIntoReset

<?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执行的流程图

method execution in Housekeeper

当创建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),因此它们不会相互影响。如果您在仓库外部调用applyWheresApplyOrderBy,它们只会影响您调用的第一个包装方法。

另一个包装选择

有两个方法可能会很烦人,您可以在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 让我们的生活更轻松!