如何给状态机加钩子?stateful_enum的before/after事件回调与参数传递完全教程
【免费下载链接】stateful_enumA very simple state machine plugin built on top of ActiveRecord::Enum项目地址: https://gitcode.com/gh_mirrors/st/stateful_enum
状态机在"状态切换"的一瞬间,往往还伴随着发送通知、记录时间、填写操作人等一连串业务动作。stateful_enum 是基于 ActiveRecord::Enum 构建的轻量级状态机插件,它原生支持before与after事件回调,让你在状态机事件的关键时刻挂上钩子,并把业务参数原样传入回调。本文用最短的路径讲清楚:钩子怎么写、参数怎么传、执行顺序是什么、有哪些坑要避。
状态机为什么需要事件钩子?
状态本身的变更只是一次整数更新,但业务真正关心的是变更"前后"要发生什么:
- 变更前:记录解决时间、校验前置条件、填写操作人
- 变更后:发送通知、写审计日志、触发下游流程
如果把这些逻辑散落在各个 Controller 里,很快就会失控。用before/after事件回调,可以把副作用集中收敛到状态机定义处——改状态的地方,就是改业务逻辑的地方。
三步给状态机加上 before/after 钩子
第 1 步:在 Gemfile 中安装插件。
gem 'stateful_enum'第 2 步:用enum定义状态,并在块里声明事件。
第 3 步:在event块内写before/after回调,写法与 README.md 的示例一致:
class Bug < ApplicationRecord enum :status, {unassigned: 0, assigned: 1, resolved: 2, closed: 3} do event :resolve do before do self.resolved_at = Time.zone.now end transition [:unassigned, :assigned] => :resolved end event :close do after do Notifier.notify "Bug##{id} has been closed." end transition all - [:closed] => :closed end end end两个要点:
- 回调通过
instance_exec执行,运行在模型实例的上下文中,所以self.resolved_at、id等模型方法都能直接使用 - 一个事件里可以写多个
before或after,按代码中出现的先后顺序依次执行
钩子参数传递:事件方法的参数会原样转发
这是 stateful_enum 钩子最有用的能力:事件方法接收的所有参数(位置参数和关键字参数),会原封不动地转发给每一个回调块。
event :close do before do |closed_by:, reason: nil| self.closed_by = closed_by self.close_reason = reason end after do |closed_by:, **| Notifier.notify "Bug##{id} was closed by #{closed_by.name}" end transition all - [:closed] => :closed end触发时直接传参:
@bug.close(closed_by: current_user, reason: 'Duplicate')参数传递的完整规则:
| 写法 | 说明 |
|---|---|
\|closed_by:\| | 必填关键字参数 |
\|reason: nil\| | 带默认值的关键字参数 |
\|**, \| | 吸收并忽略其余所有参数 |
| 位置参数 | 同样被转发,如\|arg1, arg2\| |
带!的事件方法(如@bug.close!)与不带!的版本一样转发参数,区别只在于状态不合法时一个抛异常、一个返回false。测试用例里reopen(reason: 'not fixed')的完整验证见 test/dummy/app/models/bug.rb。
执行顺序:before → 状态变更 → after
事件方法被调用时,实际发生的顺序是(可对照 lib/stateful_enum/machine.rb 中Event类的实现):
| 步骤 | 动作 |
|---|---|
| ① | 校验当前状态是否存在合法迁移(含:if/:unless条件) |
| ② | 依次执行所有before回调,参数完整传入 |
| ③ | 执行真正的状态变更(内部调用 ActiveRecord::Enum 生成的方法) |
| ④ | 依次执行所有after回调,参数完整传入 |
两个值得注意的细节:
- ⚠️迁移不合法时,钩子不会执行,事件方法直接返回
false(!版本抛出Invalid transition)。所以 before 回调可以放心当作"前置校验 + 数据准备"使用 - 💡 想在触发事件前先确认是否合法,可以用自动生成的
can_谓词,如@bug.can_close?
3 个常见坑位
- 不会自动保存数据库:事件方法只修改内存中的对象属性,不会调用
save。需要持久化时请自行保存,常见做法是在after回调里save,或在使用侧显式@bug.save - before 里做重活要谨慎:此时状态还没变更,如果 before 中抛异常,状态将保持不变——这既是风险也是"免费"的回滚机会
- 同一状态不要重复定义迁移:同一个事件里,一个来源状态只能有一条迁移,重复定义会直接报错,测试用例见 test/mechanic_machine_test.rb
调试技巧:快速查看当前可用事件
排查钩子问题时,可以先看看当前状态下哪些事件可触发:
Bug.new(status: :assigned).stateful_enum.possible_event_names #=> [:resolve, :close]该功能由 lib/stateful_enum/state_inspection.rb 提供,还可以用possible_states查看迁移后的状态列表。
总结
| 钩子 | 执行时机 | 典型用途 |
|---|---|---|
before | 状态变更之前 | 记录时间、填写操作人、数据准备 |
after | 状态变更之后 | 发送通知、写日志、持久化 |
- 事件方法的位置参数与关键字参数会全部转发给每个回调,签名灵活
- 迁移不合法时回调不执行,配合
can_x?谓词可做前置检查 - 钩子运行在模型实例上下文中,直接访问模型属性与业务方法
掌握这套 before/after 事件回调 + 参数传递机制,你的状态机就能在"最正确的时机"做"最正确的事"。
【免费下载链接】stateful_enumA very simple state machine plugin built on top of ActiveRecord::Enum项目地址: https://gitcode.com/gh_mirrors/st/stateful_enum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考