| 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.
A flow is a logical procedure which defines how to handle different steps and guide the user through them.
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 :
More step types can be created to extend the ActionFlow framework.
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
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.
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.
There are many other config options which can be used. I strongly suggest reading the whole API documentation.
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.
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.
| Event_input_name_regex | = | /^#{event_input_name_prefix}#{event_prefix}([a-zA-Z0-9_]*)$/ | Defines the |
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
# 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
# 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
# File lib/action_flow/base.rb, line 220 def self::reserved_event?(event_name) reserved_events.include? event_name.to_s end
Default method to handle the requests. All the dispatching is done here.
# 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
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
# 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
# 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 }
# 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.
# 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
# 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.
# 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