smskin / laravel-saga
laravel项目的状态机(Saga)引擎
Requires
- php: ^8.1
- laravel/framework: ^10 || ^11
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.54
- mockery/mockery: ^1.4
- orchestra/testbench: ^8 || ^9
- phpunit/phpunit: ^10.5
- vimeo/psalm: ^5.4
README
在与.Net Core和MassTransit一起工作时,发现Laravel缺少现成的状态机引擎。受MassTransit的启发,我编写了一个与MassTransit sagas类似的库。
安装
composer require smskin/laravel-saga
php artisan vendor:publish --provider=SMSkin\LaravelSaga\Providers\ServiceProvider
配置
在配置文件config/saga.php
中,您将找到引擎设置和状态机的描述。
- logger - 负责记录状态机操作过程的类。可以更改为实现
ISagaLogger
接口的另一个类。 - state-machines - 已注册的状态机的数组。
- 存储库
- default - 存储状态机状态的存储库(数据库)。
- database
- class - 存储库类。可以更改为实现
ISagaRepository
接口的另一个类。 - table - 存储状态机状态的表名。
- class - 存储库类。可以更改为实现
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
配置选项
更改沙盒数据存储仓库
- 创建一个实现
ISagaRepository
接口的类。 - 将其添加到
saga.repositories
配置。 - 在
saga.repositories.default
配置变量中指定它。
更改记录器
- 创建一个实现
ISagaLogger
接口的类。 - 在
saga.logger
配置中指定它。
创建自定义沙盒
- 创建一个继承自
BaseSaga
的类。 - 在
setup()
方法中描述状态机的逻辑。 - 在
saga.state-machines
配置中指定该类。