event
Defines one or more states for the machine and the transitions that can be performed when those events are run
- The main interface to transitioning states for an object
- Aliased as
on
- Attributes are generated for every event to allow transitions to be performed automatically when
:save
is called
vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
vehicle.state_event # => nil
vehicle.state_event = 'invalid'
vehicle.valid? # => false
vehicle.errors.full_messages # => ["State event is invalid"]
vehicle.state_event = 'ignite'
vehicle.valid? # => true
vehicle.save # => true
vehicle.state # => "idling"
vehicle.state_event # => nil
This can also be done on a mass-assignment basis:
vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling"> vehicle.state # => "idling"
This technique is always used for transitioning states when save
is left as default action
for the machine.
Security implications
- Public event attributes mean events can be fired whenever mass-assignment is used
- To prevent tampering with events through URLs / forms, the attribute should be protected:
class Vehicle < ActiveRecord::Base
attr_protected :state_event
# attr_accessible ... # Alternative technique
state_machine do
...
end
end
- To have only some events fire via mass-assignment, you can build two state machines (one public and one protected):
class Vehicle < ActiveRecord::Base
attr_protected :state_event # Prevent access to events in the first machine
state_machine do
# Define private events here
end
# Public machine targets the same state as the private machine
state_machine :public_state, :attribute => :state do
# Define public events here
end
end
Defining
Options
:human_name
- Human-readable version of this event's name
- Default: Stringifies the name and underscores spaces
Block
Each
event
requires a block that defines all transitions possible as a result of the event: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
The block is executed within the context of the actual event object
Referencing any class methods on the model requires referencing the class itself:
class Vehicle def self.safe_states [:parked, :idling, :stalled] end state_machine do event :park do transition Vehicle.safe_states => :parked end end end
More @
transition
Errors (AR)
If an event fails to fire because there are no matching transitions for the current record, a validation error is added to the record's state attribute to help in determining why it failed and for reporting via the UI
vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling"> vehicle.ignite # => false vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
If an event fails to fire because of a validation error on the record and not because a matching transition was not available, no error messages will be added to the state attribute - if you're using the bang version of the event, then the failure reason (such as the current validation errors) will be included in the exception that gets raised when the event fails. For example, assuming there's a validation on a field called
name
on the class:vehicle = Vehicle.new vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)