Related tags
Ruby on Rails Finite State Machine Plugin: Workflow
I've been using the acts_as_state_machine plugin for Ruby on Rails for ages, but I thought it was time I look for alternatives, to see if there has been any development in the Finite State Machine scene. I also started getting worried that even though acts_as_state_machine still works, development on it has pretty much been discontinued. Good or bad, you can't really say.
Lucky me. I found one, and a pretty great one.
Introducing Workflow, your alternate Finite State Machine for Ruby on Rails.
acts_as_state_machine vs Workflow
At its core acts_as_state_machine and Workflow function exactly the same way. Feature wise Workflow wins out as it makes it easier to manage the callbacks. Workflow is more up to date, and I personally prefer its syntax a lot better than acts_as_state_machine.
Workflow's syntax looks a lot more like a Domain Specific Language.
Installing Workflow
Installing Workflow as a Ruby on Rails Plugin
Workflow can be installed either via Ruby Gems or a Ruby on Rails plugin.
As a Ruby on Rails plugin, go to the root folder of your Rails application and execute:
./script/plugin install \
git://github.com/geekq/workflow.git
Note: You may need Git installed
Installing Workflow via Ruby Gems
Likewise as a Ruby Gem, Workflow can be easily installed. As your root user execute:
gem install workflow
We will need to update your config/environments.rb, and in the Rails::Initializer.run block we will need to add in config.gem 'workflow'.
Your config/environments.rb may look like so:
Rails::Initializer.run do |config| ... config.gem 'workflow' .. end
Using Workflow
Lets have a look at a sample code, featuring a Person.
class Person < ActiveRecord::Base
include Workflow
workflow do
state :sleeping do
event :shower, :transitions_to => :showering
event :work, :transitions_to => :working
end
state :showering do
event :sleep, :transitions_to => :sleeping
event :work, :transitions_to => :working
event :date, :transitions_to => :dating do |romantic_interest|
successful = self.flirt(romantic_interest)
halt unless successful
end
end
state :working do
event :shower, :transitions_to => :showering
end
state :dating do
event :shower, :transitions_to => :showering
event :sleep, :transitions_to => :showering
event :work, :transitions_to => :showering
end
end
end
Note: Workflow makes the assumption that the state of your model is saved in a field called workflow_state.
In line 2 we have to specifically include include Workflow into our model. From there we begin describing the workflow by opening up a workflow block. Inside we define what states the model is going to contain, and what events can be fired to transition the model from one state to another.
The initial state of a model is the first state defined, in this case it is the sleeping state. States are defined in a state block as seen on lines 4, 9, 17, and 23. We have four states here: sleeping, working, showering, dating. Possible events a state may fire are contained within the state block as an event method. These events describe which state they would transition to if they were fired.
Methods are created every time you define a state or event. The method created when you define is a state tests wether or not the state is within a particular state, ie person.sleeping?. The methods created when you define an event is the event itself, ie person.sleep!.
There is also an additional method current_state, where you can investigate the current state of the object.
Given our example model above, these methods were created for our model:
- sleeping?
- showering?
- working?
- dating?
- sleep!
- work!
- shower!
- date!
- current_state
Events
Events help you to transition from one state to another. So suppose your person is sleeping, and you want him to shower, well we'll just call shower!.
person.current_state # "sleeping" person.shower! person.current_state # "showering"
Note: When firing an event, Workflow calls ActiveRecord::Base.update_attribute. This means that only your record's state will be saved in the database, any other changes to the record are not. You still have to call ActiveRecord::Base.save. It also doesn't call any validations. Validations will be handled by your guards, which will be discussed later. Calling an event from a new unsaved record, will save the record into the database.
Event Arguments
You can even have your events accept arguments, this can be see on line 12, in the :date block.
state :showering do
event :sleep, :transitions_to => :sleeping
event :work, :transitions_to => :working
event :date, :transitions_to => :dating do |romantic_interest|
successful = self.flirt(romantic_interest)
halt unless successful
end
end
Note: The scope of the block is within the instance. Treat it as any other method. self refers to the instance itself. flirt is a method defined in the instance. You would have to define the flirt method yourself.
Lets have a look at an example:
person.current_state # showering romantic_interest = Person.new person.date! romantic_interest person.current_state # dating
With events being able to accept arguments, this gives us a wider range of flexibility on handling and organizing the flow of your model. But they can get more powerful with callbacks.
Callbacks
A state also includes two callbacks. One for entering the state, and one for exiting the state.
Like the event blocks, the callback blocks also accept arguments.
You can define a state's callback like so:
state :sleeping do
event :shower, :transitions_to => :showering
event :work, :transitions_to => :working
on_entry do |prior_state, triggering_event, *event_args|
# code
end
on_exit do |new_state, triggering_event, *event_args|
# code
end
end
- The
on_entrycallback is called when a person enters the sleeping state. - The
on_exitcallback is called when a person exits the sleeping state.
You can also define your callbacks outside of the state block and in your model itself as methods:
state :sleeping do event :shower, :transitions_to => :showering event :work, :transitions_to => :working end def on_sleeping_entry(prior_state, triggering_event, *event_args) # code end def on_sleeping_exit(new_state, triggering_event, *event_args) # code end
Note: Notice the naming convention of the method name? To use this method, the callback is called on_ + state name + _entry, and on_ + state name + _entry.
The arguments for the block and method are optional. Therefore for clarity, you may just enter:
state :sleeping do
event :shower, :transitions_to => :showering
event :work, :transitions_to => :working
on_entry do
# code
end
def on_sleeping_exit(new_state, triggering_event, *event_args)
# code
end
Global Callback
This gets tedious if you want to monitor the transitions of every state change. Don't worry, Workflow can handle that.
Workflow comes with an on_transition callback which is placed inside the workflow block:
workflow do
on_transition do |from, to, triggering_event, *event_args|
puts "from #{from} to #{to} via #{triggering_event}"
end
end
Callback Order
With all these callbacks, it gets a bit confusing. Here are Workflow's callbacks in context of ActiveRecord's callbacks:
- Workflow's in event
- Workflow's on_transition
- Workflow's on_exit
- ActiveRecord::Base.before_save
- ActiveRecord::Base.save
- ActiveRecord::Base.after_save
- Workflow's on_entry
Guarding States: halt!
Using Workflow your model's validations do not work when an event is fired. In fact, only the model's workflow_state is saved when an event is fired, and not any other data.
In Workflow we can setup a guard, or a halt. This will prevent the transition from ever occurring, and provide us with the safety of having our models validated.
Guards can only be used inside the event block itself. For example:
state :showering do
event :date, :transitions_to => :dating do |romantic_interest|
successful = self.flirt(romantic_interest)
halt unless successful
end
end
Assuming that the flirting with the romantic interest was unsuccessful, the person would still be in the the showering state. All thanks to the halt on line 15.
This would run as follows:
person.current_state # showering romantic_interest = Person.new person.date! romantic_interest person.current_state # showering
Instead of just a halt, you can make it more destructive by calling halt! (with an exclamation mark) instead, which will throw the Workflow::TransitionHalted Exception instead.
Conclusion
As you can see Workflow is quite feature rich, and provides more flexibility than acts_as_state_machine. I would recommend using this over acts_as_state_machine any day. So why don't you experiment with it, and have some fun?
Finder for Workflow States
Now a days for all my Finite State Machine needs on Ruby on Rails I use geekq's workflow. I find it much better to use than the previous popular Finite State Machine plugin on Rails, acts_as_state_machine.
named_scope
I need to locate all the models that are of a certain state, and thanks to named_scope. This can be done quite easily. Except when we have a lot of states I'd rather not write a named_scope for each individual state. For example:
class Order < ActiveRecord::Base
named_scope :completed, :conditions => { :workflow_state => 'completed' }
end
It would be encumbersome to write this multiple times. I'd rather have a named_scope created for each state. This is my quick hack.
class Order < ActiveRecord::Base
include Workflow
workflow do
state :new do
...
end
...
end
(self.workflow_spec.states.keys - [:new]).each do |state|
named_scope state, :conditions => { :workflow_state => state.to_s }
end
end
After the workflow code you place you write:
(self.workflow_spec.states.keys - [:new]).each do |state|
named_scope state, :conditions => { :workflow_state => state.to_s }
end
This iterates through each of our workflow states, and creates a named_scope for each one based on the name of the state. So you can easily do Order.completed and find all completed Orders.
Note: I had to remove :new (on line 12) from the array of keys as it would cause problems with my code. This is caused from overwriting the new method. You can solve this by probably putting a prefix for the named_scope name.
I know it can be extracted to use alias_method_chain and modify the Workflow sourcecode, but I enjoy my hack.
Ruby on Rails Finite State Machine Plugin: acts_as_state_machine
Update: Workflow is an alternative Finite State Machine Plugin for Ruby on Rails is available. I recommend you go give Workflow a try.
A finite state machine is a model of behavior with a finite number of states, interconnected via transitions and events.
In this guide, I will introduce you to a Ruby on Rails plugin to easily recreate a Finite State Machine with your models. The plugin we will be using is the acts_as_state_machine, which as you can see has very sparse documentation. Google does pull up some results, but not good enough, at least for beginners.
Why would you use a Finite State Machine?
For starters, if your model has a finite number of various states, and you want an easy way for callbacks to be done. Callbacks can be used to notify, validate, increment, anything, when your model changes state.
Installing acts_as_state_machine
Go to the root folder of your Rails application and execute:
./script/plugin install \ http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/
Warning: Split over two lines as it was really long.
Note: Don't forget if your project is under subversion, you can use the -x flag to externally link to the plugin
Using acts_as_state_machine
class Person < ActiveRecord::Base
acts_as_state_machine :initial => :sleeping
state :sleeping
state :showering
state :working
state :dating
event :shower do
transitions :from => :sleeping, :to => :showering
transitions :from => :working, :to => :showering
transitions :from => :dating, :to => :showering
end
event :work do
transitions :from => :showering, :to => :working
# Going to work before showering? Stinky.
transitions :from => :sleeping, :to => :working
end
event :date do
transitions :from => :showering, :to => :dating
end
event :sleep do
transitions :from => :showering, :to => :sleeping
transitions :from => :working, :to => :sleeping
transitions :from => :dating, :to => :sleeping
end
end
Note: The plugin makes an assumption that the state of your model is saved in field called state. This can be replaced by adding the additional option :column => 'field'.
Warning: If you are using a model that stores addresses, be weary of a field called "state". You can spend hours wondering why things aren't working like they should be.
Note: Personal Preference: I like to describe the states my models are in with as an adjective, and the event as a verb.
Notice how in line 2 we explicitly state the initial state of the model. In lines 3 to 6, we indicate the various states the Person may be in.
Note: There is a peculiar behavior when creating objects via new, in that the model's state is not specified. It will only be specified when saving the new record. One solution is to specify the default state from within the migration. The other solution then is to call create.
Example:
person = Person.new person.state # nil person.save # true person.state # "sleeping" person = Person.create person.state # "sleeping" person.sleeping? # true person.rotting? # false person = Person.new person.state = "rotting" # "rotting" person.rotting? # true person.sleeping? # false
Note: If you didn't notice the trend, the method to test if the model is in the state, is to append the state with a question mark: "state?"
The events you specified also creates instance methods, to transition the model from one state to another.
The following instance methods were created:
person.shower! person.work! person.date! person.sleep!
Note: The instance methods created follow the pattern "event!"
Events
Note: By calling any event, you also call ActiveRecord::Base.save. For when it fails, it only returns false. You can guard yourself by calling valid? and save!
Events help you to transition from one state to another. So suppose your person is sleeping, and you want him to shower, well we'll just call shower!.
person.state # "sleeping" person.shower! person.state # "showering"
Events can help your organize the flow of your model. But they can get more powerful with callbacks.
Callbacks
The state also comes with a few callbacks that can be used.
state :sleeping,
:enter => :get_into_bed,
:after => Proc.new {|model| model.whack_alarm_clock },
:exit => :make_up_bed
Callbacks are called when the model is transitioning into the specified state.
Note:
- Callbacks can be either a
symbolor aProc. If used as a symbol, the instance method of the model will be called - The callbacks act differently if the model is a new record and hasn't been saved, versus an already saved model.
When put into consideration with ActiveRecord's callbacks, a new record's callback would look like this:
- ActiveRecord::Base.before_save
- ActiveRecord::Base.save
- acts_as_state_machine :enter sleeping
- acts_as_state_machine :after sleeping
- ActiveRecord::Base.after_save
When the model is no longer a new record, the callbacks execute as follows, if I had called the shower! method.
- acts_as_state_machine :enter showering
- ActiveRecord::Base.before_save
- ActiveRecord::Base.save
- ActiveRecord::Base.after_save
- acts_as_state_machine :after showering
- acts_as_state_machine :exit sleeping
Guarding States
But how about if you want some sort of validation for a transition. You know, just to ensure data integrity.
event :work do
transitions :from => :showering, :to => :working
# Going to work before showering? Stinky.
transitions :from => :sleeping, :to => :working, :guard => Proc.new {|o| o.clean? }
end
The transition can be guarded by specifying a :guard option, with either a symbol or Proc (similar to the Callbacks). The method or Proc has to return true to proceed with the transition, else it will fail silently.
Conclusion
Well thats the basics, and as much as acts_as_state_machine does.
Any tips, tricks and or pointers? Leave them as comments.
Or are there any other interesting plugins you'd like me to explore?
Update: Workflow is an alternative Finite State Machine Plugin for Ruby on Rails is available. I recommend you go give Workflow a try.
