Skip to content

Commit

Permalink
Ensure initial state callbacks are invoked in the proper order when a…
Browse files Browse the repository at this point in the history
…n event is fired on a new record
  • Loading branch information
obrie committed Jul 10, 2008
1 parent 4dabe2c commit af5d9ad
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rdoc
@@ -1,5 +1,6 @@
== master

* Ensure initial state callbacks are invoked in the proper order when an event is fired on a new record
* Add before_loopback and after_loopback hooks

== 0.2.1 / 2008-07-05
Expand Down
11 changes: 8 additions & 3 deletions lib/state_machine.rb
Expand Up @@ -118,9 +118,14 @@ def initialize_with_state_machine(attributes = nil)

# Records the transition for the record going into its initial state
def run_initial_state_machine_actions
self.class.state_machines.each do |attribute, machine|
callback = "after_enter_#{attribute}_#{self[attribute]}"
run_callbacks(callback) if self[attribute] && self.class.respond_to?(callback)
# Make sure that these initial actions are only invoked once
unless @processed_initial_state_machin_actions
@processed_initial_state_machin_actions = true

self.class.state_machines.each do |attribute, machine|
callback = "after_enter_#{attribute}_#{self[attribute]}"
run_callbacks(callback) if self[attribute] && self.class.respond_to?(callback)
end
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/state_machine/event.rb
Expand Up @@ -140,6 +140,12 @@ def add_event_callbacks
# Otherwise, the default +perform+ will be invoked.
def try_transition(transition, bang, record, *args)
if transition.can_perform_on?(record)
# If the record hasn't been saved yet, then make sure we run any
# initial actions for the state it's currently in
record.run_initial_state_machine_actions if record.new_record?

# Now that the state machine has been initialized properly, proceed
# normally to the callback chain
return false if invoke_event_callbacks(:before, record, *args) == false
result = bang ? transition.perform!(record, *args) : transition.perform(record, *args)
invoke_event_callbacks(:after, record, *args)
Expand Down
14 changes: 14 additions & 0 deletions lib/state_machine/machine.rb
Expand Up @@ -30,6 +30,20 @@ module StateMachine
# * (3) after_loopback (to/from state)
# * (4) after (event)
#
# One last *important* note about callbacks is that the after_enter callback
# will be invoked for the initial state when a record is saved (assuming that
# the initial state is set). So if an event is fired on an unsaved record,
# the callback order will be:
#
# * (1) after_enter (initial state)
# * (2) before_exit (from/initial state)
# * (3) before_enter (to state)
# * (4) before (event)
# * (-) update state
# * (5) after_exit (from/initial state)
# * (6) after_enter (to state)
# * (7) after (event)
#
# == Cancelling callbacks
#
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks
Expand Down
5 changes: 5 additions & 0 deletions test/app_root/app/models/vehicle.rb
Expand Up @@ -3,13 +3,18 @@ class Vehicle < ActiveRecord::Base
belongs_to :highway

attr_accessor :force_idle
attr_accessor :callbacks

# Defines the state machine for the state of the vehicle
state_machine :state, :initial => Proc.new {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
before_exit 'parked', :put_on_seatbelt
after_enter 'parked', Proc.new {|vehicle| vehicle.update_attribute(:seatbelt_on, false)}
before_enter 'stalled', :increase_insurance_premium

# Callback tracking for initial state callbacks
after_enter 'parked', Proc.new {|vehicle| (vehicle.callbacks ||= []) << 'before_enter_parked'}
before_enter 'idling', Proc.new {|vehicle| (vehicle.callbacks ||= []) << 'before_enter_idling'}

event :park do
transition :to => 'parked', :from => %w(idling first_gear)
end
Expand Down
15 changes: 15 additions & 0 deletions test/functional/state_machine_test.rb
Expand Up @@ -52,6 +52,21 @@ def test_should_not_allow_crash
def test_should_not_allow_repair
assert !@vehicle.repair
end

def test_should_invoke_initial_state_and_event_callbacks
@vehicle.ignite
assert_equal %w(before_enter_parked before_enter_idling), @vehicle.callbacks
end
end

class VehicleAfterBeingCreatedTest < Test::Unit::TestCase
def setup
@vehicle = create_vehicle
end

def test_should_invoke_initial_state_callbacks
assert_equal %w(before_enter_parked), @vehicle.callbacks
end
end

class VehicleParkedTest < Test::Unit::TestCase
Expand Down

0 comments on commit af5d9ad

Please sign in to comment.