Используя государственную машину pluginaweek, я могу ссылаться на объект ActiveRecord, установленным в ходе мероприятия?


Я пытаюсь реализовать событие" suspend", которое переводит объект в состояние: suspended. Но мне нужно уметь "ничего не подозревать", и вернуться в прежнее состояние. Я добавил в модель поле previous_state, но не вижу, как получить к нему доступ внутри блока событий.

Это основная логика, которую я пытаюсь реализовать:

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

Документы state_machine не очень помогли, и я не могу найти примеры в интернете. Иногда трудно понять, как описать что-то другому. google :)

3 3

3 ответа:

Автор state_machine также предоставил альтернативное решение здесь: https://groups.google.com/d/msg/pluginaweek-talk/LL9VJdL_x9c/vP1qv6br734J

А именно:

Другое возможное решение состоит в том, чтобы быть немного творческим с тем, как работает государственная машина. Существует множество крючков в ORMs, таких как ActiveRecord, которые дают нам возможность устанавливать состояние на любой стадии процесса. Рассмотрим следующее:
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

В этом примере новое состояние, восстановленное, является введено, хотя на самом деле оно никогда не сохраняется в базе данных. Вместо этого мы предоставляем крюк before_validation, который перезаписывает состояние на основе предыдущего состояния. Вы можете увидеть результаты ниже:

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

Это должно потребовать на одно попадание в базу данных меньше, так как это происходит до проверки. В моем случае полная картина выглядит так:

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

На мой взгляд, это не идеальное решение, но я понял, как выполнить свою задачу:

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
Я начал с добавления состояния" unsuspended " к моей машине. Хотя я никогда не хочу, чтобы что-то оставалось в этом состоянии, я не могу динамически сказать state_machine, в какое состояние я хочу не подозревать.

Я добавил обратный вызов before_transition к событию suspend, чтобы сохранить состояние до его приостановки.

Я добавил after_transition обратного вызова для восстановления событий, поэтому состояние немедленно обновляется до предыдущего состояния, и это предыдущее состояние затем стирается, чтобы предотвратить проблемы позже в жизни объекта.

Это не идеально. Это работает, но это намного сложнее, чем просто создавать события suspend и unsuspend как автономные методы. Я не пошел по этому пути, потому что я хочу, чтобы state_machine контролировал все изменения состояния, и выход из него удаляет защиту от перемещения в/из недопустимых состояний, обратных вызовов и т. д.

Простое решение

Я предоставляю альтернативную версию решения, которая использует аргумент блока owner. Может пригодиться это в некоторых случаях.

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

Использование around_transition

Также обратите внимание, что вы можете заменить два блока unsuspend на 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