state_machine 0.7: Better integrations, more access

by aaron

Fast and furious. That’s how development’s been on state_machine over the last month (and is also what happens to sequels when you run out of bad ideas). Yesterday, 0.7 was officially added to the state_machine family of releases. With this release comes better integrations, easier access to more state information, and the all new and improved Done Right™. Check out the changelog for a detailed list of changes.

REST and events – Events are auto-fired when a machine’s action is called (or when validations run) based on the value of #{attribute}_event:

purchase = Purchase.create  # => #<Purchase id: 1, state: "pending">
purchase.state_event        # => nil
 
purchase.update_attributes(:state_event => 'authorize')
purchase.state              # => "authorized"
purchase.state_event        # => nil
 
purchase.state_event = 'capture'
purchase.valid?             # => true
purchase.state              # => "captured"
purchase.save               # => true

This means that your create/update controller actions don’t change at all. For an example of the full Rails/Merb vertical slice, see the RESTful examples.

State rollbacks – If an action fails when firing an event, the transition is rolled back:

purchase = Purchase.find(1)
purchase.state              # => "pending"
purchase.amount = nil
purchase.valid?             # => false # validates_presence_of :amount
purchase.authorize          # => false
purchase.state              # => "pending" # Returned "authorized" pre-0.7

ActiveRecord Observers – The ActiveRecord integration kicked it up a notch (bam!) with new Observer hooks:

class PurchaseObserver < ActiveRecord::Observer
  def before_capture_from_pending_to_captured(p, transition); end
  def before_capture_from_pending(p, transition); end
  def before_capture_to_captured(p, transition); end
  def before_capture(p, transition); end
  def before_transition_state_from_pending_to_captured(p, transition); end
  def before_transition_state_to_captured(p, transition); end
  def before_transition_state_from_pending(p, transition); end
  def before_transition_state(p, transition); end
  def before_transition(p, transition); end
end

You can define the same types of hooks for after_* as well.

DataMapper Defaults – The DataMapper integration no longer uses transactions by default and properties are now auto-defined if they don’t already exist:

class Purchase
  include DataMapper::Resource
  property :id, Integer, :serial => true
  # property :state, String # Automatically done by state_machine
 
  state_machine :use_transactions => true do
    ...
  end
end

Available events / transitions – The full list of available events / transitions for a state machine can be accessed via two new helpers:

purchase = Purchase.find(1) # => #<Purchase id: 1, state: "pending">
purchase.state_events       # => [:authorize, :capture]
purchase.state_transitions  # => [#<StateMachine::Transition ...>, ...]
 
purchase.authorize
purchase.state_events       # => [:capture]
purchase.state_transitions  # => [#<StateMachine::Transition ...>]

This becomes particularly useful when presenting these in a view as a dropdown for users to select from.

Backwards Incompatible! Accessing a specific event’s transition has been renamed from next_#{event}_transition to #{event}_transition:

purchase.authorize_transition       # => #<StateMachine::Transition ...>
purchase.next_authorize_transition  # => NoMethodError: undefined method...

Event arguments – Arguments to event methods can be accessed from transition callbacks:

before_transition :on => :authorize do |purchase, transition|
  amount = transition.args.first
  ...
end

after_transition callbacksBackwards incompatible! The result of the action is no longer passed to after_transition callbacks. It can be accessed from the transition instead:

after_transition :on => :authorize do |purchase, transition|
  saved = transition.result
  ...
end

This affects all integrations as well.

Parallel events – For classes that use multiple state machines, you can fire events off in parallel on an object. This means their before/after callbacks get run at the same time and actions only get called once if they’re the same:

purchase = Purchase.find(1)
purchase.state                                    # => "pending"
purchase.shipped_at                               # => nil
purchase.fire_events(:capture, :deliver_shipment) # => true
purchase.state                                    # => "captured"
purchase.shipped_at                               # => Sun Apr 05 ...
 
# Bang method is similar to events
purchase.fire_events!(:capture) # => StateMachine::InvalidTransition: ...

And the rest of the gang…

In addition to the above major changes / additions, there were 6 new minor features and 4 bug fixes in this release.

A special Billy Mays shout-out goes to the folks who helped make this release happen, including:

  • Aaron Gibralter
  • Paweł Kondzior
  • Mikhail Shirkov

Stalk me, but not in a creepy way

As always, be sure to follow the project on GitHub! Happy coding!

Comments

13 Responses to “state_machine 0.7: Better integrations, more access”

  1. rebo on April 5th, 2009 5:43 pm

    Awesome, looks like the RESTFUL implementation will really make things easier for controllers.

    GREAT JOB!

  2. Dr Nic on April 5th, 2009 6:32 pm

    Is anyone working on integrating SM into restful_authentication gem as a supported state machine system?

  3. Russell Jones on April 5th, 2009 8:44 pm

    This is a plugin I use more than any other. Thanks for making it.

    @Dr Nic … Have you looked at the Authlogic gem? It replaced restful_authentication for me a long time ago. Have a look, its stellar!

  4. aaron on April 5th, 2009 10:43 pm

    @Dr Nic – Someone e-mailed me about integrating with restful-authentication a few weeks ago, but I don’t think anyone has taken on the task yet.

    @rebo & @Russell – Thanks for the feedback :)

  5. David Backeus on April 6th, 2009 4:45 am

    From what I see in the examples this makes changing events possible through mass assignment. Does attr_protected :state_event do the trick for protection?

    Might be a good idea to point this out in the documentation since states are things that in my experience usually belong to the non mass assignable side of a model.

  6. aaron on April 6th, 2009 7:19 am

    @David – Great point. Perhaps it should even be protected by default. I’ll look into it. Thanks!

  7. Dr Nic on April 6th, 2009 8:07 am

    @Russell – investigating Ben Johnson’s *Logic gems is on my todo list.

  8. aaron on April 7th, 2009 11:37 pm

    @Dr Nic – Had a little extra time tonight and threw something together: http://github.com/obrie/restful-authentication/commit/132d0d947e5af165208df8cf932181ee0043d4f5

  9. Joe on May 5th, 2009 2:39 pm

    I came here today to feature request what you’ve already done… Saweeeeettt!!!!

  10. aaron on May 5th, 2009 10:06 pm

    @Joe – Great to hear that you’ll get use out of the new features :)

  11. Gavin on May 6th, 2009 10:12 am

    I am just starting to implement a state machine using this plugin, and was trying to graph the states. I have installed graphviz (and libraries) on ubuntu 8.04, and then installed the ruby-graphviz gem, but when I do:

    rake state_machine:draw:rails CLASS=Entry

    it always returns:
    Cannot draw the machine. `gem install ruby-graphviz` and try again.

    Is there any way to get a more comprehensive error message so I can debug the issue? Is this something that’s been seen before?

    Thanks.

  12. Gavin on May 6th, 2009 10:34 am

    Hehe, never mind. It was missing ruby1.8-dev, and couldn’t find mkmf (a standard ruby lib). Stupid packaging bites me again. Sorry.

  13. Andy Watts on August 17th, 2009 2:13 pm

    Excellent plugin. I especially like the new callback design.
    Thanks
    Andy