| 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 :
- 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.
An event is nothing more than a symbol which triggers the execution of methods or steps, depending of the mapping.
Special event names are reserved to tell the framework to do specific tasks.
These event names are reserved and serve the following purposes.
- 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.
| Event_prefix | = | '_event_' | Defines the symbols name prefix which are used to identify the next events. As an example, in a view_state, the button used to trigger the save event would be named ‘_event_save’. If we were in an action_state, to trigger the success event, we would ‘return :_event_success‘ | |
| Event_input_name | = | '_actionflow_event' | Defines the name of the parameter submitted which contains the next event name. | |
| Flow_execution_key_id | = | 'flow_exec_key' |
Defines the name of the variable which holds the flow unique state id. As
an example, a view_state would add a hidden field named
‘flow_exec_key’, which would contain the key used to store a
state in the session.
We could then restore the state by accessing it with : session[:flow_data-1234abcd].fetch( params[:flow_exec_key] ) See the SessionHandler for more details |
Makes sure that the Event_input_name constant is available to the view
# File lib/action_flow/base.rb, line 106 def self::event_input_name Event_input_name end
Makes sure that the Event_prefix constant is available to the view
# File lib/action_flow/base.rb, line 96 def self::event_prefix Event_prefix end
Makes sure that the Flow_execution_key_id constant is available to the view
# File lib/action_flow/base.rb, line 101 def self::flow_execution_key_id Flow_execution_key_id end
Initializes the class instance
# File lib/action_flow/base.rb, line 112 def initialize # Initializes the step registry of the current controller instance step_registry_value = Hash.new # Makes sure that the nil value is in the @start_step instance variable upon init start_step_value = nil # Makes sure that the nil value is in the @end_step instance variable upon init end_step_value = nil 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_step :defined_step_name
would point to the definition :
view_state :defined_step_name do
whatever
end
# File lib/action_flow/base.rb, line 262 def self::end_step(step_name) @end_step = step_name.to_s 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 206 def self::register(step_name, step) self.step_registry_value.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_step :defined_step_name
would point to the definition :
view_state :defined_step_name do
whatever
end
# File lib/action_flow/base.rb, line 226 def self::start_step(step_name) @start_step = step_name.to_s end
Makes the start_step instance variable visible to the execute and index methods
# File lib/action_flow/base.rb, line 233 def self::start_step_value return @start_step end
Default method to handle the requests. All the dispatching is done here.
# File lib/action_flow/base.rb, line 128 def index # 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 # Make sure there's a start_step defined raise (ActionFlowError.new, "Your controller must declare a start step name. Use 'start_step :step_name' and define this step in the mapping.") if self.class.start_step_value.nil? # Make sure there's an end_step defined raise (ActionFlowError.new, "Your controller must declare an end step name. Use 'end_step :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 self.class.end_step_value.nil? # Start a new flow session storage start_new_flow_session_storage # Execute the start_step execute_step self.class.start_step_value else # We have to resume a flow with a given id # Make sure there's data associated to this flow raise (ActionFlowError.new, "No flow data could be found for the given flow key. Your session doesn't exist.") unless validate_flow_existence # Get the event which was trigered and make sure that # the submit button had a value prefixed by ActionFlow::Base.event_prefix # The event name is put into 'event_name' variable. params[Event_input_name].to_s =~ /^#{Event_prefix}([a-zA-Z]+[a-zA-Z0-9_]*)$/ ? event_name = $1 : raise(ActionFlowError.new, "Your view did not submit properly an event name. The event name '#{params[Event_input_name]}' is not valid") # 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 self.class.step_registry_value.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 self.class.step_registry_value.fetch(last_step).outcome(event_name) end # Check if we continue if @end_step_reached # We clean all data on this flow in the session terminate end # Cleanup the flows which have been hanging too long in the session placeholder cleanup end
Launches the execution of a method on the controller. Encountered errors will be managed as defined in the mapping.
# File lib/action_flow/base.rb, line 271 def execute_step(step_name) step_name = step_name.to_s # Make sure that this step is registered raise (ActionFlowError.new, "The desired step ( #{step_name} ) is not present in the mapping.") unless self.class.step_registry_value.has_key? step_name # Validate that the controller will answer to the message raise (ActionFlowError.new, "Your controller doesn't have a method called '#{step_name}' as defined in your mapping.") if !respond_to?(step_name) && self.class.step_registry_value.fetch(step_name).definition_required? # Everything is ready for execution, we just have to memorize that this step # was caled. persist_last_step_name step_name # Encapsulate the execution in a begin block to manage # the errors. Runtime errors are not managed. Only subclasses # of StandardError will be. begin # Execute the step and keep the returned value return_value = self.class.step_registry_value.fetch(step_name).send :execute, self # Check if this was the end step, if so, cut the execution short if self.class.end_step_value.to_s == fetch_last_step_name # Raise that flag @end_step_reached = true # Terminate execution chain return end # Make sure that the step returned an event object raise (ActionFlowError.new, "Your step definition named '#{step_name}' didn't return an ActionFlow::Event object. All steps must return either events or errors.") unless return_value.kind_of? ActionFlow::Event # If the event name is 'render', we have to get back to the view unless return_value.name =~ Regexp.new( /^render$/, 'i' ) # Make sure that the step has an outcome defined for this event raise (ActionFlowError.new, "No outcome has been defined for the event '#{return_value.name}'. Use the 'on' method on your mapped step.") unless self.class.step_registry_value.fetch(step_name).has_an_outcome_for?(return_value.name.to_s) # Call recursively the step mapped to the returned event execute_step self.class.step_registry_value.fetch(step_name).outcome(return_value.name) end # Rescue any subclass of StandardError rescue StandardError => error # Ask the step if he handles this error if self.class.step_registry_value.fetch(step_name).handles? error.class.to_s # Get the handler step name handler_name = self.class.step_registry_value.fetch(step_name).handler(error.class.to_s) # Make sure this handler is in the registry raise (ActionFlowError.new, "The error handling step ( #{handler_name} ) is not present in the mapping.") unless self.class.step_registry_value.has_key? handler_name # Execute the error handling step recursively execute_step handler_name else # Well, we've tried. It's now an official 'Someone Else Problem' raise error end end end