Laravel应用程序的业务层基础

v4.3 2024-08-22 15:39 UTC

This package is auto-updated.

Last update: 2024-09-22 15:46:31 UTC


README

BAPI代表业务API,类似于强化版的操作

  • 它封装了应用程序的业务逻辑,使其可重用和可测试。
  • 它通过使用数据库事务来确保数据一致性。
  • 它允许在实际运行业务逻辑之前进行授权检查
  • 它允许在实际运行业务逻辑之前进行业务验证
  • 它封装了处理应用程序业务逻辑的最佳实践
  • BAPI是自包含可重用的业务逻辑片段。

为了澄清

  • 业务逻辑是指特定于您的应用程序的逻辑。例如,如果您创建了一个处理汽车库存的应用程序,那么业务逻辑就是与汽车、库存、库存报告、汽车制造商等相关的特定逻辑。
  • 业务验证与用户输入验证不同。用户输入验证是确保用户输入有效(例如,电子邮件是有效的电子邮件)。业务验证是确保数据在您试图解决的问题的上下文中有意义(例如,汽车零部件与汽车型号兼容)。

例如,用于向库存添加汽车零部件的BAPI可以这样调用

AddCarPartToInventoryBapi::run(
    partCategory: $category,
    partType: $type,
    partManufacturer: $manufacturer,
    carMake: $make,
    carModel: $model,
    carYear: $year,
    storage: $storageToBeAddedTo,
);

BAPI的目的不是取代控制器,而是由控制器和其他处理应用程序业务逻辑的方法(例如,Livewire表单、作业、命令等)使用。

为什么要使用BAPI?

如果您曾经开发过应用程序,您知道逻辑会随着每个功能添加和每个用户需求的增加而变得更加复杂。

为了能够在更复杂的流程(一系列步骤)中使用之前编写的业务逻辑时依赖它们,您应该将它们拆分成原子、可重用的业务步骤,并确保它们经过彻底测试。每个这样的步骤都可以在专门的BAPI中实现。

使用方法

安装

通过composer导入bapi包

  composer require antonioprimera/bapi

创建新的BAPI

安装包后,将提供创建新BAPI的Artisan命令。

例如,您可以在控制台中运行以下Artisan命令来在app/Bapis/Posts/CreatePostBapi.php中创建新的BAPI

php artisan make:bapi Posts/CreatePostBapi

高级BAPI生成

始终创建复杂的BAPI

这将创建一个新的基本BAPI类,位于您的Laravel应用程序的app/Bapis文件夹中。如果您想创建一个稍微复杂一些的BAPI类,具有所有钩子和方法,您应该在上面的命令中使用--full标志。

如果您想始终为您的项目创建复杂的BAPI类,而不总是每次都使用--full标志,您可以将以下设置添加到您的.env文件中

BAPI_GENERATOR_COMPLEX_BAPIS=true

为生成的BAPI继承的基类

默认情况下,继承生成的BAPI的基BAPI类是AntonioPrimera\Bapi\Bapi。如果您在项目中还有另一个基类,您可以像这样将其添加到您的.env文件中

BAPI_GENERATOR_BASE_CLASS="App\\Bapis\\Bapi"

TDD:为您的BAPI创建测试文件

您有多种选择让make:bapi命令为您的新BAPI创建单元测试

  • 如果您只想创建一个简单的测试,请将--t选项添加到您的命令中。以下示例将创建测试文件test/Unit/Bapis/Posts/CreatePostBapiTest.php
php artisan make:bapi Posts/CreatePostBapi --t
  • 如果您想要控制单元测试的路径和名称,您可以在命令中添加 --test TestPath/AndName 选项。以下示例将创建测试文件 test/Unit/Posts/CreatePostBasicTest.php
php artisan make:bapi Posts/CreatePostBapi --test Posts/CreatePostBapiBasicTest
  • 如果您总是想为所有的bapi创建一个简单、默认的测试,您可以在您的 .env 文件中添加以下条目,这将像为所有的 make:bapi 命令添加 --t 一样。
BAPI_GENERATOR_TDD=true

实现您的Bapi及其运行生命周期

每次您实例化bapi时,如果实现了,将调用 setup() 方法。默认情况下,setup方法未实现。如果您以静态方式调用run方法(请参阅关于运行您的bapi的章节),则在后台创建一个实例,因此如果实现了,setup方法将始终被调用。

当您运行您的bapi时,以下方法将按照以下顺序被调用

  1. authorize()
  2. validate()
  3. handle(...)
  4. processResult(mixed $result): mixed

调用BAPI run 方法时提供的参数必须是命名参数,并且将在整个生命周期中可用,直接在BAPI实例上,使用handle方法中的参数名称。

例如,如果您想像这样调用UpdatePostBapi...

    UpdatePostBapi::run(post: $post, title: $title, contents: $contents)

...您的 handle() 方法必须看起来像这样...

    protected function handle(Post $post, $title, $contents)

...然后,当通过 run() 方法运行您的bapi时,参数将作为实例属性在所有Bapi方法中使用,因此您可以像这样使用它们...

    return
        $this->post->title === $this->title
        && $this->post->contents === $this->contents;

最后,handle() 方法的输出将作为参数传递给 processResult() 方法,这允许您对结果进行任何转换和后处理。processResult() 方法的返回值将由BAPI run(...) 方法返回。如果需要,您可以重写 processResult() 方法并更改BAPI的结果,在它被返回之前。

运行您的Bapi

您可以使用 run() 方法调用您的bapi,无论是作为静态方法还是实例化您的bapi后的实例方法。Bapi中不存在run方法,您不应该创建run方法。此调用被相应的魔术方法拦截,并启动bapi运行生命周期。不要在您的bapi中创建run()方法。主要业务逻辑应放在 handle() 方法中。

您也可以选择调用Bapi。

例如,如果您在上面的示例中有UpdatePostBapi,您可以用以下任何一种方式调用它。

    //static method call
    UpdatePostBapi::run($post, 'New title', 'Some contents');
    //instance method call
    $updatePostBapi = new UpdatePostBapi();
    $updatePostBapi->run($post, 'New title', 'Some contents');
    //invoke
    $updatePostBapi = new UpdatePostBapi();
    $updatePostBapi($post, 'New title', 'Some contents');

跳过授权检查

有时,在更复杂的场景中,当bapi在业务逻辑中将其他BAPI作为部分调用时,您可能希望在复杂的BAPI中执行所有必要的授权检查,并在内部运行其他BAPI,而不进行授权检查(可能只是冗余的检查)。

如果您想跳过授权检查,可以以静态方式或作为实例方法调用 withoutAuthorizationCheck() 方法。

例如,如果您想在之前的示例中调用bapi而不运行授权检查,可以这样做

    //static method call
    UpdatePostBapi::withoutAuthorizationCheck()
        ->run($post, 'New title', 'Some contents');
    //instance method call
    $updatePostBapi = new UpdatePostBapi();
    $updatePostBapi->withoutAuthorizationCheck();
    $updatePostBapi->run($post, 'New title', 'Some contents');

虽然这样做是可能的,但风险较大,因为Bapis应该是原子性的代码片段,并且应该完全独立。因此,如果一个bapi调用了其他Bapis,而这些Bapis又调用其他Bapis,如此循环下去,就很难确保每个bapi都覆盖了所有必要的授权检查。这也会增加重复授权逻辑的风险。关于Bapis的结构和授权没有通用规则,所以只需运用常识,并确保彻底测试你的Bapis,否则你可能会错过Bapis的主要优势,也许使用单文件操作或简单的php类会更好,因为这些更容易实现和理解,并且包含的魔法更少。

尽管你可能永远不会使用它,但有一个withAuthorizationCheck()方法可用,可以调用它来重新启用之前已禁用的Bapi实例的授权检查。

跳过数据库事务

如果你想在没有数据库事务的情况下运行BAPI,你可以静态调用或作为实例方法调用withoutDbTransaction()方法。

例如,如果你想在不运行数据库事务的情况下调用前面示例中的bapi,你可以这样做:

    //static method call
    UpdatePostBapi::withoutDbTransaction()
        ->run($post, 'New title', 'Some contents');
    //instance method call
    $updatePostBapi = new UpdatePostBapi();
    $updatePostBapi->withoutDbTransaction();
    $updatePostBapi->run($post, 'New title', 'Some contents');

虽然在某些情况下这是可能的,也是必要的,但它存在风险,因此你应该谨慎使用。

如果你想完全禁用Bapi的数据库事务,可以将Bapi类中的$useDbTransaction属性设置为false,覆盖默认值。

