Skip to content
neoneo edited this page Apr 7, 2012 · 34 revisions

Configuring CFlow not only allows you to describe your application flow, but it also adds some nice features.

First, you get to define tasks for different phases. There are 5 phases, in order of execution: start, before, event, after and end.

  • start: these tasks are executed exactly once during the whole event handling cycle.
  • before: these tasks are executed every time an event is dispatched on this target, but only if the event is not canceled in one of the start tasks.
  • event: these tasks are specific for a given event type. They are executed only for that particular event, provided the event is not canceled in one of the previous phases.
  • after: these tasks are executed every time an event is dispatched on this target, if the event is not canceled in one of the previous phases.
  • end: these tasks are always executed exactly once, regardless of event canceling.

All this you can also do programmatically, by using the methods provided by Context.

The second nifty feature you get with configuration, is not available programmatically. You can include definitions from one target in another. These includes can be recursive: a target that is included may itself include yet another target, and so forth. See below at the <include> node description.

The XML dialect

The XML reflects how tasks are executed by CFlow. Tasks are organized in targets (a 'category') and within those in events. Additionally there are the various phases as described above.

The root node of an xml file is either <targets>, if you define multiple targets in one file, or <target>, for one target per file. Below all the node types are described, followed by an example.

<targets>

Contains one or more <target> nodes.

<target>

Attributes:

  • name: the name of the target
  • defaultcontroller: the name of the controller to use for invoke tasks, if no controller is specified there (optional)
  • abstract: a boolean; if true, this target cannot accept events directly but must be included in a concrete target (optional, default is false)

<target> can have the following child nodes:

<start>

Defines tasks for the start phase. At most 1 node can be present.

<before>

Defines tasks for the before phase. At most 1 node can be present.

<event>

Defines targets for an event on this target. For each event there will be a node. Attributes:

  • type: the event type (required)

<after>

Defines tasks for the after phase. At most 1 node can be present.

<end>

Defines tasks for the end phase. At most 1 node can be present.

<include>

Includes tasks from another target. Attributes:

  • target: the name of the target to include tasks from (required)
  • event: the event type to include (optional). If no event attribute is present, the complete target is included, including all phases defined for it.

Including another target occurs following these rules:

  • If the receiving target already has a definition for an event, the event is not included.
  • Start and before tasks from the included target are prepended to the existing start and before tasks, respectively.
  • After and end tasks are appended to any existing after and end tasks.

Of course the last two rules don't apply if you only include a specific event (i.e. if there is an event attribute).

Multiple <include>s are processed in reverse order. This is done to try to obey the principle of least surprise. If they were processed in order, the first include would execute its start tasks last, following the rules above. Semantically, this doesn't seem right. Now, in reverse order, start tasks from the first <include> are executed first, followed by start tasks from the one after that, etc., and end tasks from the first <include> are executed last.

If some to be included target A includes another target B, first target B is included in target A, and the result of this is then included. Circular references are not allowed.

Included <invoke> nodes that have no controller attribute will have this attribute set to the default controller of their original target before they are included. At the same time, their original <target> node might have no default controller specified. If this occurs, the receiving target's default controller is used, if set (and so on). If the include cascade produces tasks without controllers, a runtime exception is thrown. To prohibit this, set abstract="true" on the <target> node that contains the task.

Defining tasks

Defining tasks within the above nodes (phases) is done with the nodes <invoke>, <dispatch>, <render> and <redirect>. Additionally, the nodes <if>, <else> and <set> allow you to use some elementary programming constructs. The order of these nodes specifies the order of execution.

<invoke>

Invokes a method on a controller. Attributes:

  • controller: the name of the controller (optional). If this attribute is not present, the default controller specified in the <target> node is used instead.
  • method: the name of the method to invoke (required).

This node can contain child task nodes. These are only executed if the event is canceled.

<dispatch>

Dispatches an event. Attributes:

  • target: the target to dispatch the event on (optional). If not present, the target is the one this task is defined in.
  • event: the event type

If the dispatched event is canceled, the current event is also canceled.

This node can contain child task nodes. These are only executed if the dispatched event, and therefore the current event, is canceled.

<render>

Renders a view by including the corresponding template. Attributes:

  • view: the template to render. Leave out the .cfm extension, it is appended by the task. The task looks for the template using the view mapping (if set), in a directory with the name of the target.

<redirect>

Performs a redirect (<cflocation>). Attributes:

  • permanent: when true, uses status code 301. Otherwise, a 302 is issued (default).
  • url: the url to redirect to. When url is present, the following attributes are ignored.
  • target: the target to redirect to. When specified, the event attribute is required.
  • event: the event to redirect to. If no target attribute is specified, the target is assumed to be the target this task is defined in.
  • parameters: a comma-separated list of parameter names available in the event to append on the url. You can optionally rename these parameters, by using the form "[newname]=[name]", where [name] is the variable name available on the event.

When using the target and event attributes, the RequestStrategy is used to construct the url.

Next to the tasks, you can use the following elementary programming constructs in your configuration. You probably should keep the use of these nodes to a minimum, but on the other hand, it allows you to keep the controller a little bit cleaner. Without these, you may sometimes find yourself including methods to your controller that you don't really want, or dispatching a lot of events to get some more elaborate logic working. These constructs are:

<if>

This does what you expect. If the condition is true, child tasks are executed. Attributes:

  • condition: the condition to be evaluated (required). This is a regular ColdFusion expression that should return a boolean. Any variables that you use in the expression are looked up on the event object. It is advisable to use the CFML style operators (eq, neq, gt, lt, etc.).

<else>

Used in <if>. This node has no child nodes. This works the same as <cfif> and <cfelse>, except that the closing tag is required since we're working in xml. Attributes:

  • condition: same as with <if>, except it's not required.

<set>

Sets a parameter on the event object. This can be a literal value or a ColdFusion expression. Attributes:

  • [any name]: the value or expression to set on the event parameter with the attribute name.

Example

Let's define some tasks for a hello world application that needs authentication. This allows us to use most of the features above.

<?xml version="1.0"?>
<targets>
    <target name="session" defaultcontroller="Session">
        <start>
             <invoke method="authenticate">
                 <dispatch event="failed" />
             </invoke>
        </start>
        <event type="failed">
             <invoke controller="global.Logger" method="write" />
             <redirect event="login" />
        </event>
            <event type="login">
                 <render view="login" />
            </event>
    </target>

    <target name="world" defaultcontroller="World">
        <include target="template" />
        <include target="session" />
            <start>
                <set dispatched="false" />
            </start>
        <before>
            <invoke controller="global.Logger" method="write" />
        </before>
        <event type="hello">
            <render view="hello" />
            <set dispatched="true" />
            <dispatch event="goodbye" />
        </event>
        <event type="goodbye">
            <dispatch target="session" event="exit" />
                <if condition="dispatched">
                    <render view="goodbye" />
                <else />
                    <render view="bogus" />
                </if>
        </event>
    </target>

    <target name="template">
        <start>
            <render view="header" />
        </start>
        <end>
            <render view="footer" />
        </end>
    </target>
</targets>

As you can see this is pretty readable.
By including the session target in the world target, we've put everything that's happening in this target behind authentication. Following the rules above, in the start phase, first template/header.cfm is rendered (defined in the template target), then the Session controller's authenticate() method is invoked. If authentication fails, it cancels the event and the task's child tasks are executed, in this case dispatching an event. This event invokes the write() method on the global.Logger controller, followed by rendering session/failed.cfm. After this, the end tasks (rendering template/footer) are (always) executed.
For the case when access is allowed, it's not very difficult to follow the flow. To see the flow visualized, check out the debugging page.

Conventions

In the goodbye event in the world target, an event 'exit' is dispatched on the session target. This event is not defined there, so what happens now depends on whether setImplicitTasks(true) was called on the Context. If so, then two tasks are created and executed, based on the framework convention. In this case, these are:

  1. An invoke task that will invoke the method exit() on controller session. So the target name is used for the controller name, and the event type is used for the method name.
  2. A render task that will render the template session/exit.cfm. The target name is used for the directory (within the viewmapping set on the Context object), and the event type is the name of the template.

So you can mix configuration with convention, although it's probably not good practice.

Reading the XML configuration

For this, you use the XmlReader object. You just create one like so:

var reader = new cflow.request.XmlReader(application.context);

You then point it to the directory where your xml files are located. So you don't point to individual files. The reader uses ExpandPath() so any path that can be resolved like this, is ok. The reader now reads all xml files in this directory and its subdirectories.

reader.read("/directory/on/filesystem");

Xml files that do not have a <targets> or <target> root node are ignored. In the future, XMLSchema will be used for this.

You can call read() multiple times to read files in different directories, if needed.
When all your configuration files are read, you call:

reader.register();

At this point, first all includes are processed. Then the reader creates all Task instances and registers them at the given context. In the process, all Controller instances are created as well, so all 'static' objects are now available and ready for use.

Since the whole configuration is now compiled in object instances that execute code, CFlow contains very little boilerplate code when processing a request. Since most objects are (treated as) static, object creation is kept to a minimum, which, at least in the case of ColdFusion, is a good thing performance wise.

Clone this wiki locally