ernestobaezf / l5toolkit
一个Laravel包,通过使用仓库和其他模式简化新项目的启动
Requires
- php: ^7.1
- prettus/l5-repository: ^2.6
- psr/http-client: ^1.0
Requires (Dev)
- dg/bypass-finals: ^1.1
- nunomaduro/larastan: ^0.3.16
- phpunit/phpunit: ^8.3
README
这是一个Laravel包,通过实现许多常见的操作来节省创建(主要是面向API的应用)的时间,并遵循不同的设计模式。在这里,您可以找到以下基本内容:
-
服务提供者
-
仓库
-
API控制器
-
验证
-
日志等
使用此包可以在伪包中构建项目结构。这意味着您可以将项目的逻辑分割到名为packages的目录中,并将项目的业务分割成逻辑单元。在这种情况下,预期的目录结构如下:
project
|+ app
|- ...
|- packages
|- Package1
|+ Models
|+ Http
|+ Repositories
|+ Resources
|- Package1ServiceProvider.php
|- Package2
|+ Models
|+ Http
|+ Repositories
|+ Resources
|- Package2ServiceProvider.php
然后,在composer.json文件中按照以下方式配置此目录的命名空间:
"autoload": {
"psr-4": {
...
"Packages\\": "packages"
},
当然,并不强制要求按照解释的结构来组织项目。您可以根据自己的需要使用它。
安装
通过composer安装
composer require ernestobaezf/ltoolkit
然后在您的config/app.php文件中,将以下提供者添加到列表中
'providers' => [
...
LToolkit\ServiceProvider::class,
]
服务提供者
服务提供者是连接包与Laravel主项目的入口点。由于使用此包可以将项目分割成包,因此有许多常见操作可以重复使用,因此我们创建了BaseServiceProvider。这样,每次需要创建新包时,扩展这个类在新的包中就会变得非常简单。但是,有些事情您可能希望自定义,而不仅仅是默认实现,这就是接下来要解释的内容。
仓库模式
仓库模式是用来访问模型层(无论是本地还是远程)的。
此包提供了一种查找器,允许您根据本节中定义的约定获取给定模型的对应仓库。在此模式的实现中,涉及不同的部分
工作单元
因为,从理论上讲,仓库只是一个实体集合,所以需要有一个单元来实际修改数据库。为什么是这样?想象以下情况:
- 情况1:您需要更改多个实体
$repository->update($transaction1->toArray(), $transaction1->getId())
$repository->update($transaction2->toArray(), $transaction2->getId())
但是,如果您想避免为了性能原因而每次都向数据库发送请求,而只发送一次,那会怎样呢?
- 情况2:您需要一个原子操作
$repository->create($user->toArray())
$repository->create($userTransaction->toArray())
在这种情况下,正确的做法是有一个原子操作,这意味着只有在用户创建(并且没有在过程中失败)时才创建与用户相关的事务。
在这两种情况下,我们需要所谓的工作单元,它实际上控制数据的持久化操作。通常这部分逻辑是透明的,因为它不需要额外的配置即可使用,因为它默认配置为自动提交更改并持久化数据。
但是,为了解决前面的情况,我们需要绑定不同的工作单元实例,例如
$this->app->when(SampleAPIController::class)
->needs(UnitOfWorkInterface::class)
->give(function() {
return $this->app->make(UnitOfWork::class, ["autoCommit" => false]);
});
所以前面的情况将类似于
- 情况1:您需要更改多个实体
try {
$repository->update($transaction1->toArray(), $transaction1->getId())
$repository->update($transaction2->toArray(), $transaction2->getId())
$unitOfWork->commit();
} catch(Exception) {
$unitOfWork->rollback();
}
- 情况2:您需要一个原子操作
try {
$repository->create($user->toArray())
$repository->create($userTransaction->toArray())
$unitOfWork->commit();
} catch(Exception) {
$unitOfWork->rollback();
}
注意以下两点:
-
我们建议不要直接在代码中使用DB::beginTransaction、DB::commit或DB::rollback。始终使用工作单元。
-
仅在存在以下情况之一或需要控制数据持久性的变体时,才通过使用工作单元手动提交或回滚:
仓库结构
作为使用仓库的入口点的工作单元有一个方法UnitOfWork::getRepository($entityClass)。这基于以下规则的仓库查找器来发现与实体相关的仓库:
-
返回映射到实体的
config.LToolkit.repository_map中的仓库。如果声明了映射并且相关的仓库不存在,则抛出异常。 -
如果没有映射,如第一步所示,则返回放置在与实体命名空间关联的仓库目录中(遵循命名约定)的关联仓库。如果没有找到关联的仓库,则返回一个基本的GenericRepository。
-
如果没有指定映射,则返回的仓库是按照以下步骤2中默认映射(
Packages\<package_name>\Models => Packages\<package_name>\Repositories)的结果。
命名约定:仓库总是放置在Repositories目录中,名称以相应的模型名称开头,并以Repository结尾。例如:对于Package\<package_name>\Models\Sample的名称,仓库的名称为SampleRepository。
-
BaseRepository:如果您需要更专业的仓库,则应始终扩展BaseRepository类并添加特定方法。
-
GenericRepository:这是没有为给定模型实现特定仓库时的默认仓库。
-
RemoteRepository:这是用于处理远程服务数据的仓库。而不是直接调用任何http客户端(如guzzle)并每次以不同的方式处理响应,这种结构试图统一对数据的访问,无论数据来自何方。要使用RemoteRepository,需要安装一个实现HttpClient的包并在服务提供者中声明对其的绑定。我们建议使用php-http/guzzle6-adapter。
$this->app->singleton("Http\Client\HttpClient", Http\Adapter\Guzzle6\Client::class);
API控制器
存在一个实现了APIResourceControllerInterface的BaseAPIResourceController,它定义了最小和一组常见的方法(index、show、store、update、destroy)以处理CRUD。此BaseAPIResourceController注入了一个工作单元,允许根据所需的实体类获取仓库,同时将ValidatorResolver和CriteriaResolverInterface作为构造函数的参数。CriteriaResolver有一个默认参数,即RequestCriteria,用于使用在l5-repository包中定义的准则过滤结果。此关联可以在绑定中(见服务提供者部分)或扩展控制器中调整,以传递一个带有针对特定情况定制的准则数组的新的CriteriaResolver。
要访问远程数据,想法是遵循与BaseAPIResourceController相同的结构和模式,使用RemoteRepository来访问远程数据。因此需要:
-
为远程模型声明本地模型
-
为该模型创建仓库,扩展RemoteRepository基类
-
在步骤2中创建的仓库中定义对应于微服务公开的方法。
通过这样做,我们遵循相同的逻辑流程,可以重用大量代码。通过这样说,我们强调,没有必要,并且我们强烈反对在代码中直接使用Guzzle或其他http客户端;使用仓库。
验证
从扩展BaseAPIController的控制器中的每个操作都按照以下约定搜索匹配的验证器:
-
在
Http目录内,与Controllers和Middleware目录一起,创建一个Validators目录。 -
在
Http/Validators内,创建一个与控制器名称相同的目录(不带后缀"Controller")。 -
在内部创建一个类
<action_name>Validator(例如StoreValidator),并定义给定操作的规则。
如果您想自己声明验证器而不是遵循约定,可以按以下方式操作
class SampleController extends BaseAPIController
{
public function __construct(UnitOfWorkInterface $unitOfWork)
{
parent::__construct(
$unitOfWork, app(
ValidatorResolverInterface::class, [
"className" => static::class,
"validations" => [
"action" => new ActionValidator()
]
]
)
);
}
...
}
日志
所有扩展 BaseAPIController 的 Controller 的每个操作的活动都会被记录。这是通过使用中间件实现的。还建议使用 CustomLogFormatter 来获取易于解析的日志记录(请参阅下面的示例配置)。
# config/logging.php
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'formatter' => \LToolkit\Formatters\CustomLogFormatter::class,
'formatter_with' => [
"format" => "[%datetime%] %channel%.%level_name% %context% %extra% %message%\n",
"dateFormat" => null,
"allowInlineLineBreaks" => true,
"ignoreEmptyContextAndExtra" => false
],
'level' => 'debug',
'days' => 14,
],
在函数中记录活动有两种方式
主动式
Class SomeLoggableClass
{
use TLogAction;
...
public function all($columns = ['*']): Collection
{
return $this->evaluate(
function () use ($columns) {
return $this->getInternalRepository()->all($columns);
}, __FUNCTION__
);
}
}
然后当方法被调用 (new SomeClass())->all() 时,它将在执行开始时和结束时自动记录方法活动。
要启用这些日志,需要设置环境变量 LOG_ACTIONS=true。
被动式
/**
* @method logAll($columns = ['*']): Collection
*/
Class SomeLoggableClass implements LoggableInterface
{
use TLoggable;
...
public function all($columns = ['*']): Collection
{
return $this->getInternalRepository()->all($columns);
}
}
然后可以执行该方法
-
(new SomeClass())->log(all, ['column1', 'column2'])或 -
(new SomeClass())->logAll(['column1', 'column2'])
它将在执行开始时和结束时自动记录方法活动。
可以使用环境变量 LOGGABLE_LOG_LEVEL=<log_level> 配置日志级别,默认为 debug,这意味着要查看这些日志,必须在 config/logging.php 中将级别配置为 debug。
本地化
可以通过使用路由中间件 Localization 并在头部发送当前语言 'X-localization' 到后端来获取英文和西班牙语的 API 消息。如果没有指定语言,则使用用户偏好设置的语言(如果指定)或默认使用英文。
要设置中间件,在文件 app\Http\Kernel.php 中设置
protected $routeMiddleware = [
...
'l18n' => \LToolkit\Http\Middleware\Localization::class,
];
用例
创建简单的 API 和以下操作一样简单
-
在 Model 目录中创建模型
-
创建一个 Controller,扩展类 BaseAPIResourceController 并指定模型如下
class SampleAPIController extends BaseAPIResourceController { protected function getEntity(): string { return SampleModel::class; } }
-
如果您想添加验证。按照 验证 部分中解释的方式创建验证
-
声明 API 的路由
Route::prefix("api/v1/")
->namespace($this->getControllersNamespace())
->group([middleware => 'api'], function () {
Route::resource('sample', 'SampleAPIController')
});
这样就完成了。您现在拥有了一个具有完整 CRUD 功能的 API。
上述实现允许
- 通过只传递参数 'limit' 来获取元素的分页列表。如果没有发送此参数,则默认结果为元素的全集。
GET http://{{domain}}/api/v1/sample?limit=20&page=1
- 通过字段筛选元素
GET http://{{domain}}/api/v1/sample?search=element1&searchFields=name:like
- 获取元素列表的关联及其字段。
GET http://{{domain}}/api/v1/users?search=element1&searchFields=name:like&with=roles:id
- 获取与请求的实体相关联的实体的信息
GET http://{{domain}}/api/v1/sample/<id>?with=<relation1>:<fields,...>;<relation2>:<fields,...>
Example
GET http://{{domain}}/api/v1/users/20?with=roles:id,name