-
Notifications
You must be signed in to change notification settings - Fork 3
Events and tasks
In CFlow, everything happens within an event. In your configuration, or by convention, there are tasks associated with each event. These tasks are executed in sequence.
An event has a target and a type. You can view the target as a category or group of events. The type is the name of the event.
For instance, there could be a target 'session' that has events 'login', 'logout'. The 'login' event could then have tasks like authentication and (if successful) displaying the main page of your application.
When an event occurs, such as when a request is received, an Event object is created and populated. This object is passed from task to task. Tasks can put information on it that is then available to other tasks along the line.
There are 4 types of tasks:
- Invoke tasks: these tasks invoke a method on a controller. This method receives the
Eventobject, and can take data from it or put data back on it for subsequent tasks. - Render tasks: these tasks generate the output. These are the views, in MVC terms.
- Dispatch tasks: these tasks fire an event, which executes the tasks associated with that event. This new
Eventhas all the data available up until that point. However, data appended during task execution for this new event isn't moved back to the originatingEventobject: data can only be passed in one direction in the event chain. - Redirect tasks: these tasks perform a redirect using
<cflocation>.
The invoke and render tasks implement the indivisible parts of your application, and each task knows only that which is needed to do its job. Specifically, it doesn't know anything about other tasks. So in order to do useful work, the tasks have to be wired together. This can be done using configuration, where you describe the flow through your application, or conventions, where you describe nothing but instead use framework defaults.
To clarify things more, we'll now walk through the event handling using the framework conventions.
In your Application.cfc, edit onApplicationStart():
application.context = new cflow.Context();
application.context.setImplicitTasks(true); // enable conventions
application.context.setControllerMapping("dot.delimited.mapping");
application.context.setViewMapping("/slash/delimited/mapping");
The Context object contains all information that CFlow needs to execute tasks associated with a request. You don't have to create it in onApplicationStart() per se, but that is the logical place since only one instance of it is needed for the life of the application.
The call to setImplicitTasks() tells CFlow that if an event occurs that is not defined in the configuration, it should create tasks based on the framework convention, described below.
The calls to setControllerMapping() and setViewMapping() are optional, but given we're using only conventions, it's advisable to use them in order to control where CFlow is looking for controllers and views, respectively.
In onRequestStart():
var response = application.context.handleRequest();
response.write();
The handleRequest() method collects the url and form variables into a struct and puts that on a new Event object. It further expects parameters named target and event (present in the url or form scope), for instance /index.cfm?target=session&event=login. The method returns a Response object, which contains all content generated by your views.
If you don't want any of this, you can also implement your own RequestStrategy. This allows you to do whatever you want with urls, forms and what have you before you pass it to CFlow.
Finally, response.write() outputs the generated content.
Since we're using conventions, and no configuration is defined, CFlow will now create tasks for this event: an invoke task that will invoke a method on a controller (if this controller can be found); and a render task that will render a .cfm template. The created tasks are cached.
Let's suppose that we have a target 'session' and event type 'login'. In this case, CFlow checks if a component exists at dot.delimited.mapping.session (and that it is a Controller). If so, it creates an instance of the component and invokes the method login() (which will throw an exception if it doesn't exist). After the method is done, it will include /slash/delimited/mapping/login.cfm, which generates the output.
So, let's create those files:
component {
public void function login(required Event event) {
// dummy code
arguments.event.loggedin = true;
}
}
Notice the following:
- The
login()handler receives an argumentevent, which is the event object used to carry information from task to task. The method returns void. - You can use this object just like a struct and put extra information on it.
The view could be something like this:
<cfif loggedin>
<h1>Welcome</h1>
<cfelse>
<h1>Login failed</h1>
</cfif>
All parameters available on the event object are available in the view template as local variables. The event object itself is not available, so a view can never do anything to an event, it has only read access to the data gathered by the controllers. Which is all a view would need.
Let's extend the session controller a little bit and allow access based on some condition. If access is denied, then we want something else to happen.
component {
public void function login(required Event event) {
if (!arguments.event.allowAccess) {
arguments.event.cancel();
arguments.event.dispatch("loginFailed");
}
}
public void function loginFailed(required Event event) {
// nothing to do, but CFlow will want to call this method by convention
// if you use configuration, you won't need to do this
}
}
To keep things simple, authentication logic has been replaced by an allowAccess boolean that's passed on the event object. If access is allowed, the same thing happens as before. If access is not allowed, the method cancels the current event, dispatches another event 'loginFailed', and exits. As far as this method is concerned, it has done its job and doesn't care what happens next.
The cancel() method stops all subsequent tasks from executing. In this case, that applies to one task: the one that renders the login template.
Instead, a new event is dispatched, which means different tasks are now executed. The 'loginFailed' event is interpreted to have the same target as the current event, i.e. 'session'. A new Event object is created, all variables available on the current event object are copied to this new object, and tasks are executed. In this case, using conventions, CFlow will invoke the method loginFailed() on the controller, followed by rendering the template /slash/delimited/mapping/loginFailed.cfm.
As you can see, we can now remove the <cfif> from the login.cfm template, since it's not up to that template to determine what happened. Using the event mechanism we were able to separate concerns, killing an if statement in the process. Furthermore, we can reuse the login() method if we want, for instance for an api that will be developed in the future.
For simple applications, using only conventions is feasible. For more demanding applications, configuration is advisable.