ernestobaezf/ltoolkit

通过使用存储库和其他模式轻松启动Laravel新项目的包

2.1.2 2019-11-21 20:04 UTC

This package is auto-updated.

Last update: 2024-09-12 09:23:50 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结尾。例如: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访问远程数据。因此需要:

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

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

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

通过这样做,我们遵循相同的逻辑管道,可以重用大量代码。通过这样说,我们强调没有必要,并且我们强烈反对在代码中直接使用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的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);
    }
}

然后可以执行方法

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

  2. (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就像

  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