nickjbedford/laravel-transactions

为Laravel项目中处理失败情况的复杂事务提供简单的类结构。

0.3.5 2024-06-06 23:44 UTC

This package is auto-updated.

Last update: 2024-09-07 00:12:22 UTC


README

Laravel Transactions为Laravel项目中处理失败情况的复杂事务提供了类结构。

通过从YetAnother\Laravel\Transaction抽象类派生并实现perform()以及可选的validate()cleanupAfterFailure()方法,您可以编写复杂的交易性动作,这不仅保持了数据库中的状态完整性,也保持了外部系统的状态完整性。

例如,当处理文件上传时,您的Transaction子类应保持一个成功上传文件的列表,当基类在发生异常时调用cleanupAfterFailure()方法时,这些文件应被删除。

简要概述

此库提供了两个抽象类,您可以从中派生以实现可能导致外部副作用并在发生错误时需要清理的数据库相关事务,例如上传文件到存储服务。

这些类是

// Used by business logic to perform transactional changes.
abstract class YetAnother\Laravel\Transaction

// Used by controllers to wrap [ request -> transaction -> response ] processes.
abstract class YetAnother\Laravel\Http\TransactionResponder

它还提供了两个Artisan命令来创建这些基类的子类。

php artisan make:transaction CustomTransaction
php artisan make:responder CustomTransactionResponder

生成的类将放置在项目的app/Transactionsapp/Http/Responders目录中。

Transaction Class Flow

安装

要将Laravel Transactions安装到您的Laravel项目中,打开终端并运行以下命令:

composer require nickjbedford/laravel-transactions

这将把包添加到您的Composer包文件,并将包下载到项目的vendor文件夹。

Laravel Transactions服务提供者

要将包的Artisan命令make:transactionmake:responder自动注册,请将YetAnother\Laravel\Providers\TransactionsServiceProvider类添加到项目config/app.php文件中的providers数组。

'providers' => [
    // ...
    
    \YetAnother\Laravel\Providers\TransactionsServiceProvider::class,
],

事务执行过程

当调用$transaction->execute()时,将建立Laravel DB事务上下文,该上下文将捕获抛出的任何异常,并将更改回滚到数据库。如果正确实现,外部清理也会得到处理。

子类实现的validate()perform()方法都应抛出异常,以便回滚和清理事务更改。

已添加可选方法,以便在外部对事务流程进行详细处理时使用。

public function execute(): self
{
    try
    {
        $this->lockTableIfNecessary();
        $this->beforeTransaction();
        DB::transaction(fn() => $this->validateAndPerform());
    }
    catch(Throwable $exception)
    {
        $this->revertSideEffects();
        $this->cleanupAfterFailure();
        throw $exception;
    }
    finally
    {
        $this->afterTransaction();
        $this->unlockTableIfNecessary();
        $this->finally();
    }
    $this->fireEvent();
    return $this;
}

主要虚拟方法

protected function validate() { }

这是一个可选的重写,允许事务在执行实际工作之前分离验证检查。

如果操作或参数无效,则应抛出任何类型的Throwable,以便execute()方法捕获并处理回滚和清理事务。

protected abstract function perform()

此方法必须由子类重写以执行必要的更改。数据库更改会自动提交或回滚,但如果抛出异常,则必须通过cleanupAfterFailure()方法清理外部副作用。

次要虚拟方法

protected function cleanupAfterFailure() { }

要处理事务副作用(如文件上传或外部服务的更改)的清理,应重写此方法以执行清理。子类应维护一个可能需要采取的可逆操作列表,例如在失败时删除的文件路径。

protected function beforeTransaction/afterTransaction() { }

无论交易成功还是失败,要在交易前后执行自定义处理,应重写beforeTransaction()afterTransaction()方法。

受保护的函数 finally() { }

为了在所有其他处理之后,无论成功还是失败,在finally块中执行代码,在所有其他方法之后,重写finally()方法。

事务示例

以下是一个示例,不仅需要在数据库中创建新记录,还必须上传文件到亚马逊的S3存储服务。

如果其中一个或两个过程失败,基本类将自动回滚数据库更改,但子类需要删除任何外部副作用,在这种情况下意味着如果数据库更新失败,则从S3删除文件。

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use YetAnother\Laravel\Transaction;
use App\Models\Attachment;

/**
 * Represents a transaction that must upload a file to a
 * storage destination and update the database.
 */
class UploadAttachmentTransaction extends Transaction
{
    private ?string $uploadedFilePath = null;
    private UploadedFile $file;
    
    public ?Attachment $model = null;
    
    public function __construct(UploadedFile $file)
    {
        $this->file = $file;
    }
    