如果你想完全禁用所有Bapi的数据库事务,可以为你Bapi创建一个新的基类,并在该基类中将$useDbTransaction属性设置为false。然后,你可以设置BAPI_GENERATOR_BASE_CLASS环境变量,使其指向你的新基类,这样所有的Bapi都会从它继承。

验证业务数据

虽然控制器负责验证用户输入数据,但这些验证通常不足以处理复杂业务流程。业务验证通常更复杂,应该与业务逻辑一起在Bapi中实现,在validate方法内。

如果验证通过,validate()方法必须返回布尔值true。任何其他返回值都将被包装在BapiValidationException中,并抛出。

你还可以直接从validate()方法中抛出BapiValidationException。

BapiValidationIssue和BapiValidationException

每当发生Bapi验证问题时,你应该生成一个BapiValidationIssue实例,你可以将其传递给抛出的BapiValidationException。

每个BapiValidationIssue都必须包含生成问题的属性的名称、其值以及发生的问题,作为问题代码(例如:"AGE-LT-18")作为自由文本消息(例如:"用户未达到法定年龄!")或作为翻译键(例如:"exceptions.age.notLegal")。

    $bapiValidationIssue = new \AntonioPrimera\Bapi\Components\BapiValidationIssue(
        attributeName: 'companyName',      //the name of the attribute at fault
        attributeValue: 'Amazon UK',       //the value of the attribute
        errorMessage: 'not-unique',        //the issue that occurred
        errorCode: 'C:N:NU'                //optionally, an issue code
    );

在生成一个或多个bapi验证问题后,你可以抛出一个带有这些问题的新的BapiValidationException,或者你可以从validate()方法返回一个BapiValidationIssue实例数组。

    protected function validate()
    {
        $issues = [];
        
        //business validation - whether the company name is unique in the EU
        if ($this->comapnyNameIsNotUnique($this->company->name))
            $issues[] = new \AntonioPrimera\Bapi\Components\BapiValidationIssue(
                'companyName',
                $this->company->name,
                'not-unique',
                'C:N:NU'
            );
            
        //business validation - whether the country is registered in the EU
        if ($this->companyCountryNotValid($this->company->country))
            $issues[] = new \AntonioPrimera\Bapi\Components\BapiValidationIssue(
                'companyCountry',
                $this->company->country,
                'non-EU',
                'C:C:NEU'
            );
        
        //if any issues were found, throw a new BapiValidationException with these issues
        if ($issues)
            throw new \AntonioPrimera\Bapi\Exceptions\BapiValidationException($issues);
    }

通过使用这些Bapi验证问题和BapiValidationException,你可以在应用程序的\App\Exceptions\Handler::register()方法中渲染适当的响应。

另一种为你的业务验证异常渲染通用响应的方法是创建BapiValidationException的子类并实现render()方法。为此,你可以查看关于Error HandlingLaravel文档

验证属性

如果您想验证属性并抛出ValidationException,就像表单验证那样,您可以将ValidatesAttributes特质添加到您的Bapi中。这个特质会覆盖Bapis中的默认异常处理机制,并将包含BapiValidationIssues的BapiValidationExceptions转换为ValidationExceptions,这些异常将由默认的Laravel异常处理器处理。

具体来说,这意味着如果您将ValidatesAttributes特质添加到您的Bapi中,并从validate()方法返回BapiValidationIssue或BapiValidationIssues数组,将会抛出ValidationException,这将把验证问题添加到$errors变量中,该变量在视图中可用。

例如,如果您想要验证公司名称,可以这样做...

    use \AntonioPrimera\Bapi\Traits\ValidatesAttributes;
    
    protected function validate()
    {
        //business validation - whether the company name is unique
        if ($this->comapanyNameIsNotUnique($this->company))
            return new \AntonioPrimera\Bapi\Components\BapiValidationIssue(
                'companyName',
                $this->company->name,
                'Company name is not unique',
            );
            
        return true;
    }

...然后在您的表单中,您将能够显示公司名称的错误消息,如下所示

    @error('companyName')
        <div class="alert alert-danger">{{ $message }}</div>
    @enderror

身份验证 & 参与者

Bapi实例提供了一个公开的actor()方法,这仅仅是Auth::user()方法的包装。

protected function authorize()
{
    return $this->actor() 
        && $this->can('some-action', $someModel);
}