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 after callback 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