Using Function LocalMode Modern To More-Safely Render ColdFusion Templates In Lucee 5.3.2.77
Historically, when you set an unscoped, non-var
'ed variable inside a ColdFusion Function, the variable is applied to the variables
scope (except for when that assignment is inside a CFThread
tag). In Lucee CFML, however, we can use the Function directive - localmode
- to change this behavior. When you set the localmode
directive to "modern"
, unscoped variable assignments will be applied to the local
scope, not to the variables
scope. One place in which this could be really helpful in Lucee CFML 5.3.2.77 is in the dynamic rendering of ColdFusion templates.
The other day, I was tasked with generating a number of static HTML files based on ColdFusion templates. To do this, I was CFInclude
'ing a ColdFusion template within the body of a CFSaveContent
tag. I was then writing the output of said content buffer to a flat-file with an .htm
extension.
ASIDE: This is how most Framework rendering engines work - they execute your View templates inside of a
CFSaveContent
buffer; and then, wrap your View template inside of your Layout template using the same methodology; and then, write the resultant output to the response stream.
Consider this ColdFusion template:
<cfscript>
// Assert the expected values.
// --
// NOTE: Using param in this case as an excuse to declare more variables.
param
name = "categorization"
type = "string"
default = "My Friends" // Will assign this value if not provided.
;
param
name = "friends"
type = "array"
;
// Just creating a mapping as an excuse to declare another variable :D
myFriends = friends.map(
( friend ) => {
return( friend.name );
}
);
</cfscript>
<cfoutput>
<h2>
#encodeForHtml( categorization )#
</h2>
<ul>
<!---
In "classic" ColdFusion mode, the declaration of the iteration item,
"friend", causes "friend" to be stored into the "variables" scope. Depending
on how this template is being used, improper understanding of this behavior
would likely cause a "race-condition" at some point.
--->
<cfloop index="friend" array="#myFriends#">
<li>
#encodeForHtml( friend )#
</li>
</cfloop>
</ul>
</cfoutput>
As you can see, this ColdFusion template expects certain contextual variables, like friends
to exist. It then renders the list of friends directly to the page output. However, as it does this, notice that it is also declaring a few additional variables: myFriends
, friend
(the loop item), and, potentially, categorization
.
As stated before, these additional variables would normally be stored in the variables
scope; which, depending on the context, could cause serious race-conditions within an application. Using Lucee CFML 5.3.2.77, however, we can more safely evaluate this ColdFusion template within a Function using localmode
to help eliminate these race-conditions.
To see what I mean, consider the following execution context. In this code, we're going to render the above CFML template inside of a CFSaveContent
tag that is, itself, inside a Function with localmode
set to modern
:
<cfscript>
/**
* I execute the "template.cfm" ColdFusion template with the given values and return
* the generated output.
*
* NOTE: By using localmode="modern", we are safely capturing any unscoped variable
* assignment in the function's local scope - not the Variables scope.
*/
public string function renderTemplate() localmode = "modern" {
savecontent variable = "local.generatedTemplateOutput" {
// NOTE: The ARGUMENTS scope of this function context will implicitly provide
// passed-in values to the following template evaluation.
include "./template.cfm";
}
return( generatedTemplateOutput );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
renderedContent = renderTemplate(
friends = [
{ id: 1, name: "Sarah" },
{ id: 2, name: "Jo" },
{ id: 3, name: "Kim" }
]
);
dump( label = "VARIABLES Scope", var = variables );
</cfscript>
As you can see, I am providing the expected template variables via the arguments
scope of the renderTemplate()
Function. This function then evaluates the ColdFusion template and returns the buffered output. And, when we run the above Lucee CFML code, we get the following page output:
As you can see, when we CFDump
the variables
scope, none of the new variables that we declared inside of the CFML template are polluting the variables
scope. That's because our use of localmode="modern"
safely captures those variable declarations inside the local
scope of the renderTemplate()
function.
NOTE: The CFML template can still implicitly reference the
variables
scope. And, it can still write to thevariables
scope if it explicitly prefixes variable assignments with thevariables
scope.
Let's now contrast this behavior with the traditional - or "classic" - behavior of ColdFusion. This time, we're going to re-run the test with the localmode
directive set to classic
:
<cfscript>
/**
* I execute the "template.cfm" ColdFusion template with the given values and return
* the generated output.
*
* CAUTION: By using localmode="classic" - which is the same as omitting localmode
* altogether - we are allowing unscoped variable assignments to persist to the
* Variables scope.
*/
public string function renderTemplate() localmode = "classic" {
savecontent variable = "local.generatedTemplateOutput" {
// NOTE: The ARGUMENTS scope of this function context will implicitly provide
// passed-in values to the following template evaluation.
include "./template.cfm";
}
return( generatedTemplateOutput );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
renderedContent = renderTemplate(
friends = [
{ id: 1, name: "Sarah" },
{ id: 2, name: "Jo" },
{ id: 3, name: "Kim" }
]
);
dump( label = "VARIABLES Scope", var = variables );
</cfscript>
As you can see, this time, our renderTemplate()
Function is using localmode="classic"
, which is the same as omitting the localmode
directive altogether. And, when we run this Lucee CFML, we get the following output:
As you can see, this time, the newly-declared variables within our CFML template are "bleeding" into the variables
scope of the calling context. In a multi-threaded application (ie, every user-facing application), this behavior can (and has) caused serious - and mysterious - race-conditions. This is almost never the behavior that the developer wants.
ASIDE: To be clear, if our CFML template scoped the declared variables using
local.
, then things would be OK. The problem is, when looking at a CFML template, it is not obvious that this is needed.
Race-conditions can be the cause of many problems in a multi-threaded application. And, unfortunately, the classic behavior of unscoped variables in a ColdFusion CFML template can easily create race-conditions if you are not thinking carefully about how your CFML template is being evaluated. Thankfully, in Lucee CFML 5.3.2.77, we can use a localmode
of modern
to help prevent these "unexpected" race-conditions. This makes the dynamic rendering of CFML templates much safer.
Want to use code from this post? Check out the license.
Reader Comments
@All,
In this post, I mentioned dynamically rendering CFML templates during static site generation. As such, I wanted to quickly follow-up with a post that looks specifically at generating static sites using Lucee / ColdFusion and
localmode
:www.bennadel.com/blog/3679-using-function-localmode-to-render-templates-during-static-site-generation-in-lucee-5-3-2-77.htm
The example is really simple (in scope). But, those were the requirements that I had.
@All,
Here's a quick follow-up on some "unexpected" behavior you can get with nested Functions / Closures when you set
localmode="modern"
at the application level (ie, creating an application-wide default behavior):www.bennadel.com/blog/3680-unexpected-variable-assignment-using-function-localmode-modern-with-nested-closures-in-lucee-5-3-2-77.htm
Personally, I don't think I would set
localmode
at the Application level. I would leave it in "classic" mode; and then, switch to "modern" at lower-levels where it adds more value.