Skip to main content
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Kevin Schmidt
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Kevin Schmidt

My ColdFusion "Controller" Layer Is Just A Bunch Of Switch Statements And CFIncludes

By
Published in Comments (8)

The more experience I get, the more I appreciate using an appropriate amount of complexity when solving a problem. This is a big part of why I love ColdFusion so much: it allows one to easily scale-up in complexity if and when the requirements grow to warrant it. When I'm working on my own, I don't need a robust framework with all the bells-and-whistles. All I need is a simple dependency-injection strategy and a series of CFSwtich and CFInclude statements.

I wanted to write this post, in part, in response to a conversation that I saw taking place in our Working Code Podcast Discord chat. I wasn't following the chat closely; but, I detected some light jabbing at the CFInclude tag in ColdFusion. I sensed (?perhaps incorrectly?) that it was being denigrated as a beginner's construct - not to be used by serious developers.

Nothing could be farther from the truth. As a ColdFusion developer with nearly 25-years of CFML experience, I can attest that I use - and get much value from - the CFInclude tag every day.

To be clear, I am not advocating against frameworks. Frameworks can be wonderful, especially when you're working with larger teams. But, simpler contexts beg for simpler solutions.

And, when I started building Dig Deep Fitness, my ColdFusion fitness tracker, I wanted to build the simplest possible thing first. As Kent Beck (and others) have said: Make it work, then make it right, then make it fast.

In Dig Deep Fitness, the routing control flow is dictated by an event value that is provided with the request. The event is just a simple, dot-delimited list in which each list item maps to one of the nested switch statements. The onRequestStart() event-handler in my Application.cfc parameterizes this value to setup the request processing:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	// Define the application settings.
	this.name = "DigDeepFitnessApp";
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;

	// ... truncated code ....

	/**
	* I get called once to initialize the request.
	*/
	public void function onRequestStart() {

		// Create a unified container for all of the data submitted by the user. This will
		// make it easier to access data when a workflow might deliver the data initially
		// in the URL scope and then subsequently in the FORM scope.
		request.context = structNew()
			.append( url )
			.append( form )
		;

		// Param the action variable. This will be a dot-delimited action string of what
		// to process.
		param name="request.context.event" type="string" default="";

		request.event = request.context.event.listToArray( "." );
		request.ioc = application.ioc;

	}

}

As you can see, the request.context.event string is parsed into a request.event array. The values within this array are then read, validated, and consumed as the top-down request processing takes place, passing through a series of nested CFSwitch and CFInclude tags.

The root index.cfm of my ColdFusion application sets up this first switch statement. It also handles the initialization and subsequent rendering of the layout. As such, its switch statement is a bit more robust than any of the nested switch statements.

Ultimately, the goal of each control-flow layer is to aggregate all of the data needed for the designated layout template. Some data - like statusCode and statusText - is shared universally across all layouts. Other data-points are layout-specific. I initialize all of the universal template properties in this root index.cfm file.

