Wednesday, April 24, 2024
HomeJavaScriptMy ColdFusion "Controller" Layer Is Simply A Bunch Of Change Statements And...

My ColdFusion “Controller” Layer Is Simply A Bunch Of Change Statements And CFIncludes


The extra expertise I get, the extra I recognize utilizing an acceptable quantity of complexity when fixing an issue. It is a large a part of why I really like ColdFusion a lot: it permits one to simply scale-up in complexity if and when the necessities develop to warrant it. After I’m working alone, I do not want a sturdy framework with all of the bells-and-whistles. All I would like is a easy dependency-injection technique and a sequence of CFSwtich and CFInclude statements.

I wished to write down this put up, partially, in response to a dialog that I noticed happening in our Working Code Podcast Discord chat. I wasn’t following the chat intently; however, I detected some mild jabbing on the CFInclude tag in ColdFusion. I sensed (?maybe incorrectly?) that it was being denigrated as a newbie’s assemble – not for use by critical builders.

Nothing might be farther from the reality. As a ColdFusion developer with practically 25-years of CFML expertise, I can attest that I take advantage of – and get a lot worth from – the CFInclude tag every single day.

To be clear, I’m not advocating in opposition to frameworks. Frameworks might be fantastic, particularly once you’re working with bigger groups. However, less complicated contexts beg for less complicated options.

And, after I began constructing Dig Deep Health, my ColdFusion health tracker, I wished to construct the easiest potential factor first. As Kent Beck (and others) have mentioned: Make it work, then make it proper, then make it quick.

In Dig Deep Health, the routing management circulation is dictated by an occasion worth that is supplied with the request. The occasion is only a easy, dot-delimited checklist through which every checklist merchandise maps to one of many nested swap statements. The onRequestStart() event-handler in my Software.cfc parameterizes this worth to setup the request processing:

element
	output = false
	trace = "I outline the appliance settings and occasion handlers."
	{

	// Outline the appliance settings.
	this.identify = "DigDeepFitnessApp";
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;

	// ... truncated code ....

	/**
	* I get known as as soon as to initialize the request.
	*/
	public void operate onRequestStart() {

		// Create a unified container for the entire information submitted by the person. This can
		// make it simpler to entry information when a workflow may ship the information initially
		// within the URL scope after which subsequently within the FORM scope.
		request.context = structNew()
			.append( url )
			.append( kind )
		;

		// Param the motion variable. This can be a dot-delimited motion string of what
		// to course of.
		param identify="request.context.occasion" sort="string" default="";

		request.occasion = request.context.occasion.listToArray( "." );
		request.ioc = software.ioc;

	}

}

As you may see, the request.context.occasion string is parsed right into a request.occasion array. The values inside this array are then learn, validated, and consumed because the top-down request processing takes place, passing by way of a sequence of nested CFSwitch and CFInclude tags.

The foundation index.cfm of my ColdFusion software units up this primary swap assertion. It additionally handles the initialization and subsequent rendering of the format. As such, its swap assertion is a little more sturdy than any of the nested swap statements.

Finally, the purpose of every control-flow layer is to mixture the entire information wanted for the designated format template. Some information – like statusCode and statusText – is shared universally throughout all layouts. Different data-points are layout-specific. I initialize the entire common template properties on 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 = {
		sort: "inside",
		statusCode: 200,
		statusText: "OK",
		title: "Dig Deep Health",
		assetVersion: "2023.07.22.09.54", // Making the enter borders extra intense.
		bugsnagApiKey: config.bugsnag.shopper.apiKey
	};

	attempt {

		param identify="request.occasion[ 1 ]" sort="string" default="residence";

		swap ( request.occasion[ 1 ] ) {
			case "auth":
				embody "./views/auth/index.cfm";
			break;
			case "workouts":
				embody "./views/workouts/index.cfm";
			break;
			case "residence":
				embody "./views/residence/index.cfm";
			break;
			case "jointBalance":
				embody "./views/joint_balance/index.cfm";
			break;
			case "journal":
				embody "./views/journal/index.cfm";
			break;
			case "safety":
				embody "./views/safety/index.cfm";
			break;
			case "system":
				embody "./views/system/index.cfm";
			break;
			case "exercise":
				embody "./views/exercise/index.cfm";
			break;
			case "workoutStreak":
				embody "./views/workout_streak/index.cfm";
			break;
			default:
				throw(
					sort = "App.Routing.InvalidEvent",
					message = "Unknown routing occasion: root."
				);
			break;
		}

		// Now that we have now executed the web page, let's embody the suitable rendering
		// template.
		swap ( request.template.sort ) {
			case "auth":
				embody "./layouts/auth.cfm";
			break;
			case "clean":
				embody "./layouts/clean.cfm";
			break;
			case "inside":
				embody "./layouts/inside.cfm";
			break;
			case "system":
				embody "./layouts/system.cfm";
			break;
		}

	// NOTE: Since this attempt/catch is going on within the index file, we all know that the
	// software has, on the very least, efficiently bootstrapped and that we have now
	// entry to all of the application-scoped companies.
	} catch ( any error ) {

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

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

		embody "./layouts/error.cfm";

		if ( ! config.isLive ) {

			writeDump( error );
			abort;

		}

	}

</cfscript>

Whereas the basis index.cfm is extra sturdy than any of the others, it sets-up the sample for the remainder. You will notice that each single control-flow file has the identical primary elements. First, it parameterizes the subsequent related request.occasion index:

<cfscript>

	// Within the root controller, we care in regards to the FIRST index.
	param identify="request.occasion[ 1 ]" sort="string" default="residence";

</cfscript>

Then, as soon as the request.occasion has been defaulted, we determine which controller to embody utilizing a easy swap assertion on the parameterized occasion worth:

<cfscript>

	swap ( request.occasion[ 1 ] ) {
		case "auth":
			embody "./views/auth/index.cfm";
		break;
		case "workouts":
			embody "./views/workouts/index.cfm";
		break;

		// ... truncated code ...

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

</cfscript>

Discover that every of the case statements simply turns round and CFInclude‘s a nested controller’s index.cfm file. The entire nested index.cfm information look comparable, albeit a lot much less complicated. Let’s take, for example, the auth controller:

<cfscript>

	// Each web page within the auth subsystem will use the auth template. That is solely a
	// non-logged-in a part of the appliance and could have a simplified UI.
	request.template.sort = "auth";

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

	param identify="request.occasion[ 2 ]" sort="string" default="requestLogin";

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

</cfscript>

As you may see, this controller seems to be very comparable to the basis controller. Solely, as a substitute of parameterizing and processing request.occasion[1], it makes use of request.occasion[2] – the subsequent index merchandise within the event-list. Discover, additionally, that this controller overrides the request.template.sort worth. This can trigger the basis controller to render a special format template.

This “auth” controller would not want to show round and path to any nested controllers; though, it definitely may – when you’ve got easy swap and embody statements, it is simply controllers all the best way down. As an alternative, this “auth” controller wants to start out executing some actions. As such, its case statements embody native motion information.

Every motion file processes an motion after which features a view rendering. Some motion information are quite simple; and, some motion information are a bit extra complicated. Let us take a look at the “request login” motion file on this “auth” controller.

The purpose of this motion file is to simply accept an e mail tackle from the person and ship out a one-time, passwordless magic hyperlink e mail. Keep in mind, this controller / routing layer is simply the supply mechanism. It isn’t speculated to do any heavy lifting – all “enterprise logic” must be deferred to the “software core”. On this case, it means handing off the request to the AuthWorkflow.cfc when the shape 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.person = authWorkflow.getRequestUser();

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

		location( url = "https://www.bennadel.com/", addToken = false );

	}

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

	param identify="kind.submitted" sort="boolean" default=false;
	param identify="kind.formToken" sort="string" default="";
	param identify="kind.e mail" sort="string" default="";

	errorMessage = "";

	if ( kind.submitted && kind.e mail.trim().len() ) {

		attempt {

			oneTimeTokenService.testToken( kind.formToken, requestMetadata.getIpAddress() );
			authWorkflow.requestLogin( kind.e mail.trim() );

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

		} catch ( any error ) {

			errorMessage = requestHelper.processError( error );

			// Particular overrides to create a greater affordance for the person.
			swap ( error.sort ) {
				case "App.Mannequin.Consumer.E mail.Empty":
				case "App.Mannequin.Consumer.E mail.InvalidFormat":
				case "App.Mannequin.Consumer.E mail.SuspiciousEncoding":
				case "App.Mannequin.Consumer.E mail.TooLong":

					errorMessage = "Please enter a legitimate e mail tackle.";

				break;
				case "App.OneTimeToken.Invalid":

					errorMessage = "Your login kind has expired. Please attempt submitting your request once more.";

				break;
			}

		}

	}

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

	embody "./request_login.view.cfm";

</cfscript>

Due to the particular error-handling on this template (which is me desirous to override the error message underneath sure outcomes), this motion file is a little more complicated than the common motion file. However, the bones are all the identical: it parameterizes the inputs, it processes a kind submission, after which it CFInclude‘s the view file, request_login.view.cfm:

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

		<h1>
			Dig Deep Health
		</h1>

		<p>
			Welcome to my health monitoring software. It's at the moment a <robust>work in progress</robust>; however, you might be welcome to attempt it out if you're curious.
		</p>

		<h2>
			Login / Signal-Up
		</h2>

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

		<kind technique="put up" motion="/index.cfm">
			<enter sort="hidden" identify="occasion" worth="#encodeForHtmlAttribute( request.context.occasion )#" />
			<enter sort="hidden" identify="submitted" worth="true" />
			<enter sort="hidden" identify="formToken" worth="#encodeForHtmlAttribute( formToken )#" />

			<enter
				sort="textual content"
				identify="e mail"
				worth="#encodeForHtmlAttribute( kind.e mail )#"
				placeholder="ben@instance.com"
				inputmode="e mail"
				autocapitalize="off"
				measurement="30"
				class="enter"
			/>
			<button sort="submit">
				Login or Signal-Up
			</button>
		</kind>

	</cfoutput>
</cfsavecontent>

The one factor of be aware about this view file is that it’s not writing to the output straight – it is being captured in a CFSaveContent buffer. You might not have considered this earlier than, however that is principally what each ColdFusion framework is doing for you: rendering a .cfm file after which capturing the output. FW/1, for instance, captures this within the physique variable. I am simply being extra express right here and I am capturing it within the request.template.primaryContent variable.

Because the request has been routed down by way of the nested controllers and motion information, it has been aggregating information within the request.template construction. If you happen to recall from our root index.cfm file from above, the basis controller each routes requests and renders templates. To refresh your reminiscence, here is a related snippet from the basis controller format logic:

<cfscript>

	// ... truncated code ...

	request.template = {
		sort: "inside",
		statusCode: 200,
		statusText: "OK",
		title: "Dig Deep Health",
		assetVersion: "2023.07.22.09.54", // Making the enter borders extra intense.
		bugsnagApiKey: config.bugsnag.shopper.apiKey
	};

	attempt {

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

		// Now that we have now executed the web page, let's embody the suitable rendering
		// template.
		swap ( request.template.sort ) {
			case "auth":
				embody "./layouts/auth.cfm";
			break;
			case "clean":
				embody "./layouts/clean.cfm";
			break;
			case "inside":
				embody "./layouts/inside.cfm";
			break;
			case "system":
				embody "./layouts/system.cfm";
			break;
		}

	// NOTE: Since this attempt/catch is going on within the index file, we all know that the
	// software has, on the very least, efficiently bootstrapped and that we have now
	// entry to all of the application-scoped companies.
	} catch ( any error ) {

		// ... truncated code ...

	}

</cfscript>

As you may see, within the final a part of the attempt block, after the request has been routed to the lower-level controller, the final step is to render the designated format. Every format operates type of like an “motion file” in that’s has its personal logic and its personal view. Sticking with the “auth” instance from above, here is the ./layouts/auth.cfm format file:

<cfscript>

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

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

	// Reset the output buffer.
	cfcontent( sort = "textual content/html; charset=utf-8" );

	embody "./auth.view.cfm";

</cfscript>

As you may see, the format motion file parameterizes (and paperwork) the request.template properties that it wants for rendering, resets the output, after which contains the “format view” file. In contrast to an “motion view” file, which is captured in a CFSaveContent buffer, the “format view” file writes on 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" />
		<hyperlink rel="stylesheet" sort="textual content/css" href="http://www.bennadel.com/css/temp.css?model=#request.template.assetVersion#" />

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

</cfoutput>

And similar to that, a request is obtained by my ColdFusion software, routed by way of a sequence of swap statements and embody tags, builds-up a content material buffer, after which renders the response for the person.

For me, there’s so much to love about this strategy. At first, it is quite simple. Which means – from a mechanical perspective – there’s simply not that a lot occurring. The request is processed in a top-down method; and, each single file is being explicitly included / invoked. There’s zero magic within the translation of a request right into a response for the person.

Moreover, as a result of I’m utilizing easy .cfm information for the controller layer, I’m compelled to maintain the logic in these information comparatively easy. At first blush, I missed with the ability to outline a non-public “utility” controller technique on a .cfc-based element. However, what I got here to find is that these “non-public strategies” may really be moved into “utility elements”, in the end making them extra reusable throughout the appliance. It’s a clear instance of the “energy of constraints.”

I additionally recognize that whereas there are clear patterns on this code, these patterns are by conference, not by mandate. This permits me to interrupt the sample if and when it serves a goal. Proper now, I solely have one root error handler within the software. Nonetheless, if I had been to create an API controller, for instance, I may very simply give the API controller its personal error dealing with logic that normalized all error constructions popping out of the API.

And, talking of error dealing with, I really like having an express error handler within the routing logic that’s separate from the onError() event-handler within the Software.cfc. This permits me to make robust assumptions in regards to the state of the appliance relying on which error-handling mechanism is being invoked.

I really like that the motion information and the view information are collocated within the folder construction. This makes it painless to edit and keep information that usually evolve in lock-step with one another. No having to flip back-and-forth between “controller” folders and “view” folders.

And talking of “painless modifying”, for the reason that motion/view information are all simply .cfm information, there isn’t any caching of the logic. Which implies, I by no means should re-initialize the appliance simply to make an edit to the best way through which my request is being routed and rendered.

ASIDE: This “no caching” level is not a clear-cut win. There are advantages to caching. And, there are advantages to not caching. And, the “enterprise logic” continues to be all being cached inside ColdFusion elements. So, if that adjustments, the appliance nonetheless must be re-initialized.

Considered one of ColdFusion’s tremendous powers is that it permits you be so simple as you need and as complicated as you want. In actual fact, I’d argue that the existence of the CFInclude tag is a key contributor to this fascinating flexibility. So key, in actual fact, that I’m able to create a sturdy and resilient routing and controller system utilizing nothing however a sequence of attempt, swap, and embody tags.

Need to use code from this put up?
Take a look at the license.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments