ernestobaezf/l5toolkit

一个Laravel包,通过使用仓库和其他模式简化新项目的启动

2.1.2 2019-11-21 20:04 UTC

This package is auto-updated.

Last update: 2024-09-12 08:25:52 UTC


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

注意以下两点:

  1. 我们建议不要直接在代码中使用DB::beginTransactionDB::commitDB::rollback。始终使用工作单元

  2. 仅在存在以下情况之一或需要控制数据持久性的变体时,才通过使用工作单元手动提交或回滚:

仓库结构

作为使用仓库的入口点的工作单元有一个方法UnitOfWork::getRepository($entityClass)。这基于以下规则的仓库查找器来发现与实体相关的仓库:

  1. 返回映射到实体的config.LToolkit.repository_map中的仓库。如果声明了映射并且相关的仓库不存在,则抛出异常。

  2. 如果没有映射,如第一步所示,则返回放置在与实体命名空间关联的仓库目录中(遵循命名约定)的关联仓库。如果没有找到关联的仓库,则返回一个基本的GenericRepository。

  3. 如果没有指定映射,则返回的仓库是按照以下步骤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来访问远程数据。因此需要:

  1. 为远程模型声明本地模型

  2. 为该模型创建仓库,扩展RemoteRepository基类

  3. 在步骤2中创建的仓库中定义对应于微服务公开的方法。

通过这样做,我们遵循相同的逻辑流程,可以重用大量代码。通过这样说,我们强调,没有必要,并且我们强烈反对在代码中直接使用Guzzle或其他http客户端;使用仓库。

验证

从扩展BaseAPIController的控制器中的每个操作都按照以下约定搜索匹配的验证器:

  1. Http目录内,与ControllersMiddleware目录一起,创建一个Validators目录。

  2. Http/Validators内,创建一个与控制器名称相同的目录(不带后缀"Controller")。

  3. 在内部创建一个类 <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);
    }
}

然后可以执行该方法

  1. (new SomeClass())->log(all, ['column1', 'column2'])

  2. (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 和以下操作一样简单

  1. Model 目录中创建模型

  2. 创建一个 Controller,扩展类 BaseAPIResourceController 并指定模型如下

    class SampleAPIController extends BaseAPIResourceController { protected function getEntity(): string { return SampleModel::class; } }

  3. 如果您想添加验证。按照 验证 部分中解释的方式创建验证

  4. 声明 API 的路由

Route::prefix("api/v1/")
       ->namespace($this->getControllersNamespace())
       ->group([middleware => 'api'], function () {
               Route::resource('sample', 'SampleAPIController')
           });

这样就完成了。您现在拥有了一个具有完整 CRUD 功能的 API。

上述实现允许

  1. 通过只传递参数 'limit' 来获取元素的分页列表。如果没有发送此参数,则默认结果为元素的全集。
GET http://{{domain}}/api/v1/sample?limit=20&page=1
  1. 通过字段筛选元素
GET http://{{domain}}/api/v1/sample?search=element1&searchFields=name:like
  1. 获取元素列表的关联及其字段。
GET http://{{domain}}/api/v1/users?search=element1&searchFields=name:like&with=roles:id
  1. 获取与请求的实体相关联的实体的信息
GET http://{{domain}}/api/v1/sample/<id>?with=<relation1>:<fields,...>;<relation2>:<fields,...>

Example
GET http://{{domain}}/api/v1/users/20?with=roles:id,name