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


The extra revel in I am getting, the extra I admire the usage of an suitable quantity of complexity when fixing an issue. This can be a giant a part of why I really like ColdFusion such a lot: it permits one to simply scale-up in complexity if and when the necessities develop to warrant it. When I am operating alone, I are not looking for a powerful framework with the entire bells-and-whistles. All I want is a easy dependency-injection technique and a sequence of CFSwtich and CFInclude statements.

I sought after to put in writing this publish, partly, in line with a dialog that I noticed going down in our Running Code Podcast Discord chat. I wasn’t following the chat intently; however, I detected some gentle jabbing on the CFInclude tag in ColdFusion. I sensed (?most likely incorrectly?) that it used to be being denigrated as a newbie’s assemble – now not for use by way of critical builders.

Not anything may well be further from the reality. As a ColdFusion developer with just about 25-years of CFML revel in, I will attest that I take advantage of – and get a lot worth from – the CFInclude tag on a daily basis.

To be clean, I’m now not advocating in opposition to frameworks. Frameworks may also be glorious, particularly if you find yourself operating with higher groups. However, more practical contexts beg for more practical answers.

And, after I began development Dig Deep Health, my ColdFusion health tracker, I sought after to construct the most straightforward conceivable factor first. As Kent Beck (and others) have mentioned: Make it paintings, then make it proper, then make it speedy.

In Dig Deep Health, the routing management stream is dictated by way of an occasion worth that is supplied with the request. The occasion is only a easy, dot-delimited listing wherein each and every listing merchandise maps to one of the crucial nested transfer statements. The onRequestStart() event-handler in my Software.cfc parameterizes this worth to setup the request processing:

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

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

	// ... truncated code ....

	/**
	* I am getting known as as soon as to initialize the request.
	*/
	public void serve as onRequestStart() {

		// Create a unified container for all the records submitted by way of the consumer. This may
		// provide help to get entry to records when a workflow may ship the information first of all
		// within the URL scope after which therefore within the FORM scope.
		request.context = structNew()
			.append( url )
			.append( shape )
		;

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

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

	}

}

As you’ll see, the request.context.occasion string is parsed right into a request.occasion array. The values inside of this array are then learn, validated, and fed on because the top-down request processing takes position, passing via a sequence of nested CFSwitch and CFInclude tags.

The foundation index.cfm of my ColdFusion software units up this primary transfer observation. It additionally handles the initialization and next rendering of the structure. As such, its transfer observation is a little more tough than any of the nested transfer statements.

In the end, the objective of each and every control-flow layer is to mixture all the records wanted for the designated structure template. Some records – like statusCode and statusText – is shared universally throughout all layouts. Different data-points are layout-specific. I initialize all the common template houses on this root index.cfm report.

<cfscript>

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

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

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

	take a look at {

		param identify="request.occasion[ 1 ]" kind="string" default="house";

		transfer ( request.occasion[ 1 ] ) {
			case "auth":
				come with "./perspectives/auth/index.cfm";
			spoil;
			case "workout routines":
				come with "./perspectives/workout routines/index.cfm";
			spoil;
			case "house":
				come with "./perspectives/house/index.cfm";
			spoil;
			case "jointBalance":
				come with "./perspectives/joint_balance/index.cfm";
			spoil;
			case "magazine":
				come with "./perspectives/magazine/index.cfm";
			spoil;
			case "safety":
				come with "./perspectives/safety/index.cfm";
			spoil;
			case "device":
				come with "./perspectives/device/index.cfm";
			spoil;
			case "exercise":
				come with "./perspectives/exercise/index.cfm";
			spoil;
			case "workoutStreak":
				come with "./perspectives/workout_streak/index.cfm";
			spoil;
			default:
				throw(
					kind = "App.Routing.InvalidEvent",
					message = "Unknown routing occasion: root."
				);
			spoil;
		}

		// Now that we have got carried out the web page, let's come with the proper rendering
		// template.
		transfer ( request.template.kind ) {
			case "auth":
				come with "./layouts/auth.cfm";
			spoil;
			case "clean":
				come with "./layouts/clean.cfm";
			spoil;
			case "interior":
				come with "./layouts/interior.cfm";
			spoil;
			case "device":
				come with "./layouts/device.cfm";
			spoil;
		}

	// NOTE: Since this take a look at/catch is occurring within the index report, we all know that the
	// software has, on the very least, effectively bootstrapped and that we have got
	// get entry to to the entire application-scoped products and services.
	} catch ( any error ) {

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

		request.template.kind = "error";
		request.template.statusCode = errorResponse.statusCode;
		request.template.statusText = errorResponse.statusText;
		request.template.identify = errorResponse.identify;
		request.template.message = errorResponse.message;

		come with "./layouts/error.cfm";

		if ( ! config.isLive ) {

			writeDump( error );
			abort;

		}

	}

</cfscript>

Whilst the basis index.cfm is extra tough than any of the others, it sets-up the trend for the remainder. You’re going to see that each unmarried control-flow report has the similar elementary elements. First, it parameterizes the subsequent related request.occasion index:

