ernestobaezf / ltoolkit
通过使用存储库和其他模式轻松启动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结尾。例如:SampleRepository对应于Package\<package_name>\Models\Sample的模型名称。
-
BaseRepository:如果您需要更专业的仓库,则应始终扩展BaseRepository类并添加特定方法。
-
GenericRepository:这是没有为特定模型实现特定仓库实现时的默认仓库。
-
RemoteRepository:这是用于处理远程服务数据的仓库。而不是直接调用任何http客户端(如guzzle)并每次处理不同的响应,这种结构试图统一访问数据,无论其来源如何。要使用RemoteRepository,需要安装一个实现HttpClient的包,并在您的服务提供者中声明对其的绑定。我们建议使用php-http/guzzle6-adapter。
$this->app->singleton("Http\Client\HttpClient", Http\Adapter\Guzzle6\Client::class);
API控制器
有一个BaseAPIResourceController,它实现了APIResourceControllerInterface,定义了最小和一组常用方法(索引、显示、存储、更新、删除)来处理CRUD。此BaseAPIResourceController注入了一个工作单元,允许根据所需的实体类获取仓库,同时将ValidatorResolver和CriteriaResolverInterface作为构造函数的参数。CriteriaResolver的默认参数是RequestCriteria,用于通过在l5-repository包中定义的准则过滤结果。此关联可以在绑定中根据不同需求进行适配(参见服务提供者部分)或扩展控制器并传递一个新的CriteriaResolver,其中包含针对特定情况的定制准则数组。
要访问远程数据,想法是遵循与BaseAPIResourceController相同的结构和模式,通过使用RemoteRepository访问远程数据。因此需要:
-
为远程模型声明本地模型
-
为该模型创建仓库,扩展RemoteRepository基类
-
在创建的仓库中定义对应于微服务公开的方法。
通过这样做,我们遵循相同的逻辑管道,可以重用大量代码。通过这样说,我们强调没有必要,并且我们强烈反对在代码中直接使用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的Controllers中每个操作的activity都会被记录。这是通过使用中间件来实现的。同时,建议使用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,
],
有2种方式在函数中记录activity
主动式
Class SomeLoggableClass
{
use TLogAction;
...
public function all($columns = ['*']): Collection
{
return $this->evaluate(
function () use ($columns) {
return $this->getInternalRepository()->all($columns);
}, __FUNCTION__
);
}
}
然后,一旦调用方法 (new SomeClass())->all(),它将自动记录方法activity的开始执行和结束。
要启用这些日志,需要设置环境变量 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'])
它将在开始执行和结束时自动记录方法activity。
可以通过环境变量 LOGGABLE_LOG_LEVEL=<log_level> 配置日志级别,默认为 debug,这意味着要在 config/logging.php 中将日志级别配置为debug,才能看到这些日志。
本地化
可以通过使用路由中间件 Localization 并在headers中发送当前语言 '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