Add this line to your application's Gemfile:
gem 'ar_state_machine'And then execute:
$ bundle
Or install it yourself as:
$ gem install ar_state_machine
config/initializers/ar_state_machine.rb
ARStateMachine.configure do |config|
config.system_id = system_id
config.should_log_state_change = true
endSimply define the state machine at the top of your model in the format below:
self.state_machine({
first_state: [:second_state, :third_state],
:second_state: :third_state,
third_state: []
})Convention - states should usually be past tense (completed, imported, etc) or present participle for ongoing actions (building, returning).
This example indicates that first_state can transition to second_state or third_state but second_state can only transition to third_state which is then a dead end.
Then add any before, after and after_commit callbacks to fire on state changes.
These use the active record callback chain:
Note: if you have multiple of the same transition type they execute in the order they are put in your model file.
before_transition_to,before_transition_fromrun onbefore_update- returning false or raising an exception in here will halt the transaction stop running transitions
after_transition_to,after_transition_fromrun onafter_updateafter_commit_transition_to,after_commit_transition_fromrun onafter_commit, on: :update
Here are some rules of thumb to follow:
before_transition- only if you need to be able to halt the transaction (validation generally) or you need to set a value on the model being transitioned. You can also use this to halt AR state machine callbacks but still allow the transaction to save (payment failure is a good example, we still want to save the failure).after_transition- use if you need to update child records or do something that uses AR to update the database. If you need to halt the transaction you'll need to do it with an exception as returning false does nothing here.after_commit_transition- most of the time, background job queue, cache clearing and similar tasks, do not use this if what you're doing will result in another transaction;update_columns,update_all,delete,delete_allare okay but nothing that results in an AR save/create/update/state transition.
before_transition_to :second_state do |from, to|
puts "doing a before transition from #{self.from} to #{self.state}"
end
before_transition_to :second_state do |from, to|
puts "doing another before transition #{self.state}"
end
after_transition_to :second_state do |from, to|
puts "doing an after transition #{self.state}"
end
after_commit_transition_to :second_state do |from, to|
puts "doing an after commit transition #{self.state}"
end
before_transition_from :second_state do |from, to|
puts "doing before transition from #{self.state}"
end
after_transition_from :second_state do |from, to|
puts "doing an after transition from #{self.state}"
end
after_commit_transition_from :second_state do |from, to|
puts "doing an after commit transition from #{self.state}"
endthis also supports arrays
after_transition_to [:second_state, :first_state] do |from, to|
puts "doing an after transition #{self.state}"
endand reusable functions
after_transition_to :second_state, :function_name
def function_name(from, to)
puts "doing an after transition #{self.state}"
endIt provides the instance methods for each state_name:
model_instance.is_state_name? => true/false
model_instance.is_not_state_name? => true/false
model_instance.can_make_state_name? => true/false
model_instance.make_state_name => # transitions to new state if it can, otherwise adds rails validation error messages
model_instance.make_state_name! => # transitions to new state if it can, otherwise throws exception and adds rails validation error messagesClass level constants for each state
Model::STATE_NAME => 'state_name'
And the class scopes for each state_name:
ModelName.state_name
ModelName.not_state_nameWhich is chainable with any other scopes you might have EX:
Order.completed.by_date(Date.today)
Order.not_completed.by_date(Date.today)You can add timestamps to your models that will automatically get filled in. In the format #{state_name}_at. IE: a completed_at timestamp will get populated with the timestamp that the object moves to a completed state.
Once these timestamps are in place; helper methods are available to determine if an instance has ever been in a state given. IE: if you have Order and field completed_at; then you also have order_model.has_been_made_completed? and the inverse order_model.has_not_been_made_completed?
All transitions are logged in the state_changes table.
After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ar_state_machine.