Making A Case For Var Declarations In ColdFusion Templates
Last week, I opened a feature request in the Adobe bug tracker to allow for var
declarations in CFML templates. After I opened this, I shared it within the Working Code discord; and, all those who responded did so in opposition to the idea. As such, I wanted to take a moment to more clearly articulate my case for allowing var
declarations in, essentially, any ColdFusion context.
The Current Situation
Today, the var
keyword is explicitly defined in the Adobe ColdFusion documentation as pertaining to Function local variables. When you declare a variable inside a function
block using the var
keyword, ColdFusion restricts access to said variable to within the body of the function (and to any ColdFusion closures that are defined within that function body).
As it stands now, the var
keyword can only be used within a function. And, any attempt to use the var
keyword outside of a function will lead to the ColdFusion error:
The local variable environment cannot be declared outside of a function.
All variables defined with the var keyword must be declared inside a function.
For example, if I try to execute this CFML template (my-view.cfm
), I will get the aforementioned error:
<cfscript>
var environments = [ "development", "staging", "production" ];
for ( var environment in environments ) {
writeOutput( environment );
}
</cfscript>
Of course, if I copy-paste this code into a function
body and then execute the function, it will work just fine. Which feels like a completely unnecessary differentiation in behavior; and, has always diverged from the behavior of JavaScript—the world's most popular programming language.
If I try to take the above CFML template (my-view.cfm
) and CFInclude
it into a function body, which for all intents and purposes moves the var
keyword into a function context, I still get an error:
<cfscript>
writeOutput( renderView( "my-view.cfm" ) );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I capture and return the rendering of the given CFML template.
*/
private string function renderView( required string viewTemplate ) {
savecontent variable = "local.viewOutput" {
// Render the given view into the buffer.
include "./#viewTemplate#";
}
return viewOutput;
}
</cfscript>
Attempting to execute this CFML template and invoke the renderView()
function still throws the same var
error. The fact that this technique throws an error in Adobe ColdFusion is surprising. I believe it means that the var
analysis is a compile time issue not a runtime issue. But, I'm just guessing.
A Dangerous Syntax Constraint
In the above case, the error is problematic. Because removing the var
declarations fundamentally changes the meaning of the code. If I update the view to remove the var
keywords:
<cfscript>
// CAUTION: No more `var` keywords.
environments = [ "development", "staging", "production" ];
for ( environment in environments ) {
writeOutput( environment );
}
</cfscript>
And then update my renderView()
example to output the variables
scope after executing the function:
<cfscript>
writeOutput( renderView( "my-view-2.cfm" ) );
// Output the page context private scope.
writeDump(
var = variables,
label = "VARIABLES Scope"
);
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I capture and return the rendering of the given CFML template.
*/
private string function renderView( required string viewTemplate ) {
savecontent variable = "local.viewOutput" {
// Render the given view into the buffer.
include "./#viewTemplate#";
}
return viewOutput;
}
</cfscript>
We get the following page output:
As you can see, both the environments
and the environment
variables have leaked out of the function
body and into the page's variables
scope.
This is the technique that many ColdFusion frameworks use to render views (see FW/1's internalView()
function). Of course, views are normally tag-base and interleave CFML and HTML tags. As such, you might be used to seeing view code that looks more like this:
<cfoutput>
<!--- "environments" provided as request-context (RC) variable. --->
<cfloop array="#rc.environments#" index="environment">
<p>
#encodeForHtml( environment )#
</p>
</cfloop>
</cfoutput>
This ColdFusion code just committed the same offense. The environment
variable declared as the CFLoop
index has leaked out of the parent function and into the variables
scope. To prevent this, you'd have to explicitly add a local.
scoping to the index
attribute:
<cfloop array="..." index="local.environment">
In a ColdFusion framework like FW/1, this is less of an issue since the ColdFusion framework component is newly instantiated on every request. As such, variable leakage is, at the very least, locked-down to the current request.
But, if this were part of a different rendering technique, such as when rendering CFMail
content or performing static site generation, those variable leaks would almost certainly be persisted across requests which could have devastating consequences in a multi-tenant environment (ie, one user seeing another user's data due to persisted variable leakage).
Aside: Lucee CFML provides the
localmode
construct for overriding this behavior, which makes it ideal for dynamic view rendering. This would be a great feature for Adobe ColdFusion to add as well.
Not only is restricting var
usage to the function body a seemingly unnecessary constraint, it's a dangerous constraint.
The Wrong Solution
There will almost certainly be people who read this article and think to themselves, "That's why every variable reference should be explicitly scoped." This is the wrong solution. Having to explicitly scope every variable makes life harder for web developers. Languages need to be evolving to create a better DX (Developer Experience)—not forcing developers to rely on arcane techniques.
var
(My Feature Request)
An Evolving Notion of Right now, people are very hung up on the notion that var
is specifically for function
bodies. But, if you think just a little more abstractly, what var
is doing is merely scoping a given variable to the closest "variable container".
In ColdFusion, there are really only two variable containers: the function local scope and the page scope. The function local scope is relatively self-explanatory. And, you can think of the "page scope" as being akin to the variables
scope. When you make a request to a ColdFusion template, that template receives the top-level page scope (which may or may not be the Application.cfc
page scope, depending on your use of the onRequest()
life-cycle method). Then, each ColdFusion component and custom tag invocation receives its own unique page scope (and its own, isolated variables
scope).
Aside: The
CFThread
tag is kind of its own variables container; but, under the hood it's being executed as a function and acts as a function-like container.
If you keep the idea of these two types of variable containers in mind, you can begin to think of the var
keyword as locking variable access to the closest variable container.
As such, when the var
keyword is used inside a function body, it will lock access of that variable to the function local scope. And, when the var
keyword is used inside a CFML template / custom tag / component pseudo constructor, etc., it will lock access of that variable to the page scope.
This creates a more consistent syntax (the var
keyword can be used anywhere); and, it creates a more consistent sense of variable scoping (the variable is always scoped to the closest "variable container"). This also leads to a simpler mental model where code can be copy-pasted from one context into another without having to worry about whether or not the var
keyword is being used.
This also does nothing to hurt backwards compatibility of the CFML language since it's basically codifying what unscoped template-level variable declarations are already doing.
Variable Leakage Is Still a Problem
To be clear, this does nothing to combat variable leakage (ie, forgetting to var
-scope a function local variable). Only something like localmode
would do that. This is an unrelated issue (albeit an important one).
Want to use code from this post? Check out the license.
Reader Comments
I've also opened a feature request to include a
localmode
or similar feature to define how unscoped variables should be handled:https://tracker.adobe.com/#/view/CF-4223964
I've actually gotten into the strong habit of using
local.
for just about everything. I rarely usevar
anymore.Developing primarily on top of Coldbox, all the views run (I think as includes) within a function somewhere.
Also,
local.
works in straight .cfm files, too, which I often use when I'm just testing an idea or trying to solve some weird problem, outside the framework in my "scratch" site.I have always felt this way, though I could have never articulated it quite so well. I often write my code in a page's context, then refactor into functions and find it frustrating to have to worry about adding (or removing) the var statement depending on context.
@Will,
I didn't know that
local
works in CFM templates. I'm guessing that what it does is actually createvariables.local.value
behind the scenes. That's been a quirky behavior of ColdFusion forever—that you can just assign to an arbitrarily deep struct-path and ColdFusion will on-the-fly create the struct for you.@Chris,
Exactly!! Just copying code from place to place shouldn't require a rewrite. Such an unnecessary step!
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →