State Machine with Rails. Basics by releu
State Machine comes to the rescue when models in your application need statuses and events.
There is a #state_machine gem for #ruby.
I recommend you to spend some time studying basics before you start using it with #rails.
What is State Machine?
Imaging that in your app you have a billing system with Payment model. In each moment of time payment has a state – pending or paid. But what will happen when state is changed from pending to paid and could it be switched backwards? How to implement safe methods which can't be called twice? As you can see there are a bunch of questions which should be answered before we start coding and acquire real payments.
State Machine cares pretty much about everything we asked. Let's dive in!
Vocabulary
state is self explanatory. Usually it is a column in a database storing one of the predefined states.
event is a method which changes state of our model and does other stuff you need. Event is an abstraction over method - state of an object could be changed only by triggering an event.
Example application
Imagine that we are creating application with articles (blog, news portal, anything related to publishing documents). So we will define model Article with state, events and play with it.
class Article < ActiveRecord::Base
end
Storing state's value
Let's begin by creating a column in database's table. By default its name is state.
State Machine also supports #mongoid, #activemodel and others.
ActiveRecord::Migration.create_table :articles do |t|
t.integer :state
t.index :state
end
State machine logic
Initialization
To add #state_machine logic to a model is pretty simple:
class Article < ActiveRecord::Base
state_machine initial: :pending do
state :pending, value: 0
state :published, value: 1
end
end
value's value will be stored in database. So an article with state 1 should be considered as published.
You can also use strings, it does not matter.
As ActiveRecord state_machine defines a bunch of methods:
article = Article.create
article.pending? # => true
article.published? # => false
article.state_name # => :pending
and scopes with_state and with_states:
Article.with_state(:published) # => SELECT "articles".* FROM "articles" WHERE ("articles"."state" IN (1))
Changing states. Events.
OK, now we need some events - methods, which will change our state. Take a look at this example:
class Article < ActiveRecord::Base
state_machine initial: :pending do
state :pending, value: 0
state :published, value: 1
event :publish do
transition :pending => :published
end
event :hide do
transition :published => :pending
end
end
end
As you can see we have some kind of a DSL here and this code will define some event methods:
article = Article.create
article.can_hide? # => false
article.hide # => false
article.hide! # Exception (Cannot transition state via :hide from :pending)
article.publish # => true
article.hide # => true
Callbacks
state_machine produces before, after and around callbacks for transitions.
This is not callbacks like in ActiveRecord::Base - they are not running in one database transaction.
That's important thing to know:
If we will have an exception in
aftercallback it doesn't rollback database's changes. It is very critical when you do a billing system.
If we need to produce safe-data methods we need to create a transaction manually. It is very easy to do with around_transition callback.
class Article < ActiveRecord::Base
state_machine initial: :pending do
state :pending, value: 0
state :published, value: 1
event :publish do
transition :pending => :published
end
around_transition on: :publish do |article, transition, block|
article.transaction do
block.call # block is an event's proc. we need to perform it
article.pay_author!
end
end
end
def pay_author!
raise "not yet implemented"
end
end
Now if we try to publish article we will fail with exception and article will not be published.
Validations
You can extend your states logic with validations.
class Article < ActiveRecord::Base
state_machine initial: :pending do
state :pending, value: 0
state :published, value: 1 do
validates :body, length: { more_than_or_equal_to: 160 }
end
event :publish do
transition :pending => :published
end
end
end
article = Article.create
article.can_publish? # => true
article.publish # => false
article.errors # => "Body's length is less than 160"
Further reading
State Machine is awesome gem. It also has internalization support, other callbacks, state namespaces and much more useful things.
You can found that information on github https://github.com/pluginaweek/state_machine
Comments
psychocandy commented 3 months ago
Pretty awesome post, thanks!