<cfscript>

	config = request.ioc.get( "config" );
	errorService = request.ioc.get( "lib.ErrorService" );
	logger = request.ioc.get( "lib.logger.Logger" );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	request.template = {
		type: "internal",
		statusCode: 200,
		statusText: "OK",
		title: "Dig Deep Fitness",
		assetVersion: "2023.07.22.09.54", // Making the input borders more intense.
		bugsnagApiKey: config.bugsnag.client.apiKey
	};

	try {

		param name="request.event[ 1 ]" type="string" default="home";

		switch ( request.event[ 1 ] ) {
			case "auth":
				include "./views/auth/index.cfm";
			break;
			case "exercises":
				include "./views/exercises/index.cfm";
			break;
			case "home":
				include "./views/home/index.cfm";
			break;
			case "jointBalance":
				include "./views/joint_balance/index.cfm";
			break;
			case "journal":
				include "./views/journal/index.cfm";
			break;
			case "security":
				include "./views/security/index.cfm";
			break;
			case "system":
				include "./views/system/index.cfm";
			break;
			case "workout":
				include "./views/workout/index.cfm";
			break;
			case "workoutStreak":
				include "./views/workout_streak/index.cfm";
			break;
			default:
				throw(
					type = "App.Routing.InvalidEvent",
					message = "Unknown routing event: root."
				);
			break;
		}

		// Now that we have executed the page, let's include the appropriate rendering
		// template.
		switch ( request.template.type ) {
			case "auth":
				include "./layouts/auth.cfm";
			break;
			case "blank":
				include "./layouts/blank.cfm";
			break;
			case "internal":
				include "./layouts/internal.cfm";
			break;
			case "system":
				include "./layouts/system.cfm";
			break;
		}

	// NOTE: Since this try/catch is happening in the index file, we know that the
	// application has, at the very least, successfully bootstrapped and that we have
	// access to all the application-scoped services.
	} catch ( any error ) {

		logger.logException( error );
		errorResponse = errorService.getResponse( error );

		request.template.type = "error";
		request.template.statusCode = errorResponse.statusCode;
		request.template.statusText = errorResponse.statusText;
		request.template.title = errorResponse.title;
		request.template.message = errorResponse.message;

		include "./layouts/error.cfm";

		if ( ! config.isLive ) {

			writeDump( error );
			abort;

		}

	}

</cfscript>

While the root index.cfm is more robust than any of the others, it sets-up the pattern for the rest. You will see that every single control-flow file has the same basic ingredients. First, it parameterizes the next relevant request.event index:

<cfscript>

	// In the root controller, we care about the FIRST index.
	param name="request.event[ 1 ]" type="string" default="home";

</cfscript>

Then, once the request.event has been defaulted, we figure out which controller to include using a simple switch statement on the parameterized event value:

<cfscript>

	switch ( request.event[ 1 ] ) {
		case "auth":
			include "./views/auth/index.cfm";
		break;
		case "exercises":
			include "./views/exercises/index.cfm";
		break;

		// ... truncated code ...

		case "workoutStreak":
			include "./views/workout_streak/index.cfm";
		break;
		default:
			throw(
				type = "App.Routing.InvalidEvent",
				message = "Unknown routing event: root."
			);
		break;
	}

</cfscript>

Notice that each of the case statements just turns around and CFInclude's a nested controller's index.cfm file. All of the nested index.cfm files look similar, albeit much less complex. Let's take, as an example, the auth controller:

<cfscript>

	// Every page in the auth subsystem will use the auth template. This is exclusively a
	// non-logged-in part of the application and will have a simplified UI.
	request.template.type = "auth";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	param name="request.event[ 2 ]" type="string" default="requestLogin";

	switch ( request.event[ 2 ] ) {
		case "loginRequested":
			include "./login_requested.cfm";
		break;
		case "logout":
			include "./logout.cfm";
		break;
		case "requestLogin":
			include "./request_login.cfm";
		break;
		case "verifyLogin":
			include "./verify_login.cfm";
		break;
		default:
			throw(
				type = "App.Routing.Auth.InvalidEvent",
				message = "Unknown routing event: auth."
			);
		break;
	}

</cfscript>

As you can see, this controller looks very similar to the root controller. Only, instead of parameterizing and processing request.event[1], it uses request.event[2] - the next index item in the event-list. Notice, also, that this controller overrides the request.template.type value. This will cause the root controller to render a different layout template.

This "auth" controller doesn't need to turn around and route to any nested controllers; although, it certainly could - when you have simple switch and include statements, it's just controllers all the way down. Instead, this "auth" controller needs to start executing some actions. As such, its case statements include local action files.

Each action file processes an action and then includes a view rendering. Some action files are very simple; and, some action files are a bit more complex. Let's look at the "request login" action file in this "auth" controller.

The goal of this action file is to accept an email address from the user and send out a one-time, passwordless magic link email. Remember, this controller / routing layer is just the delivery mechanism. It's not supposed to do any heavy lifting - all "business logic" needs to be deferred to the "application core". In this case, it means handing off the request to the AuthWorkflow.cfc when the form is submitted:

<cfscript>

	authWorkflow = request.ioc.get( "lib.workflow.AuthWorkflow" );
	errorService = request.ioc.get( "lib.ErrorService" );
	oneTimeTokenService = request.ioc.get( "lib.OneTimeTokenService" );
	logger = request.ioc.get( "lib.logger.Logger" );
	requestHelper = request.ioc.get( "lib.RequestHelper" );
	requestMetadata = request.ioc.get( "lib.RequestMetadata" );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	request.user = authWorkflow.getRequestUser();

	// If the user is already logged-in, redirect them to the app.
	if ( request.user.id ) {

		location( url = "/", addToken = false );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	param name="form.submitted" type="boolean" default=false;
	param name="form.formToken" type="string" default="";
	param name="form.email" type="string" default="";

	errorMessage = "";

	if ( form.submitted && form.email.trim().len() ) {

		try {

			oneTimeTokenService.testToken( form.formToken, requestMetadata.getIpAddress() );
			authWorkflow.requestLogin( form.email.trim() );

			location(
				url = "/index.cfm?event=auth.loginRequested",
				addToken = false
			);

		} catch ( any error ) {

			errorMessage = requestHelper.processError( error );

			// Special overrides to create a better affordance for the user.
			switch ( error.type ) {
				case "App.Model.User.Email.Empty":
				case "App.Model.User.Email.InvalidFormat":
				case "App.Model.User.Email.SuspiciousEncoding":
				case "App.Model.User.Email.TooLong":

					errorMessage = "Please enter a valid email address.";

				break;
				case "App.OneTimeToken.Invalid":

					errorMessage = "Your login form has expired. Please try submitting your request again.";

				break;
			}

		}

	}

	request.template.title = "Request Login / Sign-Up";
	formToken = oneTimeTokenService.createToken( 5, requestMetadata.getIpAddress() );

	include "./request_login.view.cfm";

</cfscript>

Because of the special error-handling in this template (which is me wanting to override the error message under certain outcomes), this action file is a bit more complex than the average action file. But, the bones are all the same: it parameterizes the inputs, it processes a form submission, and then it CFInclude's the view file, request_login.view.cfm:

<cfsavecontent variable="request.template.primaryContent">
	<cfoutput>

		<h1>
			Dig Deep Fitness
		</h1>

		<p>
			Welcome to my fitness tracking application. It is currently a <strong>work in progress</strong>; but, you are welcome to try it out if you are curious.
		</p>

		<h2>
			Login / Sign-Up
		</h2>

		<cfif errorMessage.len()>
			<p>
				#encodeForHtml( errorMessage )#
			</p>
		</cfif>

		<form method="post" action="/index.cfm">
			<input type="hidden" name="event" value="#encodeForHtmlAttribute( request.context.event )#" />
			<input type="hidden" name="submitted" value="true" />
			<input type="hidden" name="formToken" value="#encodeForHtmlAttribute( formToken )#" />

			<input
				type="text"
				name="email"
				value="#encodeForHtmlAttribute( form.email )#"
				placeholder="ben@example.com"
				inputmode="email"
				autocapitalize="off"
				size="30"
				class="input"
			/>
			<button type="submit">
				Login or Sign-Up
			</button>
		</form>

	</cfoutput>
</cfsavecontent>

The only thing of note about this view file is that it isn't writing to the output directly - it's being captured in a CFSaveContent buffer. You may not have thought about this before, but this is basically what every ColdFusion framework is doing for you: rendering a .cfm file and then capturing the output. FW/1, for example, captures this in the body variable. I'm just being more explicit here and I'm capturing it in the request.template.primaryContent variable.

As the request has been routed down through the nested controllers and action files, it's been aggregating data in the request.template structure. If you recall from our root index.cfm file from above, the root controller both routes requests and renders templates. To refresh your memory, here's a relevant snippet from the root controller layout logic:

<cfscript>

	// ... truncated code ...

	request.template = {
		type: "internal",
		statusCode: 200,
		statusText: "OK",
		title: "Dig Deep Fitness",
		assetVersion: "2023.07.22.09.54", // Making the input borders more intense.
		bugsnagApiKey: config.bugsnag.client.apiKey
	};

	try {

		// ... truncated code ...
		// ... truncated code ...
		// ... truncated code ...

		// Now that we have executed the page, let's include the appropriate rendering
		// template.
		switch ( request.template.type ) {
			case "auth":
				include "./layouts/auth.cfm";
			break;
			case "blank":
				include "./layouts/blank.cfm";
			break;
			case "internal":
				include "./layouts/internal.cfm";
			break;
			case "system":
				include "./layouts/system.cfm";
			break;
		}

	// NOTE: Since this try/catch is happening in the index file, we know that the
	// application has, at the very least, successfully bootstrapped and that we have
	// access to all the application-scoped services.
	} catch ( any error ) {

		// ... truncated code ...

	}

</cfscript>

As you can see, in the last part of the try block, after the request has been routed to the lower-level controller, the last step is to render the designated layout. Each layout operates kind of like an "action file" in that is has its own logic and its own view. Sticking with the "auth" example from above, here's the ./layouts/auth.cfm layout file:

<cfscript>

	param name="request.template.statusCode" type="numeric" default=200;
	param name="request.template.statusText" type="string" default="OK";
	param name="request.template.title" type="string" default="";
	param name="request.template.primaryContent" type="string" default="";
	param name="request.template.assetVersion" type="string" default="";

	// Use the correct HTTP status code.
	cfheader(
		statusCode = request.template.statusCode,
		statusText = request.template.statusText
	);

	// Reset the output buffer.
	cfcontent( type = "text/html; charset=utf-8" );

	include "./auth.view.cfm";

</cfscript>

As you can see, the layout action file parameterizes (and documents) the request.template properties that it needs for rendering, resets the output, and then includes the "layout view" file. Unlike an "action view" file, which is captured in a CFSaveContent buffer, the "layout view" file writes directly to the response stream:

<cfoutput>

	<!doctype html>
	<html lang="en">
	<head>
		<cfinclude template="./shared/meta.cfm" />
		<cfinclude template="./shared/title.cfm" />
		<cfinclude template="./shared/favicon.cfm" />
		<link rel="stylesheet" type="text/css" href="/css/temp.css?version=#request.template.assetVersion#" />

		<cfinclude template="./shared/bugsnag.cfm" />
	</head>
	<body>
		#request.template.primaryContent#
	</body>
	</html>

</cfoutput>

And just like that, a request is received by my ColdFusion application, routed through a series of switch statements and include tags, builds-up a content buffer, and then renders the response for the user.

For me, there's a lot to like about this approach. First and foremost, it's very simple. Meaning - from a mechanical perspective - there's just not that much going on. The request is processed in a top-down manner; and, every single file is being explicitly included / invoked. There's zero magic in the translation of a request into a response for the user.

Furthermore, because I am using simple .cfm files for the controller layer, I am forced to keep the logic in those files relatively simple. At first blush, I missed being able to define a private "utility" controller method on a .cfc-based component. But, what I came to discover is that those "private methods" could actually be moved into "utility components", ultimately making them more reusable across the application. It is a clear example of the "power of constraints."

I also appreciate that while there are clear patterns in this code, those patterns are by convention, not by mandate. This allows me to break the pattern if and when it serves a purpose. Right now, I only have one root error handler in the application. However, if I were to create an API controller, for example, I could very easily give the API controller its own error handling logic that normalized all error structures coming out of the API.

And, speaking of error handling, I love having an explicit error handler in the routing logic that is separate from the onError() event-handler in the Application.cfc. This allows me to make strong assumptions about the state of the application depending on which error-handling mechanism is being invoked.

I love that the action files and the view files are collocated in the folder structure. This makes it painless to edit and maintain files that often evolve in lock-step with each other. No having to flip back-and-forth between "controller" folders and "view" folders.

And speaking of "painless editing", since the action/view files are all just .cfm files, there's no caching of the logic. Which means, I never have to re-initialize the application just to make an edit to the way in which my request is being routed and rendered.

ASIDE: This "no caching" point is not a clear-cut win. There are benefits to caching. And, there are benefits to not caching. And, the "business logic" is still all being cached inside ColdFusion components. So, if that changes, the application still needs to be re-initialized.

One of ColdFusion's super powers is that it allows you be as simple as you want and as complex as you need. In fact, I would argue that the existence of the CFInclude tag is a key contributor to this desirable flexibility. So key, in fact, that I am able to create a robust and resilient routing and controller system using nothing but a series of try, switch, and include tags.

Want to use code from this post? Check out the license.

Reader Comments

238 Comments

As they say, every application uses a framework. Either you're using one someone else wrote, or you're writing your own. Good stuff 🙌

15,848 Comments

@Chris,

Totally! I think people often use that phrase in a negative / pejorative way. But, if you keep your complexity in alignment with your needs, I think having your own simple way to do things keeps things at the right level of complexity and maintainability.

It's like, not every JavaScript project needs TypeScript - not every ColdFusion app needs a robust framework in order to get things done.

1 Comments

Great post! This methodology really resonates with how I've always built things when not using a formal framework. Keeps it nice and simple to understand and very straightforward for someone else to jump in and work on. That said, I've always wondered how cfincludes, or the equivalent in PHP perform; I assume at some layer there's some caching rather than it needing to do disk I/O multiple times for every call?

15,848 Comments

@James,

Great question! My understanding is that there are several layers of caching that happen. First, the CFML (the code / markup) is compiled down into Java Byte code, which is cached. Then, I believe file-existence is also cached, so it doesn't have to be checked again. That said, I also believe the last point is influenced by some performance & caching settings in the ColdFusion Admin.

According to this ColdFusion Tuning Guide by Convective:

Trusted Cache

On every page request, ColdFusion will first check if the page being requested has been changed since it was last executed. If it has been changed, ColdFusion will recompile it. This introduces a slight overhead with every page request. During development, this is exactly the behavior you want. When a file is updated, the changes must be immediately reflected. However, once your code makes it to production it should not change that often.

Enabling the trusted cache allows ColdFusion to bypass this check. ColdFusion will only compile files one time, and then trust that the version it has compiled is the latest. By removing this verification step during the execution of a request the overall performance of your site can be improved. For sites where templates are not updated during the life of the server, file system overhead is reduced. Not bad for a single checkbox.

However, there are a couple caveats to consider when considering this feature. This feature should only be enabled in your production environment. Any changes to files will not be recognized until the trusted cached is purged (which is explained below). Second, your production deployment process will need to be updated to purge the trusted cache everytime new code is pushed to your server. The default for this setting is disabled.

I think I actually have this turned off in my personal server, since I never have to clear the cache for changes to take effect. That's something I should look into! 😨 I'm pretty sure I can clear the trusted cache progammatically when I have to refresh the files. I'll have to look into that. Thanks for the challenging question!

3 Comments

As a 27 year ColdFusion veteran, I agree: cfinclude makes it much easier to compartmentalize similar chunks of code, makes debugging faster, allows you to comment out features when needed, and your code base is cleaner and easier to read.

15,848 Comments

@Randy,

Clearly, great minds think alike 😉 But, in all seriousness, I completely agree. I also find that it's easier to debug why a request might not be routed to the correct place. This is not a knock on FW/1; but, I can't tell you how much time I've wasted in my life trying to understand why FW/1 wasn't routing to my controller.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel