Module: StateMachine::Integrations::ActiveModel

Extended by:
ClassMethods
Includes:
Base
Included in:
ActiveRecord, MongoMapper, Mongoid
Defined in:
lib/state_machine/integrations/active_model.rb,
lib/state_machine/integrations/active_model/observer.rb,
lib/state_machine/integrations/active_model/versions.rb

Overview

Adds support for integrating state machines with ActiveModel classes.

Examples

If using ActiveModel directly within your class, then any one of the following features need to be included in order for the integration to be detected:

  • ActiveModel::Dirty

  • ActiveModel::Observing

  • ActiveModel::Validations

Below is an example of a simple state machine defined within an ActiveModel class:

class Vehicle
  include ActiveModel::Dirty
  include ActiveModel::Observing
  include ActiveModel::Validations

  attr_accessor :state
  define_attribute_methods [:state]

  state_machine :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end
  end
end

The examples in the sections below will use the above class as a reference.

Actions

By default, no action will be invoked when a state is transitioned. This means that if you want to save changes when transitioning, you must define the action yourself like so:

class Vehicle
  include ActiveModel::Validations
  attr_accessor :state

  state_machine :action => :save do
    ...
  end

  def save
    # Save changes
  end
end

Validation errors

In order to hook in validation support for your model, the ActiveModel::Validations feature must be included. If this is included and an event fails to successfully fire because there are no matching transitions for the object, a validation error is added to the object’s state attribute to help in determining why it failed.

For example,

vehicle = Vehicle.new
vehicle.ignite                # => false
vehicle.errors.full_messages  # => ["State cannot transition via \"ignite\""]

Security implications

Beware that public event attributes mean that events can be fired whenever mass-assignment is being used. If you want to prevent malicious users from tampering with events through URLs / forms, the attribute should be protected like so:

class Vehicle
  include ActiveModel::MassAssignmentSecurity
  attr_accessor :state

  attr_protected :state_event
  # attr_accessible ... # Alternative technique

  state_machine do
    ...
  end
end

If you want to only have some events be able to fire via mass-assignment, you can build two state machines (one public and one protected) like so:

class Vehicle
  include ActiveModel::MassAssignmentSecurity
  attr_accessor :state

  attr_protected :state_event # Prevent access to events in the first machine

  state_machine do
    # Define private events here
  end

  # Public machine targets the same state as the private machine
  state_machine :public_state, :attribute => :state do
    # Define public events here
  end
end

Callbacks

All before/after transition callbacks defined for ActiveModel models behave in the same way that other ActiveSupport callbacks behave. The object involved in the transition is passed in as an argument.

For example,

class Vehicle
  include ActiveModel::Validations
  attr_accessor :state

  state_machine :initial => :parked do
    before_transition any => :idling do |vehicle|
      vehicle.put_on_seatbelt
    end

    before_transition do |vehicle, transition|
      # log message
    end

    event :ignite do
      transition :parked => :idling
    end
  end

  def put_on_seatbelt
    ...
  end
end

Note, also, that the transition can be accessed by simply defining additional arguments in the callback block.

Observers

In order to hook in observer support for your application, the ActiveModel::Observing feature must be included. Because of the way ActiveModel observers are designed, there is less flexibility around the specific transitions that can be hooked in. However, a large number of hooks are supported. For example, if a transition for a object’s state attribute changes the state from parked to idling via the ignite event, the following observer methods are supported:

  • before/after/after_failure_to-_ignite_from_parked_to_idling

  • before/after/after_failure_to-_ignite_from_parked

  • before/after/after_failure_to-_ignite_to_idling

  • before/after/after_failure_to-_ignite

  • before/after/after_failure_to-_transition_state_from_parked_to_idling

  • before/after/after_failure_to-_transition_state_from_parked

  • before/after/after_failure_to-_transition_state_to_idling

  • before/after/after_failure_to-_transition_state

  • before/after/after_failure_to-_transition

The following class shows an example of some of these hooks:

class VehicleObserver < ActiveModel::Observer
  # Callback for :ignite event *before* the transition is performed
  def before_ignite(vehicle, transition)
    # log message
  end

  # Callback for :ignite event *after* the transition has been performed
  def after_ignite(vehicle, transition)
    # put on seatbelt
  end

  # Generic transition callback *before* the transition is performed
  def after_transition(vehicle, transition)
    Audit.log(vehicle, transition)
  end

  def after_failure_to_transition(vehicle, transition)
    Audit.error(vehicle, transition)
  end
end

More flexible transition callbacks can be defined directly within the model as described in StateMachine::Machine#before_transition and StateMachine::Machine#after_transition.

To define a single observer for multiple state machines:

class StateMachineObserver < ActiveModel::Observer
  observe Vehicle, Switch, Project

  def after_transition(object, transition)
    Audit.log(object, transition)
  end
end

Dirty Attribute Tracking

In order to hook in validation support for your model, the ActiveModel::Validations feature must be included. If this is included then state attributes will always be properly marked as changed whether they were a callback or not.

For example,

class Vehicle
  include ActiveModel::Dirty
  attr_accessor :state

  state_machine :initial => :parked do
    event :park do
      transition :parked => :parked
    end
  end
end

vehicle = Vehicle.new
vehicle.changed         # => []
vehicle.park            # => true
vehicle.changed         # => ["state"]

Creating new integrations

If you want to integrate state_machine with an ORM that implements parts or all of the ActiveModel API, the following features must be specified:

  • i18n scope (locale)

  • Machine defaults

For example,

module StateMachine::Integrations::MyORM
  include StateMachine::Integrations::ActiveModel

  @defaults = {:action = > :persist}

  def self.matches?(klass)
    defined?(::MyORM::Base) && klass <= ::MyORM::Base
  end

  def self.extended(base)
    locale = "#{File.dirname(__FILE__)}/my_orm/locale.rb"
    I18n.load_path << locale unless I18n.load_path.include?(locale)
  end

  protected
    def runs_validations_on_action?
      action == :persist
    end

    def i18n_scope
      :myorm
    end
end

If you wish to implement other features, such as attribute initialization with protected attributes, named scopes, or database transactions, you must add these independent of the ActiveModel integration. See the ActiveRecord implementation for examples of these customizations.

Defined Under Namespace

Modules: Observer

Instance Attribute Summary

Attributes included from Base::ClassMethods

#defaults

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ClassMethods

extended, state_machines

Methods included from Base::ClassMethods

#available?, #extended, #integration_name, #locale_path, #matches?, #version, #versions

Class Method Details

.available?Boolean

Whether this integration is available. Only true if ActiveModel is defined.

Returns:

  • (Boolean)


272
273
274
# File 'lib/state_machine/integrations/active_model.rb', line 272

def self.available?
  defined?(::ActiveModel)
end

.included(base) ⇒ Object

:nodoc:



259
260
261
# File 'lib/state_machine/integrations/active_model.rb', line 259

def self.included(base) #:nodoc:
  base.versions.unshift(*versions)
end

.matches?(klass) ⇒ Boolean

Should this integration be used for state machines in the given class? Classes that include ActiveModel::Dirty, ActiveModel::Observing, or ActiveModel::Validations will automatically use the ActiveModel integration.

Returns:

  • (Boolean)


280
281
282
# File 'lib/state_machine/integrations/active_model.rb', line 280

def self.matches?(klass)
  %w(Dirty Observing Validations).any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
end

Instance Method Details

#invalidate(object, attribute, message, values = []) ⇒ Object

Adds a validation error to the given object



297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/state_machine/integrations/active_model.rb', line 297

def invalidate(object, attribute, message, values = [])
  if supports_validations?
    attribute = self.attribute(attribute)
    options = values.inject({}) do |options, (key, value)|
      options[key] = value
      options
    end
    
    default_options = default_error_message_options(object, attribute, message)
    object.errors.add(attribute, message, options.merge(default_options))
  end
end

#reset(object) ⇒ Object

Resets any errors previously added when invalidating the given object



311
312
313
# File 'lib/state_machine/integrations/active_model.rb', line 311

def reset(object)
  object.errors.clear if supports_validations?
end

#write(object, attribute, value, *args) ⇒ Object

Forces the change in state to be recognized regardless of whether the state value actually changed



286
287
288
289
290
291
292
293
294
# File 'lib/state_machine/integrations/active_model.rb', line 286

def write(object, attribute, value, *args)
  result = super
  
  if (attribute == :state || attribute == :event && value) && supports_dirty_tracking?(object) && !object.send("#{self.attribute}_changed?")
    object.send("#{self.attribute}_will_change!")
  end
  
  result
end