smskin/laravel-saga

laravel项目的状态机(Saga)引擎

1.0.0 2024-05-08 06:55 UTC

This package is auto-updated.

Last update: 2024-09-16 17:26:29 UTC


README

Composer Static Analysis Tests

在与.Net Core和MassTransit一起工作时,发现Laravel缺少现成的状态机引擎。受MassTransit的启发,我编写了一个与MassTransit sagas类似的库。

安装

  1. composer require smskin/laravel-saga
  2. php artisan vendor:publish --provider=SMSkin\LaravelSaga\Providers\ServiceProvider

配置

在配置文件config/saga.php中,您将找到引擎设置和状态机的描述。

  • logger - 负责记录状态机操作过程的类。可以更改为实现ISagaLogger接口的另一个类。
  • state-machines - 已注册的状态机的数组。
  • 存储库
    • default - 存储状态机状态的存储库(数据库)。
    • database
      • class - 存储库类。可以更改为实现ISagaRepository接口的另一个类。
      • table - 存储状态机状态的表名。

Saga 结构

让我们通过这个库中的示例(SMSkin\LaravelSaga\Example\SagaExample)来检查一个saga。

属性 $context

此属性描述了状态机存储对象的类型(转换)。可以是继承自SagaContext的任何类。此对象允许在操作期间与其它服务交互时存储中间值。

方法 setup()

此方法描述了状态机的操作算法。

三个关键的saga块

  • correlation
  • 初始化事件/命令
  • 状态机转换逻辑

Correlation

此块描述了在存储库中获取状态机上下文标识符的算法。它使用两种方法进行描述

  • correlatedById - 通过ID获取对象。
  • correlatedBy - 通过任何存储字段获取。
$this->builder()
     ->correlatedById(EUserCreated::class, static function (EUserCreated $event) {
            return $event->corrId;
     })

此块可以解释为:在收到EUserCreated事件时,从corrId属性中获取状态机的ID。

$this->builder()
     ->correlatedBy(EUserBlocked::class, 'userId', static function (EUserBlocked $event) {
            return $event->userId;
     });

此块可以解释为:在收到EUserBlocked事件时,通过userId字段找到状态机,从userId事件中获取值。因此,引擎可以通过UUID或任何上下文字段来搜索状态机的上下文。

Initialization Event/Command

此块描述了将初始化状态机的事件/命令。

onInitEvent方法接受两个参数

  • 要注册到Laravel的事件类。
  • 一个闭包,用于将事件转换为状态机的上下文。使用此方法,您可以在上下文对象中保存一些初始化数据。
 $this->builder()
      ->onInitEvent(CreateUserCommand::class, static function (CreateUserCommand $command) {
            return (new SagaExampleContext($command->correlationId))
                ->setEmail($command->email);
      });

此块可以解释为

  • 在收到CreateUserCommand命令时,初始化状态机。
  • correlationId命令中获取状态机的ID。
  • 将命令中的email保存到状态机的上下文中。

状态机转换逻辑

此块描述了状态机的算法。关键词

  • duringState - 在状态机状态中。
  • on - 接收到事件时。
  • then - 执行(闭包)。
  • activity - 执行子程序(实现IActivity接口的类)。
  • transitionTo - 切换状态机的状态。
  • publish - 发布事件。
  • initial - 初始化的糖(第一阶段)。
  • finalize - 最终化的糖。
$this->builder()
     ->initial()
     ->transitionTo(SagaExampleStates::USER_CREATING)
     ->activity(UserCreatingActivity::class)
     ->then(function () {
            (new UserCommandService())->create(
                $this->context->getId(),
                $this->context->getEmail()
            );
    });

此块可以解释为

  • 初始化时。
  • 将状态切换到 USER_CREATING
  • 执行 UserCreatingActivity 子程序。
  • 执行闭包 - 调用 UserCommandService->create,传递 上下文ID电子邮件(我们在初始化期间存储在上下文中)。
 $this->builder()
      ->duringState(SagaExampleStates::USER_CREATING)
      ->on(EUserCreated::class)
      ->then(function () {
            $event = $this->getHandledEvent();
            $this->context->setUserId($event->userId);
      })
      ->transitionTo(SagaExampleStates::USER_BLOCKING)
      ->then(function () {
            (new UserCommandService())->block(
                $this->context->getUserId()
            );
      });

此块可以解释为

  • 处于 USER_CREATING 状态时。
  • 接收到 EUserCreated 事件。
  • 执行闭包,将事件的 userId(来自事件)写入状态机的上下文中。
  • 将状态切换到 USER_BLOCKING
  • 执行闭包 - 调用 UserCommandService->block,传递上下文中的 userId
$this->builder()
     ->duringState(SagaExampleStates::USER_BLOCKING)
     ->on(EUserBlocked::class)
     ->finalize()
     ->publish(function () {
            return new ESagaExampleFinalized($this->context->getId());
     });

此块可以解释为

  • 处于 USER_BLOCKING 状态时。
  • 接收到 EUserBlocked 事件。
  • 最终化状态机。
  • 发布事件 ESagaExampleFinalized,传递 沙盒ID

基本操作原理

引擎基于 Laravel 事件 运行。在 setup() 方法中描述的事件已在 EventServiceProvider 中注册。沙盒作为监听器。

当事件进入总线时,Laravel 代理执行为该事件注册的沙盒的 handle 方法。

执行优化

由于沙盒注册的事件在沙盒内部描述,Laravel 需要时间从所有沙盒中计算这些事件。为了优化这一点,编写了一个 artisan 命令,该命令保存预计算的缓存事件=沙盒映射,以便注册。

缓存

php artisan saga:cache

缓存清除

php artisan saga:cache:clear

配置选项

更改沙盒数据存储仓库

  1. 创建一个实现 ISagaRepository 接口的类。
  2. 将其添加到 saga.repositories 配置。
  3. saga.repositories.default 配置变量中指定它。

更改记录器

  1. 创建一个实现 ISagaLogger 接口的类。
  2. saga.logger 配置中指定它。

创建自定义沙盒

  1. 创建一个继承自 BaseSaga 的类。
  2. setup() 方法中描述状态机的逻辑。
  3. saga.state-machines 配置中指定该类。