    /**
     * Validates the action before it is performed.
     * @throws Throwable
     */
    protected function validate() : void
    {
        $extension = strtolower($this->file->getClientOriginalExtension());
        
        if (!in_array($extension, [ 'png', 'jpg', 'jpeg', 'gif' ]))
            throw new InvalidArgumentException('Uploaded file is not a valid file type.');
    }
    
    /**
     * Uploads the file to Amazon S3 then creates an
     * Attachment model in the database.
     * @throws Exception
     */
    protected function perform() : void
    {
        $this->uploadFileToS3();
        $this->createAttachment();
    }
    
    protected function uploadFileToS3(): void
    {
        $path = 'some/path/to/' . $this->file->getClientOriginalName();
        $s3 = Storage::disk('s3');
        
        if ($s3->put($this->file, $path))
            $this->uploadedFilePath = $path;
    }
    
    protected function createAttachment(): void
    {
        $this->model = Attachment::create([
            'disk' => 's3',
            'path' => $this->uploadedFilePath
        ]);
    }
    
    /**
     * Deletes the file from S3 if any processes afterwards
     * failed. The database transaction has already been rolled
     * back at this time. 
     */
    public function cleanupAfterFailure() : void
    {
        parent::cleanupAfterFailure();
        
        if ($this->uploadedFilePath)
        {
            $s3 = Storage::disk('s3');
            $s3->delete($this->uploadedFilePath);
        }
    }
}

事务事件触发

事务成功完成后还可以触发事件。要指定要触发的事件类类型,按如下方式重写$event属性。事件将在数据库事务提交且未抛出异常后触发。

定义事务事件

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use YetAnother\Laravel\Transaction;

/**
 * This event is fired after the transaction completes.
 */
class TransactionComplete
{
    use Dispatchable, SerializesModels;
    
    public function __construct(Transaction $transaction)
    {
        //
    }
}

指定要触发的事件类

use YetAnother\Laravel\Transaction;
use App\Events\TransactionComplete;

class EventFiringTransaction extends Transaction
{
    protected ?string $event = TransactionComplete::class; 
    
    protected function perform() : void
    {
        // 
    }
}

默认情况下,如果事件构造函数接受参数,这将尝试将事务实例传递给事件构造函数。如果您希望创建用于分发的自定义事件,请按如下方式重写createEventInstance()方法

use YetAnother\Laravel\Transaction;
use App\Events\TransactionComplete;

class EventFiringTransaction extends Transaction
{
    protected function perform() : void
    {
        // 
    }
    
    /**
     * Create a custom event to dispatch. 
     * @return mixed
     */
    protected function createEvent()
    {
        return new TransactionComplete($this);
    }
}

事务响应者

由于Laravel自动响应处理,控制器方法可以返回一个对象,并且可以使用TransactionResponder基类自动处理和交易请求。

在控制器动作的上下文中,事务响应者几乎可以被认为是事务的“执行视图”。

例如,控制器的一个store方法可以返回一个事务响应者,该响应者旨在从请求创建模型,然后提供正确的响应。

use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class ModelController extends Controller
{
    /**
     * Stores the model and redirects to the show action.
     * @param Request $request
     * @return Responsable
     */
    public function store(Request $request): Responsable
    {
        return new CreateModelTransactionResponder();    
    }
}

Laravel和TransactionResponder类会通过请求响应者执行事务来完成其余工作。

一旦事务成功,就会从子类请求响应。这可以是一个JSON响应,重定向或其他任何有效的Laravel响应。

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use YetAnother\Laravel\Http\TransactionResponder;
use YetAnother\Laravel\Transaction;
use App\Transactions\CreateModelTransaction;

class CreateModelTransactionResponder extends TransactionResponder
{
    /**
     * Creates the appropriate response for a successful transaction. 
     * @param Transaction $transaction
     * @return Response
     */
    protected function getResponseAfterExecution(Transaction $transaction): Response
    {
        /** @var CreateModelTransaction $transaction */
        $redirectTo = route('model.show', [ 'model' => $transaction->model->id ]);
        return redirect($redirectTo);
    }
    
    /**
     * Creates the desired transaction from an incoming request. TransactionResponder
     * will execute this transaction. 
     * @param Request $request
     * @return Transaction
     */
    protected function createTransaction(Request $request): Transaction
    {
        $name = trim($request->name);
        $description = trim($request->description);
        
        return new CreateModelTransaction($name, $description);
    }
    
    /**
     * Overrides the response to return when a transaction fails with an exception.
     * By default, this throws the exception onto Laravel's handling mechanisms, but
     * this could be used to return a structured JSON response or other custom response. 
     * @param Throwable $exception
     * @return Response
     */
    protected function exceptionToResponse(Throwable $exception) : Response
    {
        return response()->json([
            'status' => false,
            'error' => $exception->getMessage(),
            'code' => $exception->getCode()
        ]);
    }
}