Anatomy of a Framework - Mach-II - Part I
Posted on Aug 09, 2007
To many of us who use frameworks with ColdFusion, and obviously all those who don't, the framework can be a bit of a black box. We may know how to configure it using the XML and may even have looked inside a file or two at one point, but we generally don't know what is going on behind the scenes. On the premise that we often fear that which we don't fully understand, I thought it might be an interesting series to actually dissect the various ColdFusion frameworks bit-by-bit. The goal here is to give those of us who use the framework a better understanding of how it functions and those of us who don't use frameworks some insight into what a framework actually does.
To start this series off, I decided to dissect Mach-II because it is a framework I use (I hope to do the same to Model-Glue, Fusebox and ColdBox in the future if there is interest in this series). Mach-II is the oldest of the object-oriented frameworks for ColdFusion and has recently released a beta of version 1.5 (I am actually basing this post on the beta1 of 1.5). This new version added a number of really important features including includes, modules and subroutines, and you will see those referenced frequently in this discussion. In this first edition, I will examine what Mach-II does when it initializes, which is a lot actually. So, let's get digging.The Application Start Process
If you are using the Application.cfc bootstrapper, then you will see that from within OnApplicationStart() it calls a LoadFramework() method that it contains via inheritance as it extends MachII.mach-ii.cfc. We are going to take a close look at what happens in this call to better understand Mach II's start-up processes. It is important to note that prior to the OnApplicationStart() method (and outside of any method) the CFC sets a number of Mach II specific variables that will seem familiar if you previously used the Application.cfm bootstrapper.
The LoadFramework method of mach-ii.cfc is only a few lines long. For the most part it creates the appLoader (/MachII/framework/AppLoader.cfc) and then passes off responsibility to it via the constructor (ok, so init() is not a true constructor but for the purposes of this document I will call it that from here on out since that is its intent). For it's part the AppLoader creates the AppFactory, whose constructor simply returns itself.
After setting some application properties, the AppLoader calls an internal method called reloadConfig() which is where we begin to get at the guts of the start-up process. Within the setAppManager() call, you will note that the AppFactory's createAppManager() method is called. This method is where the configuration files are read, parsed and all the appropriate items loaded.
Essentially, the first thing this method does is to try to read your base configuration xml file:
<!--- Read the XML configuration file. ---> <cftry> <cffile action="READ" file="#arguments.configXmlPath#" variable="configXmlFile" /> <cfcatch type="any"> <cfthrow type="MachII.framework.CannotFindBaseConfigFile" message="Unable to find the base config file." detail="configPath=#arguments.configXmlPath#" /> </cfcatch> </cftry>
It then validates the XML against the DTD if you have the optional DTD validation turned on in your Application.cfc. Finally, it creates an array of configuration files and adds the base configuration file to this array - remember that in 1.5 you can have additional configuration files in the format of includes and modules. In the following steps, which I discuss in the order they are processed, the framework loads up each of the elements that make up a Mach-II application.
Includes are a new means of including a separate Mach-II XML configuration file, as you may have guessed. This allows you to separate some logical portions of your application and keep your configuration files more maintainable. With regard to the application startup, the next process that occurs is that the loadIncludes() method is called, which does exactly what you expect it to. It appends each include defined within your XML configuration and adds it to the array. However, since includes can contain includes, this is a recursive function.
The following step creates the actual AppManager.cfc object and sets the appKey property of it. It then, based upon the arguments passed to the CreateAppManager() method determines if this has a parent AppManager. This code is also for handling modules, which have their own appManager as a child of the main application's AppManager.
Next the Utils.cfc is loaded, which currently only has two functions within it. According to Peter Farrell, lead developer for the Mach-II project, this class was added since several managers in Mach-II need these functions and it is not best practice to just copy and paste the function inside the required managers. As the framework grows, I suspect that this Utils.cfc will become more widely utilized.
Properties are essentially a bunch of name value keys that you generally place at the beginning of your Mach-II configuration file. An important change in Mach-II 1.5 is that these properties can also be complex data types like arrays and structures. Also, using the new Property.cfc, you can configure a property that are actually CFC instances. This is all handled by the propertyManager which is loaded and populated with the property element data.
After loading in all the properties, the framework loads a requestManager singleton which I assume we will see more of in the next part of this series where I dissect the request process. The next set of processes, while still within the CreateAppManager() method, all follow a similar set of steps and get at the core of what Mach-II is about which is managing calls to model and passing that data to the views. Obviously the model, in this case, is simply a set of CFCs and you can communicate with via either listeners, filters or plugins and that data is passed to the views, which are standard CFML templates.
Listeners are the primary means of communicating between Mach II and your service layer. Generally speaking, a Mach-II event will trigger a number of listener calls (via implicit invocation) which process the data needed to handle the request. Listeners are simply specialized CFCs that extend MachII.framework.Listener and are required to have a configure() method. For the most part, functions within your listener simply accept the Mach II event object as the argument. The event object contains any user input (i.e. URL and form variables) as well as any data placed there by prior listener calls, filters or plugins.
Once the framework initializes its ListenerManager.cfc it then parses out all the listeners from each of your XML configuration files. Next it instantiates your listener CFC passing in any parameters you may have defined. The framework also instantiates the invoker defined for each listener, which is generally just the default (i.e. it isn't defined). The invoker is just a specialized CFC with an InvokeListener() method that handles calling the listener methods. Each of these instantiated CFC are added to a structure keyed on the listener's name which you defined in the XML.
In Mach-II a filter is another specialized CFC extending MachII.framework.EventFilter that can be added to an event definition and, in simple terms, either allows the event to continue processing if certain conditions are met, or announces a different event if they are not. This concept is typically referred to as flow control. An example would be checking to see if a user is logged in, and, if not, redirecting the user to the log in event.
Within the framework startup process, filters are loaded in the same basic manner as listeners. The EventFilterManager.cfc checks your defined configuration files for filter nodes, then loops through and initializes each filter component, passing in any parameters defined in the XML. Each instantiated filter component is then added to a structure keyed on the filters name.
Subroutines are yet another new feature in Mach-II 1.5. Essentially, subroutines function like reusable snippets within the XML configuration that are executed as soon as they are encountered. Let's say you have several events within your configuration that have a common subset of actions. In Mach-II 1.5, you can move this snippet to a subroutine definition and thereby not have to duplicate the code within each event. However, subroutines cannot be called via the URL like events can.
At its core, subroutines are handled in the the same way as listeners and filters in that the SubroutineManager.cfc loops through the subroutine nodes and populates a structure of subroutines keyed on the subroutine name. However, in this case a subroutine is made up of a sequence of commands based upon the type of node within the subroutine (i.e. a filter, listener, view, etc.) and are kept within a subroutine handler. It is this SubroutineHandler.cfc instance that the structure of subroutines contains.
As I am sure you understand by now, events define the core of how Mach-II handles a request. Each request is actually defined by an event announcement and the event defines the filters, listeners, views and other elements that make up the application's response to this event. Much like subroutines then, an event is simply a sequence of commands and it is thereby handled in a nearly identical manner to subroutines.
Views are how Mach-II assembles the user-interface and they are fairly standard CFML templates. The main difference between a standard CFML template and a Mach-II view is that the Mach II view shouldn't have any application logic on it. It simply knows what variables it should be getting and handling the display of the data to the user. Since views simply generate output that Mach-II needs to capture, their handling within the application startup is pretty straightforward. The viewManager.cfc simply creates a structure keyed on the view name that contains the page nodes attributes structure, which is generally just the path of the template.
A plugin is another Mach-II specialized CFC type that extends MachII.framework.Plugin and runs on every request at a specified plug-in point. Allowable plug-in points include: preProcess, postProcess, preEvent, postEvent, preView, postView and handleException. I think the names of the plug-in points make it pretty self-explanatory when it would get called within the request process. Plugins allow you to easily apply cross-cutting application logic that is defined in a single location in the application.
Within the Mach-II framework code, plugins are handled much like the listeners and filters whereby any plugins you define are initialized with any related parameters and kept in an structure. In this case, however, because of the variety of plug-in points, there are several structures and arrays. I am actually going to gloss over several structures and arrays created here so as not to make this more confusing than it needs to be, but effectively there is a separate structure for each plug-in point containing the initialized plugin CFC that implements that plug-in point.
Modules are a major addition to Mach-II in version 1.5. Modules are similar to includes in that they are an independent XML configuration that can be included into the primary configuration file. However, modules have some unique features that are specifically tailored towards including third party applications without actually needing to modify the included applications XML configuration. Within a module definition, you can override the properties, listeners, views, plugins, etc. of the module application without touching the module XML configuration. This makes upgrading a module application, for example MachBlog, a simple process.
The framework loads modules into module.cfc instances that contain the name of the module, the location of the XML configuration file and whether this configuration overrides the parent configuration or not (for instance if element names overlap). The module configuration isn't actually loaded at this point but is loaded during the configure process. This is because modules are child applications of the parent -- the parent application needs to be completely loaded before a module can start loading. However, the loading of a module is no different than the parent except that instead of using the Application.cfc as the bootstrapper, the parent AppManager begins the process.
One thing you will notice is that just about every CFC in Mach-II is required to have a configure method. The configure method handles processing that occurs post-initialization but must be done in order for the component to be used. I am not certain where this convention comes from, but I believe it is tied to the fact that each of these components extends a base component that already has an init() function. Rather than override the parent's init function, configure() is used to create a secondary initialization process. Regardless of the reason, the last thing the createAppManager() function does is call the appManager's configure function.
The appManager's configure function simply runs through all the different manager's created above and runs their respective configure() methods in the same order they were created above. Since this process if run for every one of the application's defined configuration files, the module manager is only configured if this appManager has a parent appManager.
What We've Learned
For me, going through this process has added some additional light to the nature of each of the Mach-II framework elements like listeners, plugins and filters for example. It has also shown that a great deal of work and a significant amount of changes went into the 1.5 update as much of the code discussed touched on new elements and the multiple configuration file aspect of includes and modules affected nearly every other aspect of the code.
Most importantly, I think you can take away is that, not to make light of the complexity of the framework code, but there is nothing magical going on underneath the covers. In nearly every case, all Mach-II does is instantiate a collection of components that are then cached to allow them to be called at the appropriate time in the request process. For those of you who are not framework users, you probably do something similar since this is a necessary element of component based development but you don't XML configure it and perhaps you don't even call it a framework (even if you follow a standardized configuration somehow). Speaking of the request process, look for part two soon where I will dissect that process within the Mach-II framework code.
Please comment if you find this useful as this was a lot of work and I will gauge interest in dissecting future frameworks on the response to this post and the subsequent one.
This is a REALLY great resource - please keep it up. You should probably have a word with Matt and Pete about this being added to the docs or at least a link. It never huryts to have more documentation and this kind of walk through is ideal to get people up to speed with what the framework is doing.
It's also great that it includes 1.5 as there are a bunch of cool new features and it's nice to see them in context.
Posted By Peter Bell / Posted on 08/09/2007 at 5:19 AM
Hey Brian - this was _really_ useful. I've been using Mach-II since 1.0.4 and I've never had the time to really dig in to the framework and see what was happening under the hood. Your article explained so much so clearly that I really feel like I don't need to do that exploration myself. =) Thanks for the great details and this great time-saver!
Posted By Brian Klaas / Posted on 08/09/2007 at 6:40 AM
Posted By Dan Wilson / Posted on 08/09/2007 at 6:51 AM
Posted By Peter Bell / Posted on 08/09/2007 at 7:08 AM
Posted By Peter J. Farrell / Posted on 08/09/2007 at 8:28 AM
Posted By Tariq / Posted on 08/09/2007 at 10:05 AM
Well done indeed, it's posts like these which do all the more to increase interest in not only Mach-II but frameworks in general.
Keep going when you have the time! The readership will only grow :)
Posted By Michael Sharman / Posted on 08/09/2007 at 2:48 PM
Fantastic work! Being in the midst of my own series of framework related posts, I can sympathize.
Keep 'em coming! At least your off the hook for writing about that little known event delegation package called Dispatcher... ;)
Posted By Paul Marcotte / Posted on 08/13/2007 at 1:51 PM
Posted By Randolph / Posted on 08/28/2007 at 2:03 PM
thanks, what a great post. I have been pulling together machII info to help some other developers get up to spreed with things. This kind of depth is super. I have referenced it from our machII space.
Posted By james / Posted on 06/13/2008 at 9:53 AM
Posted By Sonny / Posted on 10/06/2008 at 10:08 AM
Posted By George Murphy / Posted on 10/31/2008 at 9:57 AM