class StateMachine::Machine
Represents a state machine for a particular attribute. State
machines consist of states, events and a set of transitions that define how the state changes after a particular event is fired.
A state machine will not know all of the possible states for an object unless they are referenced somewhere in the state machine definition. As a result, any unused states should be defined with the other_states
or state
helper.
Actions¶ ↑
When an action is configured for a state machine, it is invoked when an object transitions via an event. The success of the event becomes dependent on the success of the action. If the action is successful, then the transitioned state remains persisted. However, if the action fails (by returning false), the transitioned state will be rolled back.
For example,
class Vehicle attr_accessor :fail, :saving_state state_machine :initial => :parked, :action => :save do event :ignite do transition :parked => :idling end event :park do transition :idling => :parked end end def save @saving_state = state fail != true end end vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked"> vehicle.save # => true vehicle.saving_state # => "parked" # The state was "parked" was save was called # Successful event vehicle.ignite # => true vehicle.saving_state # => "idling" # The state was "idling" when save was called vehicle.state # => "idling" # Failed event vehicle.fail = true vehicle.park # => false vehicle.saving_state # => "parked" vehicle.state # => "idling"
As shown, even though the state is set prior to calling the save
action on the object, it will be rolled back to the original state if the action fails. Note that this will also be the case if an exception is raised while calling the action.
Indirect transitions¶ ↑
In addition to the action being run as the result of an event, the action can also be used to run events itself. For example, using the above as an example:
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked"> vehicle.state_event = 'ignite' vehicle.save # => true vehicle.state # => "idling" vehicle.state_event # => nil
As can be seen, the save
action automatically invokes the event stored in the state_event
attribute (:ignite
in this case).
One important note about using this technique for running transitions is that if the class in which the state machine is defined also defines the action being invoked (and not a superclass), then it must manually run the StateMachine
hook that checks for event attributes.
For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel, the default action (save
) is already defined in a base class. As a result, when a state machine is defined in a model / resource, StateMachine
can automatically hook into the save
action.
On the other hand, the Vehicle class from above defined its own save
method (and there is no save
method in its superclass). As a result, it must be modified like so:
def save self.class.state_machines.transitions(self, :save).perform do @saving_state = state fail != true end end
This will add in the functionality for firing the event stored in the state_event
attribute.
Callbacks¶ ↑
Callbacks are supported for hooking before and after every possible transition in the machine. Each callback is invoked in the order in which it was defined. See StateMachine::Machine#before_transition
and StateMachine::Machine#after_transition
for documentation on how to define new callbacks.
Note that callbacks only get executed within the context of an event. As a result, if a class has an initial state when it’s created, any callbacks that would normally get executed when the object enters that state will not get triggered.
For example,
class Vehicle state_machine :initial => :parked do after_transition all => :parked do raise ArgumentError end ... end end vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked"> vehicle.save # => true (no exception raised)
If you need callbacks to get triggered when an object is created, this should be done by one of the following techniques:
-
Use a
before :create
or equivalent hook:class Vehicle before :create, :track_initial_transition state_machine do ... end end
-
Set an initial state and use the correct event to create the object with the proper state, resulting in callbacks being triggered and the object getting persisted (note that the
:pending
state is actually stored as nil):class Vehicle state_machine :initial => :pending after_transition :pending => :parked, :do => :track_initial_transition event :park do transition :pending => :parked end state :pending, :value => nil end end vehicle = Vehicle.new vehicle.park
-
Use a default event attribute that will automatically trigger when the configured action gets run (note that the
:pending
state is actually stored as nil):class Vehicle < ActiveRecord::Base state_machine :initial => :pending after_transition :pending => :parked, :do => :track_initial_transition event :park do transition :pending => :parked end state :pending, :value => nil end def initialize(*) super self.state_event = 'park' end end vehicle = Vehicle.new vehicle.save
Canceling callbacks¶ ↑
Callbacks can be canceled by throwing :halt at any point during the callback. For example,
... throw :halt ...
If a before
callback halts the chain, the associated transition and all later callbacks are canceled. If an after
callback halts the chain, the later callbacks are canceled, but the transition is still successful.
These same rules apply to around
callbacks with the exception that any around
callback that doesn’t yield will essentially result in :halt being thrown. Any code executed after the yield will behave in the same way as after
callbacks.
Note that if a before
callback fails and the bang version of an event was invoked, an exception will be raised instead of returning false. For example,
class Vehicle state_machine :initial => :parked do before_transition any => :idling, :do => lambda {|vehicle| throw :halt} ... end end vehicle = Vehicle.new vehicle.park # => false vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
Observers¶ ↑
Observers, in the sense of external classes and not Ruby’s Observable mechanism, can hook into state machines as well. Such observers use the same callback api that’s used internally.
Below are examples of defining observers for the following state machine:
class Vehicle state_machine do event :park do transition :idling => :parked end ... end ... end
Event/Transition behaviors:
class VehicleObserver def self.before_park(vehicle, transition) logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}" end def self.after_park(vehicle, transition, result) logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}" end def self.before_transition(vehicle, transition) logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}" end def self.after_transition(vehicle, transition) logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}" end def self.around_transition(vehicle, transition) logger.info Benchmark.measure { yield } end end Vehicle.state_machine do before_transition :on => :park, :do => VehicleObserver.method(:before_park) before_transition VehicleObserver.method(:before_transition) after_transition :on => :park, :do => VehicleObserver.method(:after_park) after_transition VehicleObserver.method(:after_transition) around_transition VehicleObserver.method(:around_transition) end
One common callback is to record transitions for all models in the system for auditing/debugging purposes. Below is an example of an observer that can easily automate this process for all models:
class StateMachineObserver def self.before_transition(object, transition) Audit.log_transition(object.attributes) end end [Vehicle, Switch, Project].each do |klass| klass.state_machines.each do |attribute, machine| machine.before_transition StateMachineObserver.method(:before_transition) end end
Additional observer-like behavior may be exposed by the various integrations available. See below for more information on integrations.
Overriding instance / class methods¶ ↑
Hooking in behavior to the generated instance / class methods from the state machine, events, and states is very simple because of the way these methods are generated on the class. Using the class’s ancestors, the original generated method can be referred to via super
. For example,
class Vehicle state_machine do event :park do ... end end def park(*args) logger.info "..." super end end
In the above example, the park
instance method that’s generated on the Vehicle class (by the associated event) is overridden with custom behavior. Once this behavior is complete, the original method from the state machine is invoked by simply calling super
.
The same technique can be used for state
, state_name
, and all other instance and class methods on the Vehicle class.
Method conflicts¶ ↑
By default state_machine does not redefine methods that exist on superclasses (including Object
) or any modules (including Kernel) that were included before it was defined. This is in order to ensure that existing behavior on the class is not broken by the inclusion of state_machine.
If a conflicting method is detected, state_machine will generate a warning. For example, consider the following class:
class Vehicle state_machine do event :open do ... end end end
In the above class, an event named “open” is defined for its state machine. However, “open” is already defined as an instance method in Ruby’s Kernel module that gets included in every Object
. As a result, state_machine will generate the following warning:
Instance method "open" is already defined in Object, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true.
Even though you may not be using Kernel’s implementation of the “open” instance method, state_machine isn’t aware of this and, as a result, stays safe and just skips redefining the method.
As with almost all helpers methods defined by state_machine in your class, there are generic methods available for working around this method conflict. In the example above, you can invoke the “open” event like so:
vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil> vehicle.fire_events(:open) # => true # This will not work vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
If you want to take on the risk of overriding existing methods and just ignore method conflicts altogether, you can do so by setting the following configuration:
StateMachine::Machine.ignore_method_conflicts = true
This will allow you to define events like “open” as described above and still generate the “open” instance helper method. For example:
StateMachine::Machine.ignore_method_conflicts = true class Vehicle state_machine do event :open do ... end end vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil> vehicle.open # => true
By default, state_machine helps prevent you from making mistakes and accidentally overriding methods that you didn’t intend to. Once you understand this and what the consequences are, setting the ignore_method_conflicts
option is a perfectly reasonable workaround.
Integrations
¶ ↑
By default, state machines are library-agnostic, meaning that they work on any Ruby class and have no external dependencies. However, there are certain libraries which expose additional behavior that can be taken advantage of by state machines.
This library is built to work out of the box with a few popular Ruby libraries that allow for additional behavior to provide a cleaner and smoother experience. This is especially the case for objects backed by a database that may allow for transactions, persistent storage, search/filters, callbacks, etc.
When a state machine is defined for classes using any of the above libraries, it will try to automatically determine the integration to use (Agnostic, ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel) based on the class definition. To see how each integration affects the machine’s behavior, refer to all constants defined under the StateMachine::Integrations
namespace.
Attributes
The action to invoke when an object transitions
The callbacks to invoke before/after a transition is performed
Maps :before => callbacks and :after => callbacks
The events that trigger transitions. These are sorted, by default, in the order in which they were defined.
The name of the machine, used for scoping methods generated for the machine as a whole (not states or events)
An identifier that forces all methods (including state predicates and event methods) to be generated with the value prefixed or suffixed, depending on the context.
The class that the machine is defined in
A list of all of the states known to this state machine. This will pull states from the following sources:
-
Initial state
-
State
behaviors -
Event
transitions (:to, :from, and :except_from options) -
Transition
callbacks (:to, :from, :except_to, and :except_from options) -
Unreferenced states (using
other_states
helper)
These are sorted, by default, in the order in which they were referenced.
Whether the machine will use transactions when firing events
Public Class Methods
Draws the state machines defined in the given classes using GraphViz. The given classes must be a comma-delimited string of class names.
Configuration options:
-
:file
- A comma-delimited string of files to load that contain the state machine definitions to draw -
:path
- The path to write the graph file to -
:format
- The image format to generate the graph in -
:font
- The name of the font to draw state names in
# File lib/state_machine/machine.rb 464 def draw(class_names, options = {}) 465 raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any? 466 467 # Load any files 468 if files = options.delete(:file) 469 files.split(',').each {|file| require file} 470 end 471 472 class_names.split(',').each do |class_name| 473 # Navigate through the namespace structure to get to the class 474 klass = Object 475 class_name.split('::').each do |name| 476 klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name) 477 end 478 479 # Draw each of the class's state machines 480 klass.state_machines.each_value do |machine| 481 machine.draw(options) 482 end 483 end 484 end
Attempts to find or create a state machine for the given class. For example,
StateMachine::Machine.find_or_create(Vehicle) StateMachine::Machine.find_or_create(Vehicle, :initial => :parked) StateMachine::Machine.find_or_create(Vehicle, :status) StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
If a machine of the given name already exists in one of the class’s superclasses, then a copy of that machine will be created and stored in the new owner class (the original will remain unchanged).
# File lib/state_machine/machine.rb 431 def find_or_create(owner_class, *args, &block) 432 options = args.last.is_a?(Hash) ? args.pop : {} 433 name = args.first || :state 434 435 # Find an existing machine 436 if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name] 437 # Only create a new copy if changes are being made to the machine in 438 # a subclass 439 if machine.owner_class != owner_class && (options.any? || block_given?) 440 machine = machine.clone 441 machine.initial_state = options[:initial] if options.include?(:initial) 442 machine.owner_class = owner_class 443 end 444 445 # Evaluate DSL 446 machine.instance_eval(&block) if block_given? 447 else 448 # No existing machine: create a new one 449 machine = new(owner_class, name, options, &block) 450 end 451 452 machine 453 end
Creates a new state machine for the given attribute
# File lib/state_machine/machine.rb 539 def initialize(owner_class, *args, &block) 540 options = args.last.is_a?(Hash) ? args.pop : {} 541 assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions) 542 543 # Find an integration that matches this machine's owner class 544 if options.include?(:integration) 545 @integration = options[:integration] && StateMachine::Integrations.find_by_name(options[:integration]) 546 else 547 @integration = StateMachine::Integrations.match(owner_class) 548 end 549 550 if @integration 551 extend @integration 552 options = (@integration.defaults || {}).merge(options) 553 end 554 555 # Add machine-wide defaults 556 options = {:use_transactions => true, :initialize => true}.merge(options) 557 558 # Set machine configuration 559 @name = args.first || :state 560 @attribute = options[:attribute] || @name 561 @events = EventCollection.new(self) 562 @states = StateCollection.new(self) 563 @callbacks = {:before => [], :after => [], :failure => []} 564 @namespace = options[:namespace] 565 @messages = options[:messages] || {} 566 @action = options[:action] 567 @use_transactions = options[:use_transactions] 568 @initialize_state = options[:initialize] 569 @action_hook_defined = false 570 self.owner_class = owner_class 571 self.initial_state = options[:initial] unless sibling_machines.any? 572 573 # Merge with sibling machine configurations 574 add_sibling_machine_configs 575 576 # Define class integration 577 define_helpers 578 define_scopes(options[:plural]) 579 after_initialize 580 581 # Evaluate DSL 582 instance_eval(&block) if block_given? 583 end
Public Instance Methods
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
# File lib/state_machine/machine.rb 1945 def action_hook?(self_only = false) 1946 @action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)} 1947 end
Creates a callback that will be invoked after a transition failures to be performed so long as the given requirements match the transition.
See before_transition
for a description of the possible configurations for defining callbacks. Note however that you cannot define the state requirements in these callbacks. You may only define event requirements.
The callback¶ ↑
Failure callbacks get invoked whenever an event fails to execute. This can happen when no transition is available, a before
callback halts execution, or the action associated with this machine fails to succeed. In any of these cases, any failure callback that matches the attempted transition will be run.
For example,
class Vehicle state_machine do after_failure do |vehicle, transition| logger.error "vehicle #{vehicle} failed to transition on #{transition.event}" end after_failure :on => :ignite, :do => :log_ignition_failure ... end end
# File lib/state_machine/machine.rb 1781 def after_failure(*args, &block) 1782 options = (args.last.is_a?(Hash) ? args.pop : {}) 1783 options[:do] = args if args.any? 1784 assert_valid_keys(options, :on, :do, :if, :unless) 1785 1786 add_callback(:failure, options, &block) 1787 end
Creates a callback that will be invoked after a transition is performed so long as the given requirements match the transition.
See before_transition
for a description of the possible configurations for defining callbacks.
# File lib/state_machine/machine.rb 1686 def after_transition(*args, &block) 1687 options = (args.last.is_a?(Hash) ? args.pop : {}) 1688 options[:do] = args if args.any? 1689 add_callback(:after, options, &block) 1690 end
Creates a callback that will be invoked around a transition so long as the given requirements match the transition.
The callback¶ ↑
Around callbacks wrap transitions, executing code both before and after. These callbacks are defined in the exact same manner as before / after callbacks with the exception that the transition must be yielded to in order to finish running it.
If defining around
callbacks using blocks, you must yield within the transition by directly calling the block (since yielding is not allowed within blocks).
For example,
class Vehicle state_machine do around_transition do |block| Benchmark.measure { block.call } end around_transition do |vehicle, block| logger.info "vehicle was #{state}..." block.call logger.info "...and is now #{state}" end around_transition do |vehicle, transition, block| logger.info "before #{transition.event}: #{vehicle.state}" block.call logger.info "after #{transition.event}: #{vehicle.state}" end end end
Notice that referencing the block is similar to doing so within an actual method definition in that it is always the last argument.
On the other hand, if you’re defining around
callbacks using method references, you can yield like normal:
class Vehicle state_machine do around_transition :benchmark ... end def benchmark Benchmark.measure { yield } end end
See before_transition
for a description of the possible configurations for defining callbacks.
# File lib/state_machine/machine.rb 1747 def around_transition(*args, &block) 1748 options = (args.last.is_a?(Hash) ? args.pop : {}) 1749 options[:do] = args if args.any? 1750 add_callback(:around, options, &block) 1751 end
Gets the actual name of the attribute on the machine’s owner class that stores data with the given name.
# File lib/state_machine/machine.rb 714 def attribute(name = :state) 715 name == :state ? @attribute : :"#{self.name}_#{name}" 716 end
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
The callback¶ ↑
Callbacks must be defined as either an argument, in the :do option, or as a block. For example,
class Vehicle state_machine do before_transition :set_alarm before_transition :set_alarm, all => :parked before_transition all => :parked, :do => :set_alarm before_transition all => :parked do |vehicle, transition| vehicle.set_alarm end ... end end
Notice that the first three callbacks are the same in terms of how the methods to invoke are defined. However, using the :do
can provide for a more fluid DSL.
In addition, multiple callbacks can be defined like so:
class Vehicle state_machine do before_transition :set_alarm, :lock_doors, all => :parked before_transition all => :parked, :do => [:set_alarm, :lock_doors] before_transition :set_alarm do |vehicle, transition| vehicle.lock_doors end end end
Notice that the different ways of configuring methods can be mixed.
State
requirements¶ ↑
Callbacks can require that the machine be transitioning from and to specific states. These requirements use a Hash syntax to map beginning states to ending states. For example,
before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
In this case, the set_alarm
callback will only be called if the machine is transitioning from parked
to idling
or from idling
to parked
.
To help define state requirements, a set of helpers are available for slightly more complex matching:
-
all
- Matches every state/event in the machine -
all - [:parked, :idling, ...]
- Matches every state/event except those specified -
any
- An alias forall
(matches every state/event in the machine) -
same
- Matches the same state being transitioned from
See StateMachine::MatcherHelpers
for more information.
Examples:
before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling before_transition all => :parked, :do => ... # Matches all states to parked before_transition any => same, :do => ... # Matches every loopback
Event
requirements¶ ↑
In addition to state requirements, an event requirement can be defined so that the callback is only invoked on specific events using the on
option. This can also use the same matcher helpers as the state requirements.
Examples:
before_transition :on => :ignite, :do => ... # Matches only on ignite before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
Verbose Requirements¶ ↑
Requirements can also be defined using verbose options rather than the implicit Hash syntax and helper methods described above.
Configuration options:
-
:from
- One or more states being transitioned from. If none are specified, then all states will match. -
:to
- One or more states being transitioned to. If none are specified, then all states will match. -
:on
- One or more events that fired the transition. If none are specified, then all events will match. -
:except_from
- One or more states not being transitioned from -
:except_to
- One more states not being transitioned to -
:except_on
- One or more events that *did not* fire the transition
Examples:
before_transition :from => :ignite, :to => :idling, :on => :park, :do => ... before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
Conditions¶ ↑
In addition to the state/event requirements, a condition can also be defined to help determine whether the callback should be invoked.
Configuration options:
-
:if
- A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value. -
:unless
- A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
Examples:
before_transition :parked => :idling, :if => :moving?, :do => ... before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
Accessing the transition¶ ↑
In addition to passing the object being transitioned, the actual transition describing the context (e.g. event, from, to) can be accessed as well. This additional argument is only passed if the callback allows for it.
For example,
class Vehicle # Only specifies one parameter (the object being transitioned) before_transition all => :parked do |vehicle| vehicle.set_alarm end # Specifies 2 parameters (object being transitioned and actual transition) before_transition all => :parked do |vehicle, transition| vehicle.set_alarm(transition) end end
Note that the object in the callback will only be passed in as an argument if callbacks are configured to not be bound to the object involved. This is the default and may change on a per-integration basis.
See StateMachine::Transition
for more information about the attributes available on the transition.
Usage with delegates¶ ↑
As noted above, state_machine uses the callback method’s argument list arity to determine whether to include the transition in the method call. If you’re using delegates, such as those defined in ActiveSupport or Forwardable, the actual arity of the delegated method gets masked. This means that callbacks which reference delegates will always get passed the transition as an argument. For example:
class Vehicle extend Forwardable delegate :refresh => :dashboard state_machine do before_transition :refresh ... end def dashboard @dashboard ||= Dashboard.new end end class Dashboard def refresh(transition) # ... end end
In the above example, Dashboard#refresh
must defined a transition
argument. Otherwise, an ArgumentError
exception will get raised. The only way around this is to avoid the use of delegates and manually define the delegate method so that the correct arity is used.
Examples¶ ↑
Below is an example of a class with one state machine and various types of before
transitions defined for it:
class Vehicle state_machine do # Before all transitions before_transition :update_dashboard # Before specific transition: before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt # With conditional callback: before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on? # Using helpers: before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard ... end end
As can be seen, any number of transitions can be created using various combinations of configuration options.
# File lib/state_machine/machine.rb 1675 def before_transition(*args, &block) 1676 options = (args.last.is_a?(Hash) ? args.pop : {}) 1677 options[:do] = args if args.any? 1678 add_callback(:before, options, &block) 1679 end
Defines a new helper method in an instance or class scope with the given name. If the method is already defined in the scope, then this will not override it.
If passing in a block, there are two side effects to be aware of
-
The method cannot be chained, meaning that the block cannot call
super
-
If the method is already defined in an ancestor, then it will not get overridden and a warning will be output.
Example:
# Instance helper machine.define_helper(:instance, :state_name) do |machine, object| machine.states.match(object).name end # Class helper machine.define_helper(:class, :state_machine_name) do |machine, klass| "State" end
You can also define helpers using string evaluation like so:
# Instance helper machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 def state_name self.class.state_machine(:state).states.match(self).name end end_eval # Class helper machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1 def state_machine_name "State" end end_eval
# File lib/state_machine/machine.rb 754 def define_helper(scope, method, *args, &block) 755 helper_module = @helper_modules.fetch(scope) 756 757 if block_given? 758 if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method) 759 ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s 760 warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true." 761 else 762 name = self.name 763 helper_module.class_eval do 764 define_method(method) do |*block_args| 765 block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args) 766 end 767 end 768 end 769 else 770 helper_module.class_eval(method, *args) 771 end 772 end
Draws a directed graph of the machine for visualizing the various events, states, and their transitions.
This requires both the Ruby graphviz gem and the graphviz library be installed on the system.
Configuration options:
-
:name
- The name of the file to write to (without the file extension). Default is “#{owner_class.name}_#{name}” -
:path
- The path to write the graph file to. Default is the current directory (“.”). -
:format
- The image format to generate the graph in. Default is “png’. -
:font
- The name of the font to draw state names in. Default is “Arial”. -
:orientation
- The direction of the graph (“portrait” or “landscape”). Default is “portrait”. -
:human_names
- Whether to use human state / event names for node labels on the graph instead of the internal name. Default is false.
# File lib/state_machine/machine.rb 1927 def draw(graph_options = {}) 1928 name = graph_options.delete(:name) || "#{owner_class.name}_#{self.name}" 1929 draw_options = {:human_name => false} 1930 draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names) 1931 1932 graph = Graph.new(name, graph_options) 1933 1934 # Add nodes / edges 1935 states.by_priority.each {|state| state.draw(graph, draw_options)} 1936 events.each {|event| event.draw(graph, draw_options)} 1937 1938 # Output result 1939 graph.output 1940 graph 1941 end
Whether a dynamic initial state is being used in the machine
# File lib/state_machine/machine.rb 687 def dynamic_initial_state? 688 instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc) 689 end
Gets a description of the errors for the given object. This is used to provide more detailed information when an InvalidTransition
exception is raised.
# File lib/state_machine/machine.rb 1871 def errors_for(object) 1872 '' 1873 end
Defines one or more events for the machine and the transitions that can be performed when those events are run.
This method is also aliased as on
for improved compatibility with using a domain-specific language.
Configuration options:
-
:human_name
- The human-readable version of this event’s name. By default, this is either defined by the integration or stringifies the name and converts underscores to spaces.
Instance methods¶ ↑
The following instance methods are generated when a new event is defined (the “park” event is used as an example):
-
park(..., run_action = true)
- Fires the “park” event, transitioning from the current state to the next valid state. If the last argument is a boolean, it will control whether the machine’s action gets run. -
park!(..., run_action = true)
- Fires the “park” event, transitioning from the current state to the next valid state. If the transition fails, then aStateMachine::InvalidTransition
error will be raised. If the last argument is a boolean, it will control whether the machine’s action gets run. -
can_park?(requirements = {})
- Checks whether the “park” event can be fired given the current state of the object. This will not run validations or callbacks in ORM integrations. It will only determine if the state machine defines a valid transition for the event. To check whether an event can fire and passes validations, use event attributes (e.g. state_event) as described in the “Events” documentation of each ORM integration. -
park_transition(requirements = {})
- Gets the next transition that would be performed if the “park” event were to be fired now on the object or nil if no transitions can be performed. Likecan_park?
this will also not run validations or callbacks. It will only determine if the state machine defines a valid transition for the event.
With a namespace of “car”, the above names map to the following methods:
-
can_park_car?
-
park_car_transition
-
park_car
-
park_car!
The can_park?
and park_transition
helpers both take an optional set of requirements for determining what transitions are available for the current object. These requirements include:
-
:from
- One or more states to transition from. If none are specified, then this will be the object’s current state. -
:to
- One or more states to transition to. If none are specified, then this will match any to state. -
:guard
- Whether to guard transitions with the if/unless conditionals defined for each one. Default is true.
Defining transitions¶ ↑
event
requires a block which allows you to define the possible transitions that can happen as a result of that event. For example,
event :park, :stop do transition :idling => :parked end event :first_gear do transition :parked => :first_gear, :if => :seatbelt_on? transition :parked => same # Allow to loopback if seatbelt is off end
See StateMachine::Event#transition
for more information on the possible options that can be passed in.
Note that this block is executed within the context of the actual event object. As a result, you will not be able to reference any class methods on the model without referencing the class itself. For example,
class Vehicle def self.safe_states [:parked, :idling, :stalled] end state_machine do event :park do transition Vehicle.safe_states => :parked end end end
Overriding the event method¶ ↑
By default, this will define an instance method (with the same name as the event) that will fire the next possible transition for that. Although the before_transition
, after_transition
, and around_transition
hooks allow you to define behavior that gets executed as a result of the event’s transition, you can also override the event method in order to have a little more fine-grained control.
For example:
class Vehicle state_machine do event :park do ... end end def park(*) take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible if result = super # Runs the transition and all before/after/around hooks applaud # Executes after the transition (and after_transition hooks) end result end end
There are a few important things to note here. First, the method signature is defined with an unlimited argument list in order to allow callers to continue passing arguments that are expected by state_machine. For example, it will still allow calls to park
with a single parameter for skipping the configured action.
Second, the overridden event method must call super
in order to run the logic for running the next possible transition. In order to remain consistent with other events, the result of super
is returned.
Third, any behavior defined in this method will not get executed if you’re taking advantage of attribute-based event transitions. For example:
vehicle = Vehicle.new vehicle.state_event = 'park' vehicle.save
In this case, the park
event will run the before/after/around transition hooks and transition the state, but the behavior defined in the overriden park
method will not be executed.
Defining additional arguments¶ ↑
Additional arguments can be passed into events and accessed by transition hooks like so:
class Vehicle state_machine do after_transition :on => :park do |vehicle, transition| kind = *transition.args # :parallel ... end after_transition :on => :park, :do => :take_deep_breath event :park do ... end def take_deep_breath(transition) kind = *transition.args # :parallel ... end end end vehicle = Vehicle.new vehicle.park(:parallel)
Remember that if the last argument is a boolean, it will be used as the run_action
parameter to the event action. Using the park
action example from above, you can might call it like so:
vehicle.park # => Uses default args and runs machine action vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
If you decide to override the park
event method and define additional arguments, you can do so as shown below:
class Vehicle state_machine do event :park do ... end end def park(kind = :parallel, *args) take_deep_breath if kind == :parallel super end end
Note that super
is called instead of super(*args)
. This allow the entire arguments list to be accessed by transition callbacks through StateMachine::Transition#args
.
Using matchers¶ ↑
The all
/ any
matchers can be used to easily execute blocks for a group of events. Note, however, that you cannot use these matchers to set configurations for events. Blocks using these matchers can be defined at any point in the state machine and will always get applied to the proper events.
For example:
state_machine :initial => :parked do ... event all - [:crash] do transition :stalled => :parked end end
Example¶ ↑
class Vehicle state_machine do # The park, stop, and halt events will all share the given transitions event :park, :stop, :halt do transition [:idling, :backing_up] => :parked end event :stop do transition :first_gear => :idling end event :ignite do transition :parked => :idling transition :idling => same # Allow ignite while still idling end end end
# File lib/state_machine/machine.rb 1342 def event(*names, &block) 1343 options = names.last.is_a?(Hash) ? names.pop : {} 1344 assert_valid_keys(options, :human_name) 1345 1346 # Store the context so that it can be used for / matched against any event 1347 # that gets added 1348 @events.context(names, &block) if block_given? 1349 1350 if names.first.is_a?(Matcher) 1351 # Add any events referenced in the matcher. When matchers are used, 1352 # events are not allowed to be configured. 1353 raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any? 1354 events = add_events(names.first.values) 1355 else 1356 events = add_events(names) 1357 1358 # Update the configuration for the event(s) 1359 events.each do |event| 1360 event.human_name = options[:human_name] if options.include?(:human_name) 1361 1362 # Add any states that may have been referenced within the event 1363 add_states(event.known_states) 1364 end 1365 end 1366 1367 events.length == 1 ? events.first : events 1368 end
Generates the message to use when invalidating the given object after failing to transition on a specific event
# File lib/state_machine/machine.rb 1883 def generate_message(name, values = []) 1884 message = (@messages[name] || self.class.default_messages[name]) 1885 1886 # Check whether there are actually any values to interpolate to avoid 1887 # any warnings 1888 if message.scan(/%./).any? {|match| match != '%%'} 1889 message % values.map {|value| value.last} 1890 else 1891 message 1892 end 1893 end
Gets the initial state of the machine for the given object. If a dynamic initial state was configured for this machine, then the object will be passed into the lambda block to help determine the actual state.
Examples¶ ↑
With a static initial state:
class Vehicle state_machine :initial => :parked do ... end end vehicle = Vehicle.new Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
With a dynamic initial state:
class Vehicle attr_accessor :force_idle state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do ... end end vehicle = Vehicle.new vehicle.force_idle = true Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false> vehicle.force_idle = false Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
# File lib/state_machine/machine.rb 682 def initial_state(object) 683 states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state') 684 end
Sets the initial state of the machine. This can be either the static name of a state or a lambda block which determines the initial state at creation time.
# File lib/state_machine/machine.rb 629 def initial_state=(new_initial_state) 630 @initial_state = new_initial_state 631 add_states([@initial_state]) unless dynamic_initial_state? 632 633 # Update all states to reflect the new initial state 634 states.each {|state| state.initial = (state.name == @initial_state)} 635 636 # Output a warning if there are conflicting initial states for the machine's 637 # attribute 638 initial_state = states.detect {|state| state.initial} 639 if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)) 640 warn( 641 "Both #{owner_class.name} and its #{name.inspect} machine have defined "\ 642 "a different default for \"#{attribute}\". Use only one or the other for "\ 643 "defining defaults to avoid unexpected behaviors." 644 ) 645 end 646 end
Initializes the state on the given object. Initial values are only set if the machine’s attribute hasn’t been previously initialized.
Configuration options:
-
:force
- Whether to initialize the state regardless of its current value -
:to
- A hash to set the initial value in instead of writing directly to the object
# File lib/state_machine/machine.rb 699 def initialize_state(object, options = {}) 700 state = initial_state(object) 701 if state && (options[:force] || initialize_state?(object)) 702 value = state.value 703 704 if hash = options[:to] 705 hash[attribute.to_s] = value 706 else 707 write(object, :state, value) 708 end 709 end 710 end
Marks the given object as invalid with the given message.
By default, this is a no-op.
# File lib/state_machine/machine.rb 1865 def invalidate(object, attribute, message, values = []) 1866 end
Sets the class which is the owner of this state machine. Any methods generated by states, events, or other parts of the machine will be defined on the given owner class.
# File lib/state_machine/machine.rb 601 def owner_class=(klass) 602 @owner_class = klass 603 604 # Create modules for extending the class with state/event-specific methods 605 @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)} 606 owner_class.class_eval do 607 extend helper_modules[:class] 608 include helper_modules[:instance] 609 end 610 611 # Add class-/instance-level methods to the owner class for state initialization 612 unless owner_class < StateMachine::InstanceMethods 613 owner_class.class_eval do 614 extend StateMachine::ClassMethods 615 include StateMachine::InstanceMethods 616 end 617 618 define_state_initializer if @initialize_state 619 end 620 621 # Record this machine as matched to the name in the current owner class. 622 # This will override any machines mapped to the same name in any superclasses. 623 owner_class.state_machines[name] = self 624 end
Generates a list of the possible transition sequences that can be run on the given object. These paths can reveal all of the possible states and events that can be encountered in the object’s state machine based on the object’s current state.
Configuration options:
-
from
- The initial state to start all paths from. By default, this is the object’s current state. -
to
- The target state to end all paths on. By default, paths will end when they loop back to the first transition on the path. -
deep
- Whether to allow the target state to be crossed more than once in a path. By default, paths will immediately stop when the target state (if specified) is reached. If this is enabled, then paths can continue even after reaching the target state; they will stop when reaching the target state a second time.
Note that the object is never modified when the list of paths is generated.
Examples¶ ↑
class Vehicle state_machine :initial => :parked do event :ignite do transition :parked => :idling end event :shift_up do transition :idling => :first_gear, :first_gear => :second_gear end event :shift_down do transition :second_gear => :first_gear, :first_gear => :idling end end end vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked"> vehicle.state # => "parked" vehicle.state_paths # => [ # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>, # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>, # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>, # #<StateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>, # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>], # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>, # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>, # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>] # ] vehicle.state_paths(:from => :parked, :to => :second_gear) # => [ # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>, # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>, # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>] # ]
In addition to getting the possible paths that can be accessed, you can also get summary information about the states / events that can be accessed at some point along one of the paths. For example:
# Get the list of states that can be accessed from the current state vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear] # Get the list of events that can be accessed from the current state vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
# File lib/state_machine/machine.rb 1858 def paths_for(object, requirements = {}) 1859 PathCollection.new(object, self, requirements) 1860 end
Gets the current value stored in the given object’s attribute.
For example,
class Vehicle state_machine :initial => :parked do ... end end vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
# File lib/state_machine/machine.rb 1087 def read(object, attribute, ivar = false) 1088 attribute = self.attribute(attribute) 1089 if ivar 1090 object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil 1091 else 1092 object.send(attribute) 1093 end 1094 end
Resets any errors previously added when invalidating the given object.
By default, this is a no-op.
# File lib/state_machine/machine.rb 1878 def reset(object) 1879 end
Customizes the definition of one or more states in the machine.
Configuration options:
-
:value
- The actual value to store when an object transitions to the state. Default is the name (stringified). -
:cache
- If a dynamic value (via a lambda block) is being used, then setting this to true will cache the evaluated result -
:if
- Determines whether an object’s value matches the state (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}). By default, the configured value is matched. -
:human_name
- The human-readable version of this state’s name. By default, this is either defined by the integration or stringifies the name and converts underscores to spaces.
Customizing the stored value¶ ↑
Whenever a state is automatically discovered in the state machine, its default value is assumed to be the stringified version of the name. For example,
class Vehicle state_machine :initial => :parked do event :ignite do transition :parked => :idling end end end
In the above state machine, there are two states automatically discovered: :parked and :idling. These states, by default, will store their stringified equivalents when an object moves into that state (e.g. “parked” / “idling”).
For legacy systems or when tying state machines into existing frameworks, it’s oftentimes necessary to need to store a different value for a state than the default. In order to continue taking advantage of an expressive state machine and helper methods, 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
This is also useful if being used in association with a database and, instead of storing the state name in a column, you want to 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. Also, notice that a lambda block is used to define the state’s value. This is required in situations (like testing) where the model is loaded without any existing data (i.e. no VehicleState records available).
One caveat to the above example is to keep performance in mind. 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 like so:
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
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 at runtime. For example,
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
In the above definition, the :purchased
state 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.
Note that the custom matcher is very important here. 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, indicating that the state machine could not figure out what the current state of the object was.
Behaviors¶ ↑
Behaviors define a series of methods to mixin with objects when the current state matches the given one(s). This allows instance methods to behave a specific way depending on what the value of the object’s state is.
For example,
class Vehicle attr_accessor :driver attr_accessor :passenger state_machine :initial => :parked do event :ignite do transition :parked => :idling end state :parked do def speed 0 end def rotate_driver driver = self.driver self.driver = passenger self.passenger = driver true end end state :idling, :first_gear do def speed 20 end def rotate_driver self.state = 'parked' rotate_driver end end other_states :backing_up end end
In the above example, there are two dynamic behaviors defined for the class:
-
speed
-
rotate_driver
Each of these behaviors are instance methods on the Vehicle class. However, which method actually gets invoked is based on the current state of the object. Using the above class as the example:
vehicle = Vehicle.new vehicle.driver = 'John' vehicle.passenger = 'Jane' # Behaviors in the "parked" state vehicle.state # => "parked" vehicle.speed # => 0 vehicle.rotate_driver # => true vehicle.driver # => "Jane" vehicle.passenger # => "John" vehicle.ignite # => true # Behaviors in the "idling" state vehicle.state # => "idling" vehicle.speed # => 20 vehicle.rotate_driver # => true vehicle.driver # => "John" vehicle.passenger # => "Jane"
As can be seen, both the speed
and rotate_driver
instance method implementations changed how they behave based on what the current state of the vehicle was.
Invalid behaviors¶ ↑
If a specific behavior has not been defined for a state, then a NoMethodError exception will be raised, indicating that that method would not normally exist for an object with that state.
Using the example from before:
vehicle = Vehicle.new vehicle.state = 'backing_up' vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
Using matchers¶ ↑
The all
/ any
matchers can be used to easily define behaviors for a group of states. Note, however, that you cannot use these matchers to set configurations for states. Behaviors using these matchers can be defined at any point in the state machine and will always get applied to the proper states.
For example:
state_machine :initial => :parked do ... state all - [:parked, :idling, :stalled] do validates_presence_of :speed def speed gear * 10 end end end
State-aware class methods¶ ↑
In addition to defining scopes for instance methods that are state-aware, the same can be done for certain types of class methods.
Some libraries have support for class-level methods that only run certain behaviors based on a conditions hash passed in. For example:
class Vehicle < ActiveRecord::Base state_machine do ... state :first_gear, :second_gear, :third_gear do validates_presence_of :speed validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone? end end end
In the above ActiveRecord model, two validations have been defined which will only run when the Vehicle object is in one of the three states: first_gear
, second_gear
, or +third_gear. Notice, also, that if/unless conditions can continue to be used.
This functionality is not library-specific and can work for any class-level method that is defined like so:
def validates_presence_of(attribute, options = {}) ... end
The minimum requirement is that the last argument in the method be an options hash which contains at least :if
condition support.
# File lib/state_machine/machine.rb 1041 def state(*names, &block) 1042 options = names.last.is_a?(Hash) ? names.pop : {} 1043 assert_valid_keys(options, :value, :cache, :if, :human_name) 1044 1045 # Store the context so that it can be used for / matched against any state 1046 # that gets added 1047 @states.context(names, &block) if block_given? 1048 1049 if names.first.is_a?(Matcher) 1050 # Add any states referenced in the matcher. When matchers are used, 1051 # states are not allowed to be configured. 1052 raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any? 1053 states = add_states(names.first.values) 1054 else 1055 states = add_states(names) 1056 1057 # Update the configuration for the state(s) 1058 states.each do |state| 1059 if options.include?(:value) 1060 state.value = options[:value] 1061 self.states.update(state) 1062 end 1063 1064 state.human_name = options[:human_name] if options.include?(:human_name) 1065 state.cache = options[:cache] if options.include?(:cache) 1066 state.matcher = options[:if] if options.include?(:if) 1067 end 1068 end 1069 1070 states.length == 1 ? states.first : states 1071 end
Creates a new transition that determines what to change the current state to when an event fires.
Defining transitions¶ ↑
The options for a new transition uses the Hash syntax to map beginning states to ending states. For example,
transition :parked => :idling, :idling => :first_gear, :on => :ignite
In this case, when the ignite
event is fired, this transition will cause the state to be idling
if it’s current state is parked
or first_gear
if it’s current state is idling
.
To help define these implicit transitions, a set of helpers are available for slightly more complex matching:
-
all
- Matches every state in the machine -
all - [:parked, :idling, ...]
- Matches every state except those specified -
any
- An alias forall
(matches every state in the machine) -
same
- Matches the same state being transitioned from
See StateMachine::MatcherHelpers
for more information.
Examples:
transition all => nil, :on => :ignite # Transitions to nil regardless of the current state transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled transition :parked => same, :on => :park # Loops :parked back to :parked transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
Verbose transitions¶ ↑
Transitions can also be defined use an explicit set of configuration options:
-
:from
- A state or array of states that can be transitioned from. If not specified, then the transition can occur for any state. -
:to
- The state that’s being transitioned to. If not specified, then the transition will simply loop back (i.e. the state will not change). -
:except_from
- A state or array of states that cannot be transitioned from.
These options must be used when defining transitions within the context of a state.
Examples:
transition :to => nil, :on => :park transition :to => :idling, :on => :ignite transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite transition :from => nil, :to => :idling, :on => :ignite transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
Conditions¶ ↑
In addition to the state requirements for each transition, a condition can also be defined to help determine whether that transition is available. These options will work on both the normal and verbose syntax.
Configuration options:
-
:if
- A method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}). The condition should return or evaluate to true or false. -
:unless
- A method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}). The condition should return or evaluate to true or false.
Examples:
transition :parked => :idling, :on => :ignite, :if => :moving? transition :parked => :idling, :on => :ignite, :unless => :stopped? transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on? transition :from => :parked, :to => :idling, :on => ignite, :if => :moving? transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
Order of operations¶ ↑
Transitions are evaluated in the order in which they’re defined. As a result, if more than one transition applies to a given object, then the first transition that matches will be performed.
# File lib/state_machine/machine.rb 1460 def transition(options) 1461 raise ArgumentError, 'Must specify :on event' unless options[:on] 1462 1463 branches = [] 1464 options = options.dup 1465 event(*Array(options.delete(:on))) { branches << transition(options) } 1466 1467 branches.length == 1 ? branches.first : branches 1468 end
Runs a transaction, rolling back any changes if the yielded block fails.
This is only applicable to integrations that involve databases. By default, this will not run any transactions since the changes aren’t taking place within the context of a database.
# File lib/state_machine/machine.rb 1900 def within_transaction(object) 1901 if use_transactions 1902 transaction(object) { yield } 1903 else 1904 yield 1905 end 1906 end
Sets a new value in the given object’s attribute.
For example,
class Vehicle state_machine :initial => :parked do ... end end vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked"> Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling' Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park' vehicle.state # => "idling" vehicle.event # => "park"
# File lib/state_machine/machine.rb 1111 def write(object, attribute, value, ivar = false) 1112 attribute = self.attribute(attribute) 1113 ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value) 1114 end
Protected Instance Methods
The method to hook into for triggering transitions when invoked. By default, this is the action configured for the machine.
Since the default hook technique relies on module inheritance, the action must be defined in an ancestor of the owner classs in order for it to be the action hook.
# File lib/state_machine/machine.rb 2108 def action_hook 2109 action && owner_class_ancestor_has_method?(:instance, action) ? action : nil 2110 end
Adds a new transition callback of the given type.
# File lib/state_machine/machine.rb 2247 def add_callback(type, options, &block) 2248 callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block) 2249 add_states(callback.known_states) 2250 callback 2251 end
Tracks the given set of events in the list of all known events for this machine
# File lib/state_machine/machine.rb 2276 def add_events(new_events) 2277 new_events.map do |new_event| 2278 # Check for other states that use a different class type for their name. 2279 # This typically prevents string / symbol misuse. 2280 if conflict = events.detect {|event| event.name.class != new_event.class} 2281 raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent" 2282 end 2283 2284 unless event = events[new_event] 2285 events << event = Event.new(self, new_event) 2286 end 2287 2288 event 2289 end 2290 end
Updates this machine based on the configuration of other machines in the owner class that share the same target attribute.
# File lib/state_machine/machine.rb 2239 def add_sibling_machine_configs 2240 # Add existing states 2241 sibling_machines.each do |machine| 2242 machine.states.each {|state| states << state unless states[state.name]} 2243 end 2244 end
Tracks the given set of states in the list of all known states for this machine
# File lib/state_machine/machine.rb 2255 def add_states(new_states) 2256 new_states.map do |new_state| 2257 # Check for other states that use a different class type for their name. 2258 # This typically prevents string / symbol misuse. 2259 if new_state && conflict = states.detect {|state| state.name && state.name.class != new_state.class} 2260 raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent" 2261 end 2262 2263 unless state = states[new_state] 2264 states << state = State.new(self, new_state) 2265 2266 # Copy states over to sibling machines 2267 sibling_machines.each {|machine| machine.states << state} 2268 end 2269 2270 state 2271 end 2272 end
Runs additional initialization hooks. By default, this is a no-op.
# File lib/state_machine/machine.rb 1951 def after_initialize 1952 end
Creates a scope for finding objects with a particular value or values for the attribute.
By default, this is a no-op.
# File lib/state_machine/machine.rb 2210 def create_with_scope(name) 2211 end
Creates a scope for finding objects without a particular value or values for the attribute.
By default, this is a no-op.
# File lib/state_machine/machine.rb 2217 def create_without_scope(name) 2218 end
Adds helper methods for automatically firing events when an action is invoked
# File lib/state_machine/machine.rb 2077 def define_action_helpers 2078 if action_hook 2079 @action_hook_defined = true 2080 define_action_hook 2081 end 2082 end
Determines whether action helpers should be defined for this machine. This is only true if there is an action configured and no other machines have process this same configuration already.
# File lib/state_machine/machine.rb 2071 def define_action_helpers? 2072 action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self} 2073 end
Hooks directly into actions by defining the same method in an included module. As a result, when the action gets invoked, any state events defined for the object will get run. Method visibility is preserved.
# File lib/state_machine/machine.rb 2087 def define_action_hook 2088 action_hook = self.action_hook 2089 action = self.action 2090 private_action_hook = owner_class.private_method_defined?(action_hook) 2091 2092 # Only define helper if it hasn't 2093 define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 2094 def #{action_hook}(*) 2095 self.class.state_machines.transitions(self, #{action.inspect}).perform { super } 2096 end 2097 2098 private #{action_hook.inspect} if #{private_action_hook} 2099 end_eval 2100 end
Adds helper methods for getting information about this state machine’s events
# File lib/state_machine/machine.rb 2019 def define_event_helpers 2020 # Gets the events that are allowed to fire on the current object 2021 define_helper(:instance, attribute(:events)) do |machine, object, *args| 2022 machine.events.valid_for(object, *args).map {|event| event.name} 2023 end 2024 2025 # Gets the next possible transitions that can be run on the current 2026 # object 2027 define_helper(:instance, attribute(:transitions)) do |machine, object, *args| 2028 machine.events.transitions_for(object, *args) 2029 end 2030 2031 # Fire an arbitrary event for this machine 2032 define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args| 2033 machine.events.fetch(event).fire(object, *args) 2034 end 2035 2036 # Add helpers for tracking the event / transition to invoke when the 2037 # action is called 2038 if action 2039 event_attribute = attribute(:event) 2040 define_helper(:instance, event_attribute) do |machine, object| 2041 # Interpret non-blank events as present 2042 event = machine.read(object, :event, true) 2043 event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil 2044 end 2045 2046 # A roundabout way of writing the attribute is used here so that 2047 # integrations can hook into this modification 2048 define_helper(:instance, "#{event_attribute}=") do |machine, object, value| 2049 machine.write(object, :event, value, true) 2050 end 2051 2052 event_transition_attribute = attribute(:event_transition) 2053 define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 2054 protected; attr_accessor #{event_transition_attribute.inspect} 2055 end_eval 2056 end 2057 end
Adds helper methods for interacting with the state machine, including for states, events, and transitions
# File lib/state_machine/machine.rb 1978 def define_helpers 1979 define_state_accessor 1980 define_state_predicate 1981 define_event_helpers 1982 define_path_helpers 1983 define_action_helpers if define_action_helpers? 1984 define_name_helpers 1985 end
Adds helper methods for accessing naming information about states and events on the owner class
# File lib/state_machine/machine.rb 2143 def define_name_helpers 2144 # Gets the humanized version of a state 2145 define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state| 2146 machine.states.fetch(state).human_name(klass) 2147 end 2148 2149 # Gets the humanized version of an event 2150 define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event| 2151 machine.events.fetch(event).human_name(klass) 2152 end 2153 2154 # Gets the state name for the current value 2155 define_helper(:instance, attribute(:name)) do |machine, object| 2156 machine.states.match!(object).name 2157 end 2158 2159 # Gets the human state name for the current value 2160 define_helper(:instance, "human_#{attribute(:name)}") do |machine, object| 2161 machine.states.match!(object).human_name(object.class) 2162 end 2163 end
Adds helper methods for getting information about this state machine’s available transition paths
# File lib/state_machine/machine.rb 2061 def define_path_helpers 2062 # Gets the paths of transitions available to the current object 2063 define_helper(:instance, attribute(:paths)) do |machine, object, *args| 2064 machine.paths_for(object, *args) 2065 end 2066 end
Defines the with/without scope helpers for this attribute. Both the singular and plural versions of the attribute are defined for each scope helper. A custom plural can be specified if it cannot be automatically determined by either calling pluralize
on the attribute name or adding an “s” to the end of the name.
# File lib/state_machine/machine.rb 2170 def define_scopes(custom_plural = nil) 2171 plural = custom_plural || pluralize(name) 2172 2173 [:with, :without].each do |kind| 2174 [name, plural].map {|s| s.to_s}.uniq.each do |suffix| 2175 method = "#{kind}_#{suffix}" 2176 2177 if scope = send("create_#{kind}_scope", method) 2178 # Converts state names to their corresponding values so that they 2179 # can be looked up properly 2180 define_helper(:class, method) do |machine, klass, *states| 2181 run_scope(scope, machine, klass, states) 2182 end 2183 end 2184 end 2185 end 2186 end
Adds reader/writer methods for accessing the state attribute
# File lib/state_machine/machine.rb 1999 def define_state_accessor 2000 attribute = self.attribute 2001 2002 @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute) 2003 @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=") 2004 end
Defines the initial values for state machine attributes. Static values are set prior to the original initialize method and dynamic values are set after the initialize method in case it is dependent on it.
# File lib/state_machine/machine.rb 1990 def define_state_initializer 1991 define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 1992 def initialize(*) 1993 self.class.state_machines.initialize_states(self) { super } 1994 end 1995 end_eval 1996 end
Adds predicate method to the owner class for determining the name of the current state
# File lib/state_machine/machine.rb 2008 def define_state_predicate 2009 call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?") 2010 define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 2011 def #{name}?(*args) 2012 args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args) 2013 end 2014 end_eval 2015 end
Determines if the machine’s attribute needs to be initialized. This will only be true if the machine’s attribute is blank.
# File lib/state_machine/machine.rb 1971 def initialize_state?(object) 1972 value = read(object, :state) 1973 (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value] 1974 end
Determines whether there’s already a helper method defined within the given scope. This is true only if one of the owner’s ancestors defines the method and is further along in the ancestor chain than this machine’s helper module.
# File lib/state_machine/machine.rb 2116 def owner_class_ancestor_has_method?(scope, method) 2117 superclasses = owner_class.ancestors[1..-1].select {|ancestor| ancestor.is_a?(Class)} 2118 2119 if scope == :class 2120 # Use singleton classes 2121 current = (class << owner_class; self; end) 2122 superclass = superclasses.first 2123 else 2124 current = owner_class 2125 superclass = owner_class.superclass 2126 end 2127 2128 # Generate the list of modules that *only* occur in the owner class, but 2129 # were included *prior* to the helper modules, in addition to the 2130 # superclasses 2131 ancestors = current.ancestors - superclass.ancestors + superclasses 2132 ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse 2133 2134 # Search for for the first ancestor that defined this method 2135 ancestors.detect do |ancestor| 2136 ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(Class) 2137 ancestor.method_defined?(method) || ancestor.private_method_defined?(method) 2138 end 2139 end
Gets the initial attribute value defined by the owner class (outside of the machine’s definition). By default, this is always nil.
# File lib/state_machine/machine.rb 2227 def owner_class_attribute_default 2228 nil 2229 end
Checks whether the given state matches the attribute default specified by the owner class
# File lib/state_machine/machine.rb 2233 def owner_class_attribute_default_matches?(state) 2234 state.matches?(owner_class_attribute_default) 2235 end
Pluralizes the given word using pluralize
(if available) or simply adding an “s” to the end of the word
# File lib/state_machine/machine.rb 2197 def pluralize(word) 2198 word = word.to_s 2199 if word.respond_to?(:pluralize) 2200 word.pluralize 2201 else 2202 "#{name}s" 2203 end 2204 end
Generates the results for the given scope based on one or more states to filter by
# File lib/state_machine/machine.rb 2190 def run_scope(scope, machine, klass, states) 2191 values = states.flatten.map {|state| machine.states.fetch(state).value} 2192 scope.call(klass, values) 2193 end
Looks up other machines that have been defined in the owner class and are targeting the same attribute as this machine. When accessing sibling machines, they will be automatically copied for the current class if they haven’t been already. This ensures that any configuration changes made to the sibling machines only affect this class and not any base class that may have originally defined the machine.
# File lib/state_machine/machine.rb 1960 def sibling_machines 1961 owner_class.state_machines.inject([]) do |machines, (name, machine)| 1962 if machine.attribute == attribute && machine != self 1963 machines << (owner_class.state_machine(name) {}) 1964 end 1965 machines 1966 end 1967 end
Always yields
# File lib/state_machine/machine.rb 2221 def transaction(object) 2222 yield 2223 end