state

Value

Stored Value

  • All stored values of states are assumed to be strings regardless of the type used for defining states and events
  • To store states as symbols instead you'll have to be explicit about it:
class Vehicle
  state_machine :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end

    states.each do |state|
      self.state(state.name, :value => state.name.to_sym)
    end
  end
end

v = Vehicle.new     # => #<Vehicle:0xb71da5f8 @state=:parked>
v.state?('parked')  # => true
v.state?(:parked)   # => true
  • Every defined state can be re-configured with a custom stored value. For example:
  class Vehicle
    state_machine :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end

      state :idling, :value => 'IDLING'
      state :parked, :value => 'PARKED
    end
  end
  • A lambda block can be used to define the state's value. For example, instead of storing the state name in a DB column, you can store the state's foreign key:
  class VehicleState < ActiveRecord::Base
  end

  class Vehicle < ActiveRecord::Base
    state_machine :attribute => :state_id, :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end

      states.each do |state|
        self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
      end
    end
  end

In the above example, each known state is configured to store it's associated database id in the state_id attribute.

To avoid constant db hits for looking up the VehicleState ids, the value is cached by specifying the :cache option. Alternatively, a custom caching strategy can be used:

  class VehicleState < ActiveRecord::Base
    cattr_accessor :cache_store
    self.cache_store = ActiveSupport::Cache::MemoryStore.new

    def self.find_by_name(name)
      cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
    end
  end

Attribute Access

  class Vehicle
    attr_accessor :state

    state_machine :state, :initial => :parked do
      ...
      state :parked, :value => 0
      start :idling, :value => 1
    end
  end

  vehicle = Vehicle.new # => #<Vehicle:0xb712da60 @state=0>
  vehicle.state         # => 0
  vehicle.parked?       # => true
  vehicle.state = 1
  vehicle.idling?       # => true

As seen in the example above, state_machine treats the attribute (state in this case) like a basic attr_accessor that's been defined on the class. There are no special behaviors added, such as allowing the attribute to be written to based on the name of a state in the machine. This is the case for a few reasons:

  • Setting the attribute directly is an edge case that is meant to only be used when you want to skip state_machine altogether. This means that state_machine shouldn't have any effect on the attribute accessor methods. If you want to change the state, you should be using one of the events defined in the state machine.
  • Many ORMs provide custom behavior for the attribute reader / writer - it may even be defined by your own framework / method implementation just the example above showed. In order to avoid having to worry about the different ways an attribute can get written, state_machine just makes sure that the configured value for a state is always used when writing to the attribute.

If you were interested in accessing the name of a state (instead of its actual value through the attribute), you could do the following:

  vehicle.state_name    # => :idling

Dynamic Values

In addition to customizing states with other value types, lambda blocks can also be specified to allow for a state's value to be determined dynamically

  class Vehicle
    state_machine :purchased_at, :initial => :available do
      event :purchase do
        transition all => :purchased
      end

      event :restock do
        transition all => :available
      end

      state :available, :value => nil
      state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
    end
  end

:purchased state here is customized with both a dynamic value and a value matcher.

When an object transitions to the purchased state, the value's lambda block will be called. This will get the current time and store it in the object's purchased_at attribute.

  • Since there's no way for the state machine to figure out an object's state when it's set to a runtime value, it must be explicitly defined. If the :if option were not configured for the state, then an ArgumentError exception would be raised at runtime

results matching ""

    No results matching ""