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