Skip to content

A Beginner's Guide to Mach II

akashbavlecha edited this page Mar 15, 2022 · 10 revisions

Table of Contents

  1. A Beginner's Guide to Mach-II
  2. Basics of MVC
  3. The Mach-II Configuration File
  4. Building a Simple Mach-II Application
  5. Moving Forward
  6. References
  7. Anatomy of Mach-II Series

Contributed by Brian Rinaldi

A Beginner's Guide to Mach-II

With the advent of ColdFusion components (CFCs) back in ColdFusion MX (i.e. version 6), object-oriented coding practices in ColdFusion became possible. Originally built by Ben Edwards and Hal Helms as Fusebox MX, Mach-II was the first object-oriented ColdFusion framework built around the Model-view-controller architecture (MVC). Currently the framework is maintained by Peter Farrell, Kurt Wiersma and Matt Woodward with commercial support from GreatBizTools, LLC and is available at http://www.mach-ii.com.

Example code is available with this guide. Please see the attachments section on the bottom of this page.

Basics of MVC

The primary purpose of an MVC framework is to help improve code organization, ease maintenance and promote code reuse through a rather rigid separation of concerns, those concerns being the domain layer (i.e. the model) and the view layer. This separation allows changes to the view layer to occur without impacting the model and, likewise, changes to the model layer can occur without changes to the views layer. The controller layer, in simplistic terms, handles passing data between the layers.

The Model

In ColdFusion, your model will generally consist of a lot of CFCs. While the MVC architecture and Mach-II do not specify how you architect your domain model, Mach-II generally assumes some form of object-oriented methodology with some components representing services and objects/beans and others handling data access and persistence. However, your model should be built so as to have no knowledge that your application uses Mach-II.

The View

This layer will be made up of standard .cfm files. These files will have a slight awareness of Mach-II due to the fact that they will receive their data via the Mach-II event object. Nonetheless, the view files should, as a general rule, not contain any business logic.

The Controller

In this scenario, Mach-II represents the controller layer whereby data is transferred between the Model and View layers via specialized components called listeners, filters and plugins. The order and process by which these components are called in response to pre-defined events is defined within the Mach-II configuration file. Mach-II uses implicit invocation for event handling, meaning that events are simply announced and do not directly call any procedure. Instead, a procedure registers an interest in that event and runs whenever that event is announced. This allows the event to remain unaware of the specifics of the code that it needs execute.

The Mach-II Configuration File

The Mach-II XML configuration file defines how the Mach-II framework will manage your application. For example, this is where you define listeners and events, and where you lay out the various actions that are executed when an event is announced. In this section we will cover the various items that you define in your configuration file in the order that they typically appear.

Properties

Properties are name/value pairs that you can define and use throughout your application. A handful of properties are required by Mach-II to run, but you can define properties for any number of custom values and, as of version 1.5, representing any ColdFusion data type.

There are two required properties in Mach-II:

Property Name Data Type Required Default Value Description
defaultEvent string Required n/a As one would assume, this property defines what event is announced when no event is explicitly called.
exceptionEvent string Required n/a Again, as the name implies, this defines what event will be announced when an exception occurs during processing.

In addition to these required properties there are four additional properties that you will generally see defined in a Mach-II application as they are recommended but not required:

Property Name Data Type Required Default Value Description
applicationRoot string Optional "" This defines the root of your Mach-II application. If Mach-II lives in the root of your site, then its value will be "/". However, if your Mach-II application lwas in a subdirectory of your site, the value would be the path to that directory from the root of your site.
eventParameter string Optional event This is the name of the variable which contains the event being announced. Events are typically announced in the URL, so if the value of this is the default "event", your URL would look something like /index.cfm?event=foo. It is important to note however that since Mach-II combines both form and URL scopes, the event variable could be within a form field.
parameterPrecedence string
Allowed Values:
form, url
Optional form This property determines whether the form or the URL scope will take precedence when being added to the event object, therefore the only allowable values are "form" or "url". By default the form scope takes precedence.
maxEvents numeric Optional 10 This property defines how many events can get added to the event queue. Primarily this serves to prevent events from announcing other events in an infinite loop

Listeners

A listener is simply a specialized CFC that extend base Mach-II listener object and is required to have a configure() method. However, listeners are the primary means by which Mach-II can communicate to your model layer. While your Mach-II XML configuration defines what methods are called when an event is announced, the listener component actually implements that method. Listener component methods are framework aware and can contain any ColdFusion code. However in most cases your listener methods will simply pass data from the event object to be processed by a method on a service component.

Filters

A filter is another specialized CFC in Mach-II that extends the base filter component and, as is the standard in Mach-II, is required to have a configure() method. Filters can be selectively added to events within the Mach-II configuration file and are called before the event is processed. The filter returns a boolean value that tells Mach-II whether the event should continue processing and, if not, announces another event. Filters can be useful for any number of processes that need to run across a selective group of events, such as form validation or security.

Event Handlers

The "behavior" of every event is defined within an event handler inside the Mach-II configuration. Event handlers can be public or private, whereby private events can only be announced by another Mach-II event. A basic event handler will contain one or more notify tags to call a method on one of the defined listeners as well as the view or views that need to be called for that method. However, events can contain any number of other elements including filters, event mappings and redirects and can even announce other events.

Views

Views are basically just the CFM template(s) that determines the user-interface (UI) for any given event. They are framework aware in that they generally contain calls to get variables out of the event object in the form of event.getArg("variableName"). However, they should not have any knowledge of the underlying Mach-II framework beyond this. All data that your UI needs to render should come from the model via Mach-II (for instance, your views should not contain any queries). Other than that, views are coded exactly the same as any other standard ColdFusion template.

Plugins

When a plugin is defined, it runs at the specified plugin point on every event. There are seven plugin points available which are preProcess, postProcess, preEvent, postEvent, preView, postView and handleException. Any single plugin could include one, several or all of these plugin points. Once again, a plugin is actually a specialized CFC in Mach-II that extends the base plugin component and is required to have a configure() method. Plugins are good for code that cuts across the entire application, for instance you may commonly utilize plugins for integrating third-party tools within your Mach-II application. You will also find that a number of Mach-II developers share their plugins with the community.

Building a Simple Mach-II Application

Our sample application will build and expand upon the example I wrote about in my Objects and Composition series (refer below for references). In that example I created a set of example objects that represented an Xbox console, the games, the controllers and accessories. In this example we will utilize the same model from the "no framework" sample and build the interface for adding and editing the objects. First, let's configure Mach-II.

Defining Properties

For the purposes of this example, our application will exist in a subfolder off the root of the site called "/m2". In addition, it will use a datasource named "xbox". Therefore, we will modify the default property for applicationRoot but leave the others alone. In addition, we will add a property for our DSN. In the end, our properties section of our Mach-II configuration file will look like this:

    <properties>
        <property name="applicationRoot" value="/m2/"/>
        <property name="defaultEvent" value="home"/>
        <property name="eventParameter" value="event"/>
        <property name="parameterPrecedence" value="form"/>
        <property name="maxEvents" value="10"/>
        <property name="exceptionEvent" value="exception"/>
        <property name="dsn" value="xbox"/>
    </properties>

Creating Listeners

Since this is a small application we will create only one listener, which we will call "consoleListener" and will be placed in the "/com" subfolder of our sample application. In order to configure this within the Mach-II XML configuration we only need to add one line to our listeners section as follows:

    <listeners>
        <listener name="consoleListener" type="m2.com.consoleListener" />
    </listeners>

If we tried to run this application right now, we would get an error stating "Could not find the ColdFusion Component or Interface m2.com.consoleListener" even though we have not made any calls to this listener yet. This is because Mach-II creates and caches all of your listeners, filters and plugins when it initializes (a link containing detailed information on the startup and request processes of the Mach-II framework are listed in the references section below). So, let's create the listener...

As stated earlier, there are only two requirements when creating a listener: it must extend "MachII.framework.Listener" and it must contain a configure() method which returns void. In our case, our consoleListener needs to have access to our consoleService component. Our consoleService component is dependent on a number of components, specifically the data access object components (DAO) and gateway components for each of our objects (console, control, accessory and game). What this means in practice is that we will need to create each of these DAOs and gateways and pass them to our service before we can cache it by placing it in the variables scope of our component (note: the this scope within components is unprotected, thus variables within components are generally placed within the variables scope and accessed via accessor methods). Our completed configure method will look like this:

    <cffunction name="configure" access="public" returntype="void" output="false">

        <cfset var dsn = getProperty('dsn') />
        <cfset var consoleDAO = createObject("component",
            "m2.com.xbox.consoleDAO").init(dsn) />
        <cfset var consoleGateway = createObject("component",
            "m2.com.xbox.consoleGateway").init(dsn) />

        <cfset var controlDAO = createObject("component",
            "m2.com.xbox.controls.controlDAO").init(dsn) />
        <cfset var controlGateway = createObject("component",
            "m2.com.xbox.controls.controlGateway").init(dsn) />

        <cfset var accessoryDAO = createObject("component",
            "m2.com.xbox.accessories.accessoryDAO").init(dsn) />
        <cfset var accessoryGateway = createObject("component",
            "m2.com.xbox.accessories.accessoryGateway").init(dsn) />

        <cfset var gameDAO = createObject("component",
            "m2.com.xbox.games.gameDAO").init(dsn) />
        <cfset var gameGateway = createObject("component",
            "m2.com.xbox.games.gameGateway").init(dsn) />

        <cfset variables.consoleService = createObject("component",
                "m2.com.xbox.consoleService").init(consoleDAO, consoleGateway,
                controlDAO, controlGateway, accessoryDAO, accessoryGateway,
                gameDAO, gameGateway) />
    </cffunction>

