Class: WebFlow::Base

Inherits:
ApplicationController
  • Object
show all
Defined in:
lib/webflow/base.rb

Overview

The WebFlow framework turns an ActionController into a flow capable controller. It can handle event triggering, 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 WebFlow::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 triggered event.

  • ActionStep : calls a method of an WebFlow controller and then relays the execution to methods or steps according to the triggered event.

More step types can be created to extend the WebFlow 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 WebFlow::Base controllers. To ask the controller if an event name has been reserved for internal purposes, use :

WebFlow::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 instantiation of your controller.

Simple mapping example

class MyController < WebFlow::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 WebFlow::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 WebFlow 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 registration though. The Plugin class is not. To activate the plugins system, go to the #ACTIONFLOW_ROOT/webflow/webflow.rb file and uncomment the plugins initialisation code.

Constant Summary collapse

Event_input_name_regex =

Defines the regex used to validate the proper format of a submitted event name.

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

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_'
@@event_input_name_prefix =

Defines the name of the parameter submitted which contains the next event name.

'_submit'
@@flow_execution_key_id =

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.fetch( params )

See the SessionHandler for more details

'flow_exec_key'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.listen(internal_event_name, plugin) ⇒ Object

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

WebFlow::Base.listen :some_event, self


197
198
199
200
201
202
203
204
205
# File 'lib/webflow/base.rb', line 197

def self::listen(internal_event_name, plugin)

  # Reserve the event passed as a parameter but first validate
  # that it's a subclass of WebFlow::Plugin
  #plugin.kind_of?(WebFlow::Plugin) ? filters.fetch(internal_event_name.to_s).push(plugin) : raise(WebFlowError.new, "One of your plugins tried to register itself as a listener and is not a subclass of WebFlow::Plugin. The culprit is : " + plugin.inspect )
  
  filters.fetch(internal_event_name.to_s).push(plugin)

end

.reserve_event(event_name) ⇒ Object

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

WebFlow::Base.reserve_event :some_event_name


181
182
183
184
185
186
187
188
189
# File 'lib/webflow/base.rb', line 181

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

.reserved_event?(event_name) ⇒ Boolean

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

WebFlow::Base.reserved_event? :some_event_name

Returns:

  • (Boolean)


213
214
215
216
217
# File 'lib/webflow/base.rb', line 213

def self::reserved_event?(event_name)
    
  reserved_events.include? event_name.to_s
    
end

Instance Method Details

#indexObject

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



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/webflow/base.rb', line 221

def index

  # Holds the calls to the render method to delay their execution
  @renders = Array.new
        
  # Holds the calls to the redirect method to delay it's execution
  @redirects = Array.new

  begin
  
    # 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
    if @flow_id && !@flow_id.empty?

      # Notify plugins we've just re-started a flow
      #notify :before_flow_resume
    
      # Get the event name
     raise(WebFlowError.new, "Your view did not submit properly to 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(WebFlowError.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)

    else

      # Notify plugins we've just started a new flow
      #notify :before_new_flow

      # Make sure there's a start_step defined
      raise(WebFlowError.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(WebFlowError.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

    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
    terminate if @end_step_reached
    
    # Execute all the cached render calls
    render!
    
    # Execute all the cached redirect calls
    redirect!
    
    # Rescue interruptions thrown by the plugins
    #rescue PluginInterruptionError => error
  
      # We execute the interruption block
      #instance_eval &error.block
  
    end
end

#old_redirect_toObject

Aliases the ‘redirect_to’ method to intercept it and cache it’s execution results. Doing this allows to delay the calls to the redirect_to method until the end of the current step chain so that if a plugin wishes to interrupt the chain and redirect the request, there won’t be any conflict between previous calls to the redirect_to method.



173
# File 'lib/webflow/base.rb', line 173

alias_method :old_redirect_to, :redirect_to

#old_renderObject

Aliases the ‘render’ method to intercept it and cache it’s execution results. Doing this allows to delay the calls to the render method until the end of the current step chain so that if a plugin wishes to interrupt the chain and render some other content, there won’t be any conflict between previous calls to the render method.



165
# File 'lib/webflow/base.rb', line 165

alias_method :old_render, :render