Class ActionFlow::Base
In: lib/action_flow/base.rb
Parent: ActionController::Base

The ActionFlow framework turns an ActionController into a flow capable controller. It can handle event trigerring, the back button and any user defined step. Here are the main concepts.

Concepts

Flow

A flow is a logical procedure which defines how to handle different steps and guide the user through them.

Step

A step is a single unit of processing included inside a flow. All steps have to inherit from ActionFlow::FlowStep. They can be used to define one of the following :

  • ViewStep : displays a view and then relays the execution to methods or steps according to the trigered event.
  • ActionStep : calls a method of an ActionFlow controller and then relays the execution to methods or steps according to the trigered event.

More step types can be created to extend the ActionFlow framework.

Event

An event is nothing more than a symbol which triggers the execution of methods or steps, depending on the mapping. There are reserved events which cannot be mapped by a user inside of a subclass of ActionFlow::Base controllers. To ask the controller if an event name has been reserved for internal purposes, use :

 ActionFlow::Base.reserved_event? :event_name_to_verify

Creating a mapping

The mapping is where you tell your controller how to connect the different steps.

To declare a controller mapping, you must do it in the object‘s initialize method. This method will be automatically called upon the instanciation of your controller.

Simple mapping example

 class MyController < ActionFlow::Base

   def initialize

     start_with             :step_1
     end_with               :end_my_flow
     redirect_invalid_flows :step_1

     upon :StandardError => :end_my_flow

     action_step :step_1 do
       method :start_my_flow
       on     :success => :step_2
     end

     view_step :step_2 do
       on :finish => end_my_flow
     end

     view_step :end_my_flow

   end

   def start_my_flow
     # Nothing to be done
     event :success
   end

 end

Let‘s decompose what we just did in this very minimalist controller.

  • The flow starts with the step named step_1.
  • The flow data will be destroyed once we reach the end_my_flow step.
  • If the user sends an invalid key or his flow data is expired, we redirect the flow to the step_1 step.
  • If an error of class StandardError or any subclass of it is raised and not handled by the steps, the controller will route the flow to the end_my_flow step.
  • We declared a step named step_1. The step is implemented via the method named start_my_flow. If the success event is returned, we route the flow to the step named step_2.
  • We declared a step named step_2. If the finish event is returned, the step named end_my_flow will be called.

There are many other config options which can be used. I strongly suggest reading the whole API documentation.

Reserved events

The ActionFlow::Base controller reserves the following event names for itself.

 - render : Tells the framework that we must return the control to the view
            parser, since one of the 'render' method has been called and we
            are now ready to display something.

About Plugins

The ActionFlow framework has a plugin system, but is unstable and incomplete. Don‘t bother using it for now, unless you want to contribute of course. This class is stable and ready for plugin registrarion though. The Plugin class is not. To activate the plugins system, go to the #{ACTIONFLOW_ROOT}/action_flow/action_flow.rb file and uncomment the plugins initialisation code.

Methods

Constants

Event_input_name_regex = /^#{event_input_name_prefix}#{event_prefix}([a-zA-Z0-9_]*)$/   Defines the

Public Class methods

Class method to tell the ActionFlow framework to notify a plugin upon a certain internal event happening. Use it in a plugin class as follows :

 ActionFlow::Base.listen :some_event, self

[Source]

# File lib/action_flow/base.rb, line 201
    def self::listen(internal_event_name, plugin)
    
      # Reserve the event passed as a parameter but first validate
      # that it's a subclass of ActionFlow::Plugin
      #plugin.kind_of?(ActionFlow::Plugin) ? filters.fetch(internal_event_name.to_s).push(plugin) : raise(ActionFlowError.new, "One of your plugins tried to register itself as a listener and is not a subclass of ActionFlow::Plugin. The culprit is : " + plugin.inspect )
      
      filters.fetch(internal_event_name.to_s).push(plugin)

    end

Class method to tell the ActionFlow framework to prevent users from mapping certain event names. Use it as :

 ActionFlow::Base.reserve_event :some_event_name

[Source]

# File lib/action_flow/base.rb, line 182
    def self::reserve_event(event_name)
    
      # Reserve the event passed as a parameter
      reserved_events.push( event_name.to_s )
      
      # Remove duplicates
      reserved_events.uniq!
    
    end

Class method to ask the ActionFlow framework if a given event name has been reserved and can‘t therefor be mapped by users.

 ActionFlow::Base.reserved_event? :some_event_name

[Source]

# File lib/action_flow/base.rb, line 220
    def self::reserved_event?(event_name)
        
      reserved_events.include? event_name.to_s
        
    end

Public Instance methods

Default method to handle the requests. All the dispatching is done here.

[Source]

# File lib/action_flow/base.rb, line 232
    def index
    
      # Notify the plugins we got here
      notify :request_entry
    
      # Tells which was the last state
      @flow_id = params[flow_execution_key_id]
      
      # Flag used to know if we reached the end_step
      @end_step_reached = false
        
      # Check if the flow has already started
      unless @flow_id
      
        # Notify plugins we've just started a new flow
        notify :before_new_flow
        
        # Make sure there's a start_step defined
        raise (ActionFlowError.new, "Your controller must declare a start step name. Use 'start_with :step_name' and define this step in the mapping.") if start_step.nil?
                      
        # Make sure there's an end_step defined
        raise (ActionFlowError.new, "Your controller must declare an end step name. Use 'end_with :step_name' and define this step in the mapping. I suggest using a view step which could redirect if you don't want to create a 'thank you' screen.") if end_step.nil?
        
        # Start a new flow session storage
        start_new_flow_session_storage
        
        # Notify plugins
        notify :before_step_execution_chain
        
        # Execute the start_step
        execute_step start_step
      
      else # We have to resume a flow with a given id
      
        # Notify plugins we've just started a new flow
        notify :before_flow_resume
        
        # Get the event name
        raise(ActionFlowError.new, "Your view did not submit properly an event name.") unless event_name = url_event_value(params)
        
        # Make sure there's data associated to this flow
        return nil if redirect_invalid_flow!
        
        # We need to know where we came from
        last_step = fetch_last_step_name
        
        # Make sure that the step has an outcome defined for this event
        raise (ActionFlowError.new, "No outcome has been defined for the event '#{event_name}' on the step named '#{last_step}' as specified in the submitted data. Use the 'on' method on your mapped step or make sure that you are submitting valid data.") unless step_registry.fetch(last_step).has_an_outcome_for?(event_name)
        
        # Before doing anything, we create a new state so we can restore the previous one with
        # the back button.
        serialize
        
        # Call the resulting step associated to this event
        execute_step step_registry.fetch(last_step).outcome(event_name)
        
      end
      
      # Notify plugins
      notify :after_step_execution_chain
      
      # Cleanup the flows which have been hanging too long in the session placeholder
      cleanup
      
      
      # Check if we continue
      if @end_step_reached
      
        # We clean all data on this flow in the session
        terminate
        
      end
      
    
    end

Protected Instance methods

Called in a mapping to define the end point of the flow. Once this step finishes it‘s execution, the flow data is deleted. Pass it a symbol which bears the same name as a defined step. ie.

  end_with :defined_step_name

would point to the definition :

  view_state :defined_step_name do
    (...)
  end

[Source]

# File lib/action_flow/base.rb, line 342
      def end_with(step_name)
        @_end_step = step_name.to_s
      end

Suger method called from suclasses to return events.

Use it as :

 def my_action

  # Do something

  # Now return an event
  event :success

 end

[Source]

# File lib/action_flow/base.rb, line 447
      def event(name)
        ActionFlow::Event.new(name.to_s)
      end

Called in a mapping to define which method should be called if the user submits an invalid flow id. Use it as :

 redirect_invalid_flows :url => { :controller => :my_controller, :action => :index }

[Source]

# File lib/action_flow/base.rb, line 375
      def redirect_invalid_flows( url={:action=>:index} )
        @_no_flow_url = url
      end

Adds a step name to the registry and associates it to an object which will be used to handle a given step when necessary.

[Source]

# File lib/action_flow/base.rb, line 321
      def register(step_name, step)
        step_registry.store(step_name.to_s, step)
      end

Called in a mapping to define the starting point of the flow. Pass it a symbol which bears the same name as a defined step. ie.

  start_with :defined_step_name

would point to the definition :

  view_state :defined_step_name do
    (...)
  end

[Source]

# File lib/action_flow/base.rb, line 359
      def start_with(step_name)
        @_start_step = step_name.to_s
      end

Allows the controller to handle errors which were not handled by the steps themselves.

 class MyController < ActionFlow::Base
   def initialize
     (...)
     upon :ActionFlowError => :step_name
   end
   (...)
 end

This is also possible :

 class MyController < ActionFlow::Base
   def initialize
     (...)
     upon { :ActionFlowError => :step_name,
            :WhateverError =>   :step_name  }
   end
   (...)
 end

Only subclasses of StandardError will be rescued. This means that RuntimeError cannot be handeled.

[Source]

# File lib/action_flow/base.rb, line 409
      def upon ( hash )
              
        # Make sure we received a hash
        raise(ActionFlowError.new, "The 'upon' method takes a Hash object as a parameter. Go back to the API documents since you've obviously didn't read them well enough...") unless hash.kind_of?(Hash)
              
        # Make sure the key is not used twice in a definition
        # to enforce coherence of the mapping.
        hash.each_key do |key|
          raise (ActionFlowError.new, "An error of the class '#{key}' is already mapped. They must be unique within a controller scope.") if handlers.has_key?(key.to_s)
        end
              
        # Store the values in the handlers hash
        hash.each { |key,value| handlers.store key.to_s, value.to_s }
              
      end

[Validate]