If this code seems long and burdensome for handling a single service with dependencies, you then understand why developing with ColdSpring is such a relief. To achieve the same result using ColdSpring, your listener would look like this:

    <cffunction name="configure" access="public" returntype="void" output="false">
        <!--- This configure does not do anything yet --->
    <cffunction>

    <cffunction name="setConsoleService" access="public" returntype="void" output="false">
        <cfargument name="consoleService" type="m2.com.consoleService" required="true" />
        <cfset variables.consoleService = arguments.consoleService />
    </cffunction>

As you can see this is a lot cleaner and easier to maintain. Unfortunately, a full tutorial on ColdSpring is beyond the scope of this document. Feel free to refer to part 2 in my Objects and Composition in ColdFusion Series in the references below for a through tutorial on using ColdSpring. Next let's look at some example implementations of actual methods.

Building an Event

Our home page essentially needs just a fully composed copy of our "console" object that represents our Xbox console. To supply this information, we are going to call the getConsole() method on our listener which will check for the existence of a consoleID in the event scope (i.e. supplied via the URL in this case) and call the getConsole() method of our service. The returned console will be passed the the appropriate view, which in this case is a form, and then this view will be placed within our layout. To start with let's look at the XML required to accomplish this within the Mach-II configuration file:

    <event-handler event="home" access="public">
        <notify listener="consoleListener" method="getConsole" resultArg="console" />
        <view-page name="addConsole" contentArg="content" />
        <announce event="layout" copyEventArgs="true" />
    </event-handler>

The first line inside the event handler calls the getConsole method of the consoleListener and saves the result within the event bean under the name "console". The next line calls the view for the form and saves the output to a variable, "content." Finally, the last line announces another event and copies all the arguments within the current event. The other event, "layout", is simply a single view in this case but in a more complete application might include any other processes or views needed to complete the page layout.

Looking at the getConsole method within our listener, you will notice it is pretty simple as its primary function is to serve as a means to connect our controller (i.e. Mach-II) to our model (in this case via our service layer). Everything from the service to the DAO and gateway remain identical to the no framework example cited previously.

    <cffunction name="getConsole" access="public" returntype="any" output="false">
        <cfargument name="event" type="MachII.framework.Event" required="true" />

        <cfset var consoleID = event.getArg("consoleID") />

        <!--- if no id is defined, just get at empty console with a new id --->
        <cfif not len(consoleID)>
            <cfset consoleID = createUUID() />
        </cfif>

        <cfreturn variables.consoleService.getConsole(consoleID) />
    </cffunction>

Adding a Filter

As stated previously, filters can be selectively applied to events and contain code that determines whether the event should continue processing or another event should be announced. In our example, we are going to use this ability to add form validation to our input forms. For purposes of this article, we will look at the filter that validates the "game" input form (for a deeper discussion of where your form validation should go and alternative approaches to the one presented, refer to the article in the references).

Beyond the standard configure() method, a filter is required to have a filterEvent() method that returns a boolean indicating whether the current event should continue processing. It accepts two arguments, being the event bean and the eventContext, which is the component that handles event execution. In the below gameValidation.cfc filter method you will see that the form fields are held within the event bean, and we are just validating the required field (i.e. gameName) as well as defaulting a value for the boolean special edition indicator. If any error is found, we are appending an error message to an array that is sent back to the "failed" event that is announced when the validation fails. Within our Mach-II configuration for this event, the "failed" event is mapped to the "games.add" event. Let's look at the Mach-II configuration:

    <event-handler event="games.process" access="public">
        <event-mapping event="success" mapping="home" />
        <event-mapping event="failed" mapping="games.add" />
        <filter name="gameValidation" />

        <event-bean name="game" type="m2.com.xbox.games.game" />
        <notify listener="consoleListener" method="saveGame" />
    </event-handler>

A couple of notes on this event handler, you will see two event mappings indicating whether the form processing succeeded or failed. If processing succeeded, the "home" event is announced, effectively sending the user back to the home page. If processing fails, the user is returned to the games form (the "games.add" event) with the errors from our filter. The event-bean is a shortcut for populating our bean component for game with the relevant values from the form. The event itself only calls the gameValidation filter and the saveGame method and does not contain any views as the user will be redirected as necessary.

The filter's filterEvent method is fairly simple. It simply sets a boolean value (proceed) to return, validates the form data in the event bean and finally announces the "failed" event if the validation fails, appending the errors array to the event bean. Here's the code:

    <cffunction name="filterEvent" access="public" output="false" returntype="boolean">
        <cfargument name="event" type="MachII.framework.Event" required="true" />
        <cfargument name="eventContext" type="MachII.framework.EventContext" required="true" />

        <cfset var proceed = true />
        <cfset var errors = arrayNew(1) />

        <cfif not len(trim(event.getArg("gameName")))>
            <cfset arrayAppend(errors,"You must enter a game name") />
        </cfif>

        <cfif not len(trim(event.getArg("specialEdition")))>
            <cfset event.setArg("specialEdition",0) />
        </cfif>

        <cfif arrayLen(errors)>
            <cfset proceed = false />
            <cfset event.setArg("errors",errors) />
            <cfset announceEvent("failed", arguments.event.getArgs()) />
        </cfif>

        <cfreturn proceed />
    </cffunction>

Extending with Plugins

Before getting into the details of my plugin, I'd like to give a brief background on what it is trying to do. As anyone with software development experience knows, there are any number of ways to approach a given problem when architecting your application. In the case of my sample application, I wanted you to be able to do things like add "games" to a "console" that isn't yet persisted to the database. Finally, when your "console" is fully composed, it would be saved along with the relevant relationships (i.e. games, accessories, controllers). In order to do this, I decided to create a plugin to handle setting and clearing my current "console" object within the session scope. This approach also allows me to show you an example of a plugin that implements multiple plugin points. The point here is that the point here is not to debate the approach but rather to examine the specifics of creating a Mach-II plugin.

Within the Mach-II configuration XML, you only need to register your plugin within theplugins section of the file. You do not need to specify a plugin within your event handler because every plugin is run on every request at the specified plugin points. The exact plugin points you wish a plugin to run on are defined within the plugin component itself and not within the configuration definition.

    <plugins>
        <plugin name="sessionPlugin" type="m2.plugins.session" />
    </plugins>

As you probably could guess, your plugin does indeed require a configure() method, though is not required to contain any code. In the case of my "session" plugin, I will be implementing the preEvent and preView plugin points by creating functions with those names. Each of the functions accepts the eventContext as its only argument. You can access the event bean within a plugin function via the eventContext, however you need to consider the point within the processing that the specific plugin is called. For instance, when using a preProcess plugin point, you would access the first event within the queue by calling eventContext.getNextEvent() method since no event is actually in process yet. However, within the preEvent or preView plugin points in our example, for instance, an event is already processing, meaning you would use the eventContext.getCurrentEvent() method.

In the following example, my preEvent method simply looks for a "console" object and if it doesn't already exist within the event bean, it adds it. It does the same for consoleID just as a shortcut. The preView method simply looks for a "console" defined in the current event, and if one doesn't exist within the session scope, it adds it to the current session. It also provides a way to clear the console from the session by supplying a "clearConsole" argument within the event.

    <cffunction name="preEvent" access="public" returntype="void" output="false">
        <cfargument name="eventContext" type="MachII.framework.EventContext" required="true" />
        <cfset var event = arguments.eventContext.getCurrentEvent() />

        <cfif structKeyExists(session,"console") and not event.isArgDefined('consoleID')>
            <cfset event.setArg('consoleID',session.console.getConsoleID()) />
        </cfif>

        <cfif structKeyExists(session,"console") and not event.isArgDefined('console')>
            <cfset event.setArg('console',session.console) />
        </cfif>
    </cffunction>

    <cffunction name="preView" access="public" returntype="void" output="false">
        <cfargument name="eventContext" type="MachII.framework.EventContext" required="true" />

        <cfset var event = arguments.eventContext.getCurrentEvent() />

        <cfif event.isArgDefined('console') and (not structKeyExists(session,"console")
            or event.isArgDefined('clearConsole'))>
            <cfset session.console = event.getArg('console') />
        </cfif>
    </cffunction>

Moving Forward

This tutorial covered everything you need to get started building a complex web application with Mach-II for ColdFusion. However, with the recent introduction of Mach-II version 1.5 a number of new features are now available which were not covered. These include:

  • Organizing with Subroutines
  • Using Includes
  • Integrating Modules

You can refer to the references section below for more information on these new features and I hope to include these in a follow-up tutorial in the near future.

References

Objects and Composition in ColdFusion Series

General Thoughts

Attachments

  • xbox1.zip (3.1 MB) - added by peterfarrell “Code for this guide.”

Anatomy of Mach-II Series

Clone this wiki locally