<cfscript>

	// Within the root controller, we care concerning the FIRST index.
	param identify="request.occasion[ 1 ]" kind="string" default="house";

</cfscript>

Then, as soon as the request.occasion has been defaulted, we determine which controller to come with the usage of a easy transfer observation at the parameterized occasion worth:

<cfscript>

	transfer ( request.occasion[ 1 ] ) {
		case "auth":
			come with "./perspectives/auth/index.cfm";
		spoil;
		case "workout routines":
			come with "./perspectives/workout routines/index.cfm";
		spoil;

		// ... truncated code ...

		case "workoutStreak":
			come with "./perspectives/workout_streak/index.cfm";
		spoil;
		default:
			throw(
				kind = "App.Routing.InvalidEvent",
				message = "Unknown routing occasion: root."
			);
		spoil;
	}

</cfscript>

Realize that each and every of the case statements simply turns round and CFInclude‘s a nested controller’s index.cfm report. All the nested index.cfm recordsdata glance identical, albeit a lot much less complicated. Let’s take, for instance, the auth controller:

<cfscript>

	// Each and every web page within the auth subsystem will use the auth template. That is completely a
	// non-logged-in a part of the applying and may have a simplified UI.
	request.template.kind = "auth";

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

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

	transfer ( request.occasion[ 2 ] ) {
		case "loginRequested":
			come with "./login_requested.cfm";
		spoil;
		case "logout":
			come with "./logout.cfm";
		spoil;
		case "requestLogin":
			come with "./request_login.cfm";
		spoil;
		case "verifyLogin":
			come with "./verify_login.cfm";
		spoil;
		default:
			throw(
				kind = "App.Routing.Auth.InvalidEvent",
				message = "Unknown routing occasion: auth."
			);
		spoil;
	}

</cfscript>

As you’ll see, this controller seems very identical to the basis controller. Simplest, as a substitute of parameterizing and processing request.occasion[1], it makes use of request.occasion[2] – the following index merchandise within the event-list. Realize, additionally, that this controller overrides the request.template.kind worth. This may purpose the basis controller to render a special structure template.

This “auth” controller does not wish to flip round and path to any nested controllers; despite the fact that, it undoubtedly may just – you probably have easy transfer and come with statements, it is simply controllers the entire approach down. As a substitute, this “auth” controller wishes to begin executing some movements. As such, its case statements come with native motion recordsdata.

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

The objective of this motion report is to just accept an electronic mail deal with from the consumer and ship out a one-time, passwordless magic hyperlink electronic mail. Take into account, this controller / routing layer is simply the supply mechanism. It isn’t meant to do any heavy lifting – all “trade common sense” must be deferred to the “software core”. On this case, it approach 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.consumer = authWorkflow.getRequestUser();

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

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

	}

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

	param identify="shape.submitted" kind="boolean" default=false;
	param identify="shape.formToken" kind="string" default="";
	param identify="shape.electronic mail" kind="string" default="";

	errorMessage = "";

	if ( shape.submitted && shape.electronic mail.trim().len() ) {

		take a look at {

			oneTimeTokenService.testToken( shape.formToken, requestMetadata.getIpAddress() );
			authWorkflow.requestLogin( shape.electronic 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 consumer.
			transfer ( error.kind ) {
				case "App.Type.Person.Electronic mail.Empty":
				case "App.Type.Person.Electronic mail.InvalidFormat":
				case "App.Type.Person.Electronic mail.SuspiciousEncoding":
				case "App.Type.Person.Electronic mail.TooLong":

					errorMessage = "Please input a sound electronic mail deal with.";

				spoil;
				case "App.OneTimeToken.Invalid":

					errorMessage = "Your login shape has expired. Please take a look at filing your request once more.";

				spoil;
			}

		}

	}

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

	come with "./request_login.view.cfm";

</cfscript>

On account of the particular error-handling on this template (which is me in need of to override the mistake message beneath positive results), this motion report is a little more complicated than the common motion report. However, the bones are the entire identical: it parameterizes the inputs, it processes a sort submission, after which it CFInclude‘s the view report, request_login.view.cfm:

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

		<h1>
			Dig Deep Health
		</h1>

		<p>
			Welcome to my health monitoring software. It's recently a <sturdy>paintings in growth</sturdy>; however, you might be welcome to take a look at it out if you're curious.
		</p>

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

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

		<shape way="publish" motion="/index.cfm">
			<enter kind="hidden" identify="occasion" worth="#encodeForHtmlAttribute( request.context.occasion )#" />
			<enter kind="hidden" identify="submitted" worth="true" />
			<enter kind="hidden" identify="formToken" worth="#encodeForHtmlAttribute( formToken )#" />

			<enter
				kind="textual content"
				identify="electronic mail"
				worth="#encodeForHtmlAttribute( shape.electronic mail )#"
				placeholder="ben@instance.com"
				inputmode="electronic mail"
				autocapitalize="off"
				dimension="30"
				elegance="enter"
			/>
			<button kind="publish">
				Login or Signal-Up
			</button>
		</shape>

	</cfoutput>
</cfsavecontent>

The one factor of observe about this view report is that it’s not writing to the output without delay – it is being captured in a CFSaveContent buffer. You would possibly not have thought of this earlier than, however that is principally what each ColdFusion framework is doing for you: rendering a .cfm report after which taking pictures the output. FW/1, as an example, captures this within the frame variable. I am simply being extra specific right here and I am taking pictures it within the request.template.primaryContent variable.

Because the request has been routed down throughout the nested controllers and motion recordsdata, it is been aggregating records within the request.template construction. For those who recall from our root index.cfm report from above, the basis controller each routes requests and renders templates. To refresh your reminiscence, here is a related snippet from the basis controller structure common sense:

<cfscript>

	// ... truncated code ...

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

	take a look at {

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

		// Now that we have got carried out the web page, let's come with the proper rendering
		// template.
		transfer ( request.template.kind ) {
			case "auth":
				come with "./layouts/auth.cfm";
			spoil;
			case "clean":
				come with "./layouts/clean.cfm";
			spoil;
			case "interior":
				come with "./layouts/interior.cfm";
			spoil;
			case "device":
				come with "./layouts/device.cfm";
			spoil;
		}

	// NOTE: Since this take a look at/catch is occurring within the index report, we all know that the
	// software has, on the very least, effectively bootstrapped and that we have got
	// get entry to to the entire application-scoped products and services.
	} catch ( any error ) {

		// ... truncated code ...

	}

</cfscript>

As you’ll see, within the ultimate a part of the take a look at block, after the request has been routed to the lower-level controller, the ultimate step is to render the designated structure. Every structure operates roughly like an “motion report” in this is has its personal common sense and its personal view. Sticking with the “auth” instance from above, here is the ./layouts/auth.cfm structure report:

<cfscript>

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

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

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

	come with "./auth.view.cfm";

</cfscript>

As you’ll see, the structure motion report parameterizes (and paperwork) the request.template houses that it wishes for rendering, resets the output, after which comprises the “structure view” report. In contrast to an “motion view” report, which is captured in a CFSaveContent buffer, the “structure view” report writes without delay to the reaction move:

<cfoutput>

	<!doctype html>
	<html lang="en">
	<head>
		<cfinclude template="./shared/meta.cfm" />
		<cfinclude template="./shared/identify.cfm" />
		<cfinclude template="./shared/favicon.cfm" />
		<hyperlink rel="stylesheet" kind="textual content/css" href="http://www.bennadel.com/css/temp.css?model=#request.template.assetVersion#" />

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

</cfoutput>

And similar to that, a request is won by way of my ColdFusion software, routed via a sequence of transfer statements and come with tags, builds-up a content material buffer, after which renders the reaction for the consumer.

For me, there is so much to love about this way. Firstly, it is quite simple. That means – from a mechanical point of view – there is simply now not that a lot occurring. The request is processed in a top-down method; and, each unmarried report is being explicitly integrated / invoked. There is 0 magic within the translation of a request right into a reaction for the consumer.

Moreover, as a result of I’m the usage of easy .cfm recordsdata for the controller layer, I’m pressured to stay the common sense in the ones recordsdata reasonably easy. To start with blush, I overlooked with the ability to outline a personal “software” controller way on a .cfc-based element. However, what I got here to find is that the ones “personal strategies” may just in truth be moved into “software elements”, in the end making them extra reusable around the software. This can be a clean instance of the “energy of constraints.”

I additionally admire that whilst there are clean patterns on this code, the ones patterns are by way of conference, now not by way of mandate. This permits me to wreck the trend if and when it serves a objective. Presently, I simplest have one root error handler within the software. On the other hand, if I had been to create an API controller, as an example, I may just very simply give the API controller its personal error dealing with common sense that normalized all error constructions popping out of the API.

And, talking of error dealing with, I really like having an specific error handler within the routing common sense this is break away the onError() event-handler within the Software.cfc. This permits me to make sturdy assumptions concerning the state of the applying relying on which error-handling mechanism is being invoked.

I really like that the motion recordsdata and the view recordsdata are collocated within the folder construction. This makes it painless to edit and handle recordsdata that regularly evolve in lock-step with each and every different. No having to turn back-and-forth between “controller” folders and “view” folders.

And talking of “painless enhancing”, for the reason that motion/view recordsdata are all simply .cfm recordsdata, there is not any caching of the common sense. Which means that, I by no means must re-initialize the applying simply to make an edit to the best way wherein my request is being routed and rendered.

ASIDE: This “no caching” level is now not a straight forward win. There are advantages to caching. And, there are advantages not to caching. And, the “trade common sense” remains to be all being cached inside of ColdFusion elements. So, if that adjustments, the applying nonetheless must be re-initialized.

One in every of ColdFusion’s tremendous powers is that it permits you be so simple as you need and as complicated as you want. In reality, I might argue that the life of the CFInclude tag is a key contributor to this fascinating flexibility. So key, in reality, that I’m able to create a powerful and resilient routing and controller device the usage of not anything however a sequence of take a look at, transfer, and come with tags.

Wish to use code from this publish?
Take a look at the license.



0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
Back To Top
0
Would love your thoughts, please comment.x
()
x