-
Notifications
You must be signed in to change notification settings - Fork 3
Configuration
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 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.
Contains one or more <target> nodes.
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)
<target> can have the following child nodes:
Defines tasks for the start phase.
Defines tasks for the before phase.
Defines targets for an event on this target. Attributes:
- type: the event type (required)
Defines tasks for the after phase.
Defines tasks for the end phase.
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). You will get a runtime exception if the include cascade produces tasks without a controller.
Defining tasks within the above nodes (phases) is done with the nodes <invoke>, <dispatch> and <render>. The order of these nodes specifies the order of execution.
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.
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.
Renders a view by including the corresponding template. Attributes:
- template: 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.
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" />
<render template="failed" />
</event>
</target>
<target name="world" defaultcontroller="World">
<include target="session" />
<include target="template" />
<before>
<invoke controller="global.Navigation" method="storeCrumbTrail" />
</before>
<event type="hello">
<render template="hello" />
</event>
<event type="goodbye">
<dispatch target="session" event="exit" />
<render template="goodbye" />
</event>
</target>
<target name="template">
<start>
<render template="header" />
</start>
<end>
<render template="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.
Look at the order of the includes: the last include's start task are executed first, and its end tasks are executed last.
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:
- An invoke task that will invoke the method
exit()on controllersession. So the target name is used for the controller name, and the event type is used for the method. - A render task that will try to render the template session/exit.cfm. The target name is used for the directory (within the viewmapping set on the
Contextobject), and the event type is the name of the template.
So you can mix configuration with convention, although it's probably not good practice.
For this, you use the TaskReader object. You just create one like so:
var reader = new cflow.request.TaskReader();
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 (assuming the Context object is available in application.context):
reader.register(application.context);
This 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.