Welcome back to our blog series on converting your legacy FuseBox apps over to ColdBox MVC. In our last post we converted the sample app for FuseBox 3 to ColdBox. In this post we'll be taking the FuseBox 5.1 sample app from fusebox.org and converting it over to ColdBox MVC. This is a later version of FuseBox that relies on XML controllers so it may be a bit more familiar. Let's get started!
- The finished ColdBox code is here: https://github.com/bdw429s/FuseBox-5.1-Sample-App-Conversion
- The original Fusebox code is in the root of the above repo in a zip called "getmymail.zip"
To run the FuseBox sample app, I downloaded the FuseBox 5.1 core files from fusebox.org. I had issues getting the code to run on Railo 4.2 due to component creation paths and relative file directories not agreeing with the framework so I switched over to ColdFusion 9. The ColdBox code, however, will run on CF9+ or Railo. I had to put in a some code to handle Adobe and Railo's different script implementations, but I'll cover that later.
Overview
This sample app is called "Get My Mail' and is a simple POP3 client that will list out all the messages in your inbox. It only has three screens-- the connection information form, the E-mail list, and viewing a single message. To use the app, you'll need to have the server and login info for a POP3 server handy. I'll also note that I wrote the ColdBox code all in CFScript except for views and layouts, of course. This is simply my preference. You are free to use tags with the ColdBox Platform.
Here's a screenshot of the files in the Fusebox and ColdBox code side-by-side. There are fewer ColdBox files, but not quite as few as the image might imply. Several of the files on the FuseBox side are auto-generated or empty. I'll cover each of them as we go.
FuseBox 5.1 has no inherent MVC patterns per se, but this sample app was structured with MVC in mind which help the conversion. The controller and model folders are basically FuseBox circuits that roughly represent those breakdowns. The first thing you might notice is that ColdBox has NO XML! We believe CFML programmers should use what they're most familiar with to describe their app-- CFML! The parsed folder in FuseBox is where .cfm files are automatically generated by the framework from the XML controllers. Since ColdBox has you write your code in CFML, there's no need for an equivalent. You'll also notice that the FuseBox "models" are just .cfm files. I went ahead to moved the equivalent code into an actual CFC in the ColdBox example.
circuit.dtd and fusebox.dtd are just the XML definition for each of the circuits and the fusebox.xml file. Naturally there is no equivalent file in ColdBox. If you want to know what options you have in a ColdBox handler or config file, the easiest thing to do would be look in the docs, or use one of our application templates as an example. Handler Docs Config Docs
fusebox.init.cfm is included at the start of each FuseBox request. One way to do that in ColdBox would be to register a requestStartHandler setting in /config/ColdBox.cfc to run before every request. Another way would be to register a preProcess interceptor. Since the fusebox.init.cfm file in this sample app doesn't do anything, I didn't bother creating an equivalent in the ColdBox app.
fusebox.appinit.cfm is included every time the framework reloads. Again, this can be easily duplicated in ColdBox by registering an applicationStartHandler in the config file. There are also several interception points you can listen to as well.
Core Framework
The FuseBox framework is self-contained within the fusebox5 folder. The ColdBox framework is in the coldbox folder. The easiest place for the coldbox folder to live is in your web root so CF will automatically resolve CFC paths that start with "coldbox" there. For the security-minded, you can also move the folder elsewhere and create a server or app-specific mapping that points to it. This is especially handy if you have multiple ColdBox site and want to only maintain one copy of the framework.
Bootstrap
ColdBox bootstraps itself up by being the super class to the Application.cfc and leeching off the application and request lifecycle methods within.
component extends='coldbox.system.Coldbox' { this.name = 'FuseBoxSampleConversion'; this.sessionmanagement = "true"; }
FuseBox on the other hand has a bit of code in the index.cfm file that invokes the framework. It's worth noting the ColdBox app has an index.cfm, but is a blank placeholder just there to satisfy the CF engine.
<cfinclude template="fusebox5/fusebox5.cfm" />
The FuseBox app has a CFApplication tag in the index.cfm, but since the ColdBox app uses Application.cfc, that information is stored there. If you ever need to add methods to Application.cfc, make sure you continue to call the super-scoped method as well. If you want to use an app-specific mapping for "/coldbox", you'll need to use our non-inheritance template. Read more about this here.
Settings
The Fusebox app uses a fusebox.xml.cfm file that contains settings for the app. In ColdBox, we have a "/config" convention folder where we place all our configuration. The ColdBox.cfc file contains general configuration for the app. The WireBox.cfc file is optional but we used it to map a special model for Railo that I'll talk about under Models. The only requirements of /config/ColdBox.cfc is that it contains a method called configure() and defines a variable called coldbox.appName. There are a number of optional settings you can define here as well additional methods for environment control that override settings for you automatically on your development or staging servers. You can read about this here.
fusebox.xml.cfm
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE fusebox SYSTEM "fusebox.dtd"> <fusebox> <circuits> <circuit alias="m" path="model" /> <circuit alias="v" path="view"/> <circuit alias="mail" path="controller"/> </circuits> <parameters> <parameter name="fuseactionVariable" value="view" /> <parameter name="defaultFuseaction" value="mail.home" /> <parameter name="precedenceFormOrUrl" value="form"/> <parameter name="debug" value="true"/> <parameter name="mode" value="development-full-load"/> <parameter name="password" value=""/> <parameter name="parseWithComments" value="true" /> <parameter name="conditionalParse" value="true" /> <parameter name="scriptlanguage" value="cfmx" /> <parameter name="scriptFileDelimiter" value="cfm"/> <parameter name="maskedFileDelimiters" value="htm,cfm,cfml,php,php4,asp,aspx"/> <parameter name="characterEncoding" value="utf-8"/> </parameters> <globalfuseactions> <appinit /> <preprocess> <do action="m.setup"/> </preprocess> <postprocess /> </globalfuseactions> </fusebox>
/config/ColdBox.cfc
component { function configure(){ coldbox = { appName = "getMyMail", reinitPassword = "", defaultEvent = "mail.home", handlersIndexAutoReload = true, debugMode = true }; settings = { attachmentPath = expandPath('/attachments/') }; } }
In fusebox.xml.cfm, the <circuits> defines the main areas of the application: "m" (model), "v" (views), and "mail" (controller). ColdBox handles each of these by convention so there's not need for configuration. The ColdBox models will all go in the "/model" folder, the views in the "/views" folder, and the controllers in the "/handlers" folder.
The defaultFuseaction setting is done in ColdBox with the defaultEvent setting. I've created a "mail" handler with a "home" action, so the value is still "mail.home". The debug setting becomes debugMode and pretty much as the same purpose. Additional framework-specific debugging information is appended to the end of every request. Password becomes reinitPassword and is required to reload the application from the URL with ?fwreinit=password (or ?fwreinit=1 if no password is set). On your production servers you'll want to specify a password to use in the URL so only you can reload your site. Mode set to development-full-load is partially covered by handlersIndexAutoReload which reloads the handlers on each request. The rest of the settings don't apply to ColdBox or don't appear to be used.
The <globalfuseactions> and <preprocess> tags specify a fuseaction that is run before each request. This can be done with a requestStartHandler setting in /config/ColdBox.cfc or a preProcess interceptor. In this app, there is only one handler, so I put that logic in a preHandler method in my mail handler which the framework automatically runs before each action in mail. You'll see I also created a setting for the location of the attachments folder which I'll inject into my model later.
Circuits and Handlers
Fusebox breaks a site up into circuits, and ColdBox breaks it up via handlers. Circuits are divided into fuseactions, and handlers, are divided into actions. Instead of using an XMl file to define this, ColdBox handlers are defined by convention by a CFC in your /handlers directory. The name of the CFC determines the name of the handler. Actions are simply methods in your CFC, and the names of the methods are the names of the actions. ColdBox also allows for very powerful routes to be defined ad hoc by you to make up nice descriptive URLs that don't have to match your file/method names. Read more about that here.
The Fusebox site has circuits called "m" and "v" but ColdBox treats models and views as first-class conventions we'll cover shortly.
/controller/circuit.xml
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE circuit SYSTEM "../circuit.dtd"> <!-- Controller Circuit --> <circuit access="public"> ... </circuit>
/handlers/Mail.cfc
component { property name='sessionStorage' inject='coldbox:plugin:sessionStorage'; property name='mailService' inject='MailService'; ... }
Our circuit definition is an XML file, but our handler is just a CFC. The name of the handler is derived from the file name by default. The properties in the CFC represent other objects that the code in the handler is dependant upon. When WireBox creates the handler, it will inspect it for any properties containing an inject attribute and inject a reference to them in the variables scope (by default) using the name specified in the name attribute. coldbox:plugin:sessionStorage refers to the sessionStorage plugin which is a wrapper for the session scope. If you want to use the session scope directly, there's NOTHING STOPPING YOU! However, there are benefits to abstracting it. Namely, encapsulating external dependencies, being able to change where the storage plugin points easily (client, cookie, etc) and the ability to mock the session scope access in a unit test. MailService by default will load the MailService.cfc file in our /models directory.
/model/circuit.xml
<fuseaction name="setupXfa"> <set name="xfa.conform" value="mail.connectform" /> <set name="xfa.msglist" value="mail.list" /> <set name="xfa.msgdetail" value="mail.showmessage" /> </fuseaction>
/controller/circuit.xml
<prefuseaction> <!-- This could be a <do /> or an include, but the verbs keep it all right here, easy to see. --> <if condition="myFusebox.thisFuseaction IS 'connectform'"> <true /> <false> <if condition="structKeyExists(session,'connectionInfo') AND structCount(session.connectionInfo) GT 0"> <false> <relocate xfa="conform" /> </false> </if> </false> </if> </prefuseaction>
/handlers/Mail.cfc
// Runs before every action function preHandler( event, action, eventArguments ) { var prc = event.getCollection( private=true ); // Exit event handlers prc.xeh = {}; prc.xeh.conform = 'mail.connectform'; prc.xeh.conformSave = 'mail.connectformSave'; prc.xeh.msglist = 'mail.list'; prc.xeh.msgdetail = 'mail.showmessage'; // If they're on any action OTHER than the connection form and there isn't cached connection info... if( arguments.action != 'connectform' && arguments.action != 'connectformSave' && !sessionStorage.exists( 'connectionInfo' ) ) { // Send them back to the connection form setnextEvent( prc.xeh.conform ); } }
Here we're combining both setupXfa fuseaction in the m circuit as well as the <prefusionaction> tag in the mail circuit into the preHandler method of our mail handler. preHandler is a special method name and will be run by convention before any other action in that handler. Xfa stands for "exit fuseaction" and is basically a list of outgoing paths that the user can navigate away with. Typically, I see this as needless bit of misdirection, but I copied it for old time's sake in a struct called xeh or "exit event handler". The condition statement in the XML file is a clumsy mix of CFML and XML that we replaced in our handler as a concise bit of pure code. Basically, if you're hitting any page other than the connection form and you don't have connection details in your session, redirect back to the session form.
/controller/circuit.xml
<fuseaction name="home"> <do action="list" /> </fuseaction>
/handlers/Mail.cfc
// Default event, just redirects to list. function home( event, rc, prc ){ setNextEvent( prc.xeh.msglist ); }
Here's the home fuseaction and action. All it does is redirect to the list action. Generally this things falls in my "needless misdirection" category as we might as well have just set the default event to mail.list and been done with it. Nevertheless, I included the equivalent in the ColdBox code.
/controller/circuit.xml
<fuseaction name="connectform"> <do action="v.connectform" /> </fuseaction>
/views/setupForm.cfc
<cfparam name="form.serverName" default="" /> <cfparam name="form.serverPort" default="110" /> <cfparam name="form.userName" default="" /> <cfparam name="form.password" default="" /> <cfparam name="form.msgsPerPage" default="30" /> <cfif structKeyExists(session,'connectionInfo') AND structCount(session.connectionInfo) GT 0> <cfset form.serverName = session.connectionInfo.serverName> <cfset form.serverPort = session.connectionInfo.serverPort> <cfset form.userName = session.connectionInfo.userName> <cfset form.password = session.connectionInfo.password> <cfset form.msgsPerPage = session.connectionInfo.msgsPerPage> </cfif>
/handlers/Mail.cfc
function connectform( event, rc, prc ){ // default values rc.serverName = ''; rc.serverPort = '110'; rc.userName = ''; rc.password = ''; rc.msgsPerPage = '30'; // Overwrite with stored values if they exist if( sessionStorage.exists( 'connectionInfo' ) ) { structAppend( rc, sessionStorage.getVar( 'connectionInfo' ) ); } }
I didn't agree with the defaulting of variables in the view, so I moved them into the handler since that's where it feels like they belong. The reason why I don't need to set the view in my action is because I have placed by view file in the appropriate place with the correct name to be automatically run by convention. The FuseBox app ran the code to save the form on every request but I chose to create an additional connectFormSave action to do that.
/controller/circuit.xml
<fuseaction name="list"> <set name="attributes.start" value="1" overwrite="false" /> <set name="attributes.listVarName" value="qMailList" /> <do action="m.getListOfEmails" /> <do action="v.listmail" /> </fuseaction>
/handlers/Mail.cfc
function list( event, rc, prc ){ // Param start to 1 event.paramValue( 'start', 1 ); // The view needs this for previous/next links prc.msgsPerPage = sessionStorage.getVar( 'connectionInfo' ).msgsPerPage; // Get E-mails prc.emails = mailService.getAllMessages( rc.start ); }
And here's our list action. You can see the set tag for attributes.start is replaced with event.paramValue(). We don't need to be as explicit about the name of the variable we're passing to the view. We just place the results of our service call into the prc scope (private request collection) and it will automatically be available in the view. Again, our view will be called by convention.
I won't show the remainder of the handler since it's pretty well covered already. You can check out the code in the repo to read the entire file yourself.
But wait, there's more!
We've split this sample app conversion into two halves so you have time to soak each part in. Here's the other half where we cover views, models, and error handling. And of course, you can see all the completed code here on GitHub: https://github.com/bdw429s/FuseBox-5.1-Sample-App-Conversion
Please comment below if you have any questions or suggestions, and as usual, here's our list of handy resources:
- Check out our bite-sized PDF reference cards here: http://wiki.coldbox.org/wiki/Dashboard.cfm#PDF_Ref_Cards
- Play with the framework right inside your browser with these samples: http://runnable.com/ColdBox
- Join our Google Group. We're friendly, active, and love to help with ANY kind of CF question: https://groups.google.com/forum/?hl=en#!forum/coldbox
- Check out our video library of webinars and presentations: http://www.coldbox.org/media
- Look up answers in our comprehensive docs: http://wiki.coldbox.org/
Add Your Comment