Using pluginaweek's state_machine, can I refer

2019-04-11 22:32发布

问题:

I'm trying to implement a "suspend" event that transitions the object to the :suspended state. But I need to be able to "unsuspend", and return to the previous state. I added a previous_state field to the model, but I can't see how to access it inside an event block.

This is the basic logic I'm trying to implement:

event :suspend do
  owner.previous_state = self.state
  transition [:new, :old] => :suspended
end

event :unsuspend do
  transition :suspended => owner.previous_state.to_sym
  owner.previous_state = nil
end

The state_machine docs haven't been very helpful, and I can't find examples online. Sometimes it's tough to know how to describe something to google :)

回答1:

The author of state_machine has also provided an alternate solution here: https://groups.google.com/d/msg/pluginaweek-talk/LL9VJdL_x9c/vP1qv6br734J

To wit:

Another possible solution is to be a little creative with how the state machine works. There are plenty of hooks in ORMs like ActiveRecord that give us the ability to set the state at any stage in the process. Consider the following:

class Vehicle < ActiveRecord::Base
  before_validation do |vehicle|
    # Set the actual value based on the previous state if we've just restored
    vehicle.state = vehicle.previous_state if vehicle.restored?
  end

  state_machine :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end

    event :restore do
      transition any => :restored
    end

    state :parked do
      validates_presence_of :name
    end
  end

  # Look up previous state here...
  def previous_state
    'parked'
  end
end

In this example, a new state, restored, is introduced even though it never actually gets persisted in the database. We instead provide a before_validation hook that rewrites the state based on the previous state. You can see the results below:

v = Vehicle.new(:name => 'test')  # => #<Vehicle id: nil, name: "test", state: "parked">
v.save                            # => true
v.name = nil                      # => nil
v.ignite                          # => true
v                                 # => #<Vehicle id: 1, name: nil, state: "idling">
v.restore                         # => false
v.errors                          # => #<OrderedHash {:name=>["can't be blank"]}>
v.state                           # => "idling"
v.name = 'test'                   # => "test"
v.restore                         # => true
v                                 # => #<Vehicle id: 1, name: "test", state: "parked">
v.parked?                         # => true

This should require one less database hit as it occurs before validation. In my case the complete picture looks like this:

module Interpreting::Status

  extend ActiveSupport::Concern

  included do

    before_validation :restore_previous_state, if: :interpreter_cancelled?

    state_machine :state, :initial => :ordered do

      before_transition :to => :interpreter_booked, :do => :set_previous_state

      state :ordered

      state :confirmed

      state :interpreter_booked

      state :interpreter_cancelled # Transient status
    end

  end


protected

  def set_previous_state
    self.previous_state = self.state
  end

  def restore_previous_state
    self.state = self.previous_state
  end

end


回答2:

This isn't a perfect solution in my opinion, but I did figure out how to accomplish my task:

state_machine :initial => :new do
  state :new

  state :old

  state :suspended
  before_transition :to => :suspended, :do => :set_previous_state

  state :unsuspended
  after_transition :to => :unsuspended, :do => :restore_previous_state

  event :suspend do
    transition any - :suspended => :suspended
  end

  event :unsuspend do
    transition :suspended => :unsuspended, :if => :previous_state_present?
  end
end

private

def previous_state_present?
  previous_state.present?
end

def set_previous_state
  self.previous_state = state
end

def restore_previous_state
  if previous_state
    self.state = previous_state
    self.previous_state = nil
  end
end

I started by adding an "unsuspended" state to my machine. While I never want something to stay in this state, I can't dynamically tell state_machine what state I want to unsuspend to.

I added a before_transition callback to the suspend event, to save the state before it is suspended.

I added an after_transition callback to the unsuspend event, so the state is immediately updated to the previous state, and that previous state is then wiped out to prevent issues later in the object's life.

This is not perfect. It works, but it's a lot more complicated than just creating the suspend and unsuspend events as standalone methods. I didn't go that route because I want state_machine controlling all state changes, and breaking out of that removes the protections against moving to/from invalid states, callbacks, etc.



回答3:

Simple solution

I am providing an alternative version to the solution that uses owner block argument. Might be useful is some cases.

state_machine :initial => :new do
  state :new
  state :old

  before_transition :on => :suspend do |owner|
    owner.previous_state = owner.state
  end

  before_transition :on => :unsuspend do |owner|
    owner.previous_state.present?
  end

  after_transition :on => :unsuspend do |owner|
    owner.state = owner.previous_state
  end

  event :suspend do
    transition any - :suspended => :suspended
  end

  event :unsuspend do
    transition :suspended => :unsuspended
  end
end

Use of around_transition

Also note that you can replace the two unsuspend blocks with around_transition:

around_transition :on => :unsuspend do |owner, transition_block|
  if owner.previous_state.present?
    transition_block.call
    owner.state = owner.previous_state
  end
end