Creating Runtime Extensions And Polyfills In ColdFusion
At PAI Industries, we use the CFWheels ColdFusion framework. Wheels has a lot of "magic" baked into it. Some of it is great; and, some of it I'm not crazy about; but, the one thing that I can't get out of my mind is the use of e()
as an alias for encodeForHtml()
. A few weeks ago, I wrote about using alias functions for encodeForHtml()
and encodeForHtmlAttribute()
; and, the consensus within the comments was that it was too mysterious. And, I agree. So I wanted to revisit the idea with a more explicit implementation.
The Wheels framework doesn't provide the e()
alias - that's specific to the PAI platform. But, the e()
function is made globally available within our ColdFusion applications because Wheels orchestrates the execution of most CFML templates and auto-injects a series of CFML templates that can contain any arbitrary functions that we want.
I think the hesitation that people have about these globally-available functions (myself included) isn't that they exist, it's that exist mysteriously and surprisingly. But what if it wasn't mysterious and surprising? What if the "extensions" to the runtime were explicitly included into each CFML template context?
In the ColdFusion community, we've become so obsessed with "modern" constructs like ColdFusion Components (CFCs) that we've somewhat forgotten that the CFML template is hella powerful! Not only can it be used to transclude code into another context, it can be invoked with strong boundaries when we use the CFModule
tag instead of the CFInclude
tag.
Which is all to say, can we alleviate the sense of mystery and surprise if we simply include the runtime extensions ourselves?
I wanted to see if I could create two different experiences with this include. In the first experience, you use CFInclude
and you get all the things merged into your current variables
scope (the private page scope):
include "/cfmlx.cfm";
In the second experience, you use CFModule
and you can explicitly define the variable into which the extensions are installed using the as
attribute:
cfmodule( template="/cfmlx.cfm", as="x" );
This would put all the extensions into the x
variable in the calling context. This would be for people that want that little extra sense of explicitness. And, would also avoid any potential name collisions.
In ColdFusion, a CFML template can be invoked using either CFInclude
or CFModule
; but, there's nothing in the runtime to help differentiate this invocation. I've filed a feature enhancement for this in the Adobe Tracker; but until that's added, we have to tease-out the experience by introspecting the attributes
scope.
Aside: the easiest thing to do would be to have two different templates: one for straight includes and one for modules. But, to keep things fun in this exploration, I wanted to see if I could provide both access patters within a single point of ingress.
Here's my cfmlx.cfm
ColdFusion template. You'll see that it introspects the attributes
scope; and then, either performs a caller
-based injection or a variables
-based injection:
<cfscript> | |
(() => { | |
// It's possible that this template is being invoked as a standard CFML template, | |
// but that it's being included into a CFModule tag context. Which means that the | |
// "attributes" scope will exist, but it won't be relevant to us. As such, we have | |
// to tease-out the invocation style of the template by narrowing-down the way the | |
// attributes scope is being used. | |
var isModule = ( | |
// The "as" attribute exists to namespace the CFMLX methods. | |
( variables.attributes?.as?.len() > 0 ) && | |
// The "as" attribute is the ONLY attribute defined. | |
( variables.attributes.size() == 1 ) | |
); | |
// Todo: we could move this to a persisted scope in a more robust implementation. | |
request.cfmlx = ( request.cfmlx ?: new cfmlx() ); | |
if ( isModule ) { | |
// Namespace the CFMLX extensions into the calling context. | |
setVariable( "caller.#attributes.as#", request.cfmlx ); | |
// BUT, always append the POLYFILL function directly since those should be | |
// safe to use without conflict (since they are CFML functions). | |
structAppend( caller, request.cfmlx.$polyfills ); | |
} else { | |
// Append all CFMLX functions as closure-bound references. | |
structAppend( variables, request.cfmlx.$polyfills ); | |
structAppend( variables, request.cfmlx.$extensions ); | |
} | |
})(); | |
</cfscript> |
This cfmlx.cfm
template is just the injection mechanism - it doesn't actually define any of the functions. For a variety of reasons, not the least of which is a closure bug in Adobe ColdFusion modules, I'm using a ColdFusion component - cfmlx.cfc
- to define the functions.
This ColdFusion component, cfmlx.cfc
, contains both runtime extensions and cross-platform polyfills. For example, is provides a dump()
implementation for Adobe ColdFusion. In addition to exposing them as public methods on the CFC instance, it also exposes two special properties:
$polyfills
$extensions
These are structs that contain lexically bound references to a subset of methods on the CFC. This way, we can always append the polyfills even if we're only going to expose the extensions using the as
attribute. This was a design decision.
We'll get the implementation of the ColdFusion component in a moment, but first let's look at some consumption examples. In this first example, we're going to consume the cfmlx.cfm
template as a CFInclude
. This means that the extensions are appended directly to the private page scope (variables
):
<cfscript> | |
// Appends all of the polyfill and extension method to private page scope. | |
include "/cfmlx.cfm"; | |
data = { | |
runtime: { | |
isAdobe: isAdobe(), | |
isLucee: isLucee(), | |
isBoxlang: isBoxlang() | |
}, | |
range: rangeNew( 5 ) | |
}; | |
dump( data ); | |
systemOutput( data ); | |
systemOutput( "<print-stack-trace>" ); | |
</cfscript> | |
<script type="text/javascript"> | |
console.log( JSON.parse( "<cfoutput>#e4json( data )#</cfoutput>" ) ); | |
</script> |
First, this uses polyfills for Adobe ColdFusion:
dump()
systemOutput()
It also uses some runtime extensions:
isAdobe()
- is the runtime Adobe ColdFusion.isLucee()
- is the runtime Lucee CFML.isBoxlang()
- is the runtime Boxlang.rangeNew()
- creates an array of indices.e4json()
- serializes and encodes JSON for a JavaScript context.
If we run this in either Lucee CFML or Adobe ColdFusion, we get the same experience:

Now, let's try the same demo using the CFModule
invocation instead of CFInclude
:
<cfscript> | |
// Appends all of the POLYFILL method to private page scope. But, all non-polyfill | |
// methods are scoped to the "as" variable. | |
cfmodule( template="/cfmlx.cfm", as="x" ); | |
data = { | |
runtime: { | |
isAdobe: x.isAdobe(), | |
isLucee: x.isLucee(), | |
isBoxlang: x.isBoxlang() | |
}, | |
range: x.rangeNew( 5 ) | |
}; | |
dump( data ); | |
systemOutput( data ); | |
systemOutput( "<print-stack-trace>" ); | |
</cfscript> | |
<script type="text/javascript"> | |
console.log( JSON.parse( "<cfoutput>#x.e4json( data )#</cfoutput>" ) ); | |
</script> |
If you recall from above, the cfmlx.cfc
ColdFusion component exposes two special properties: one for extensions, one for polyfills. Even when the cfmlx.cfm
template is invoked as a module, I'm still appending the polyfills to the calling context. This is why dump()
and systemOutput()
can be accessed unscoped while x.isAdobe()
and x.e4json()
need the x
scope.
This runs exactly the same in as the previous demo.
Now, let's look at the cfmlx.cfc
ColdFusion component. First, I'm just going to show a truncated version of it that highlights the mechanics; then, I'll show the full version.
In the following code, several major things are happening:
The polyfill methods are being renamed (ex,
dumpPolyfill
becomesdump
). This is because you can't redeclare native methods. But, you can create variables with the same name.All of the public methods are being lexically bound to the component instance. This way, the methods can all consume each other regardless of how they are passed-around in the calling context.
The
$polyfills
struct is being populated with polyfill methods that do not conflict with native methods. This way, we don't override methods that we don't need to (at least, not outside of the CFC instance).The
$extensions
struct is being populated with all bound methods, less the polyfill methods.
component { | |
/** | |
* I initialize the CFMLX collection of functions. | |
*/ | |
public void function init() { | |
var nativeIndex = getFunctionList(); | |
var polyfillIndex = $mergeInPolyfills(); | |
var boundIndex = $createBoundMethods(); | |
this.$polyfills = boundIndex.filter( | |
( key ) => { | |
// Only include polyfill methods that don't collide with native methods. | |
return ( polyfillIndex.keyExists( key ) && ! nativeIndex.keyExists( key ) ); | |
} | |
); | |
this.$extensions = boundIndex.filter( | |
( key ) => { | |
// Only include bound methods that are not polyfill methods. | |
return ! this.$polyfills.keyExists( key ); | |
} | |
); | |
} | |
// .... truncated .... // | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
/** | |
* I create an index of public methods bound to the CFMLX instance. | |
*/ | |
private struct function $createBoundMethods() { | |
return structFilter( this, ( key, value ) => isCustomFunction( value ) ) | |
.map( ( key ) => methodBind( this, key ) ) | |
; | |
} | |
/** | |
* I copy the polyfill-suffixed methods into non-suffixed names. | |
* | |
* Note: even if these names conflict with native methods in the current runtime, it's | |
* still safe to copy them in since they weren't _defined_ with conflicting names. This | |
* will only affect usage of these methods scoped to this CFC. Native methods aren't | |
* overwritten when polyfills are copied to parent `variables` scope (see the filtering | |
* logic within the constructor). | |
*/ | |
private struct function $mergeInPolyfills( string suffix = "polyfill" ) { | |
var suffixLength = suffix.len(); | |
var methodIndex = {}; | |
for ( var name in variables ) { | |
if ( name.right( suffixLength ) != suffix ) { | |
continue; | |
} | |
var runtimeName = name.left( -suffixLength ); | |
methodIndex[ runtimeName ] | |
= this[ runtimeName ] | |
= variables[ runtimeName ] | |
= variables[ name ] | |
; | |
} | |
return methodIndex; | |
} | |
} |
Internally to the CFC, all extension methods are just instance methods on the CFC. As such, they can reference each other no problem. For example, the systemOutput()
polyfill method turns around and calls the dump()
polyfill method, which you'll see below. And, since they are all lexically bound when passed out of scope (via the $extensions
struct), they can continue to call each other even from within another template.
Here's the full implementation of the cfmlx.cfc
ColdFusion component. It contains a number of methods that I wasn't using in the demo; but, which are here to illustrate the thought process I'm going through.
component { | |
/** | |
* I initialize the CFMLX collection of functions. | |
*/ | |
public void function init() { | |
var nativeIndex = getFunctionList(); | |
var polyfillIndex = $mergeInPolyfills(); | |
var boundIndex = $createBoundMethods(); | |
this.$polyfills = boundIndex.filter( | |
( key ) => { | |
// Only include polyfill methods that don't collide with native methods. | |
return ( polyfillIndex.keyExists( key ) && ! nativeIndex.keyExists( key ) ); | |
} | |
); | |
this.$extensions = boundIndex.filter( | |
( key ) => { | |
// Only include bound methods that are not polyfill methods. | |
return ! this.$polyfills.keyExists( key ); | |
} | |
); | |
} | |
// --- | |
// POLYFILL METHODS. | |
// --- | |
/** | |
* I polyfill the dump() function in Adobe ColdFusion. | |
*/ | |
public void function dumpPolyfill( | |
required any var, | |
boolean expand = true, | |
string format = "html", | |
string hide = "", | |
numeric keys = 9999, | |
string label = "", | |
string output = "browser", | |
string show = "all", | |
boolean showUDFs = true, | |
numeric top = 9999, | |
boolean abort = false | |
) { | |
// Note: under the hood, Adobe ColdFusion seems to be compiling this down into an | |
// instance of the CFDump tag (based on error messages). As such, it's much more | |
// temperamental than a normal function invocation. We have to be much more | |
// explicit in our argument pass-through; and, certain attributes CANNOT be passed | |
// in as NULL. | |
writeDump( | |
var = var, | |
expand = expand, | |
format = format, | |
hide = hide, | |
keys = keys, | |
label = label, | |
output = output, | |
show = show, | |
showUDFs = showUDFs, | |
top = top, | |
abort = abort | |
); | |
} | |
/** | |
* I polyfill the echo() function in Adobe ColdFusion. | |
*/ | |
public void function echoPolyfill( required any value ) { | |
writeOutput( value ); | |
} | |
/** | |
* I polyfill the systemOutput() function in Adobe ColdFusion. | |
*/ | |
public void function systemOutputPolyfill( | |
required any value, | |
boolean addNewline = false, // Ignored. | |
boolean doErrorStream = false // Ignored. | |
) { | |
if ( isString( value ) && value.find( "<print-stack-trace>" ) ) { | |
var stacktrace = callStackGet() | |
.map( ( frame ) => "#frame.template#:#frame.lineNumber#" ) | |
.prepend( "Stacktrace:" ) | |
.toList( chr( 10 ) ) | |
; | |
value = value.replace( "<print-stack-trace>", stacktrace ); | |
} | |
dump( | |
var = value, | |
format = "text", | |
output = "console" | |
); | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
/** | |
* I dump the top N entries of the given value. | |
*/ | |
public void function dumpN( | |
required any value, | |
numeric top = 10 | |
) { | |
dump( var = value, top = top ); | |
} | |
/** | |
* Alias for native method: encodeForHtml(). | |
*/ | |
public string function e( required string value ) { | |
return encodeForHtml( value ); | |
} | |
/** | |
* Alias for native method: encodeForHtmlAttribute(). | |
*/ | |
public string function e4a( required string value ) { | |
return encodeForHtmlAttribute( value ); | |
} | |
/** | |
* Alias for native method: encodeForJavaScript(). | |
*/ | |
public string function e4j( required string value ) { | |
return encodeForJavaScript( value ); | |
} | |
/** | |
* I serialize the given value as JSON and encode for JavaScript. This is intended to | |
* be consumed in a `JSON.parse( "#e4Json()#" )` call. | |
*/ | |
public string function e4json( required any value ) { | |
return e4j( serializeJson( value ) ); | |
} | |
/** | |
* Alias for native method: encodeForUrl(). | |
*/ | |
public string function e4u( required string value ) { | |
return encodeForUrl( value ); | |
} | |
/** | |
* I determine if the CFML engine is Adobe ColdFusion. | |
*/ | |
public boolean function isAdobe() { | |
return ( ! isLucee() && ! isBoxlang() ); | |
} | |
/** | |
* I determine if the CFML engine is Boxlang. | |
*/ | |
public boolean function isBoxlang() { | |
return structKeyExists( server, "boxlang" ); | |
} | |
/** | |
* I determine if the CFML engine is Lucee CFML. | |
*/ | |
public boolean function isLucee() { | |
return structKeyExists( server, "lucee" ); | |
} | |
/** | |
* I determine if the given value is a native string. | |
*/ | |
public boolean function isString( required any value ) { | |
return isInstanceOf( value, "java.lang.String" ); | |
} | |
/** | |
* I sum the variadic arguments list. Returns `0` if no arguments. | |
*/ | |
public numeric function mathSum() { | |
var values = arrayMap( arguments, ( value ) => ( value ?: 0 ) ); | |
return values.reduce( | |
( reduction, value ) => { | |
return ( reduction + value ); | |
}, | |
0 | |
); | |
} | |
/** | |
* I return a closure that binds given method to the given source component, allowing | |
* the closure to be passed-around without scoping. | |
*/ | |
public function function methodBind( | |
required any source, | |
required string methodName, | |
any methodArguments | |
) { | |
return () => invoke( source, methodName, ( methodArguments ?: arguments ) ); | |
} | |
/** | |
* I create a new range with the given indicies. | |
*/ | |
public array function rangeNew( | |
required numeric start, | |
numeric end | |
) { | |
// If only one index is provided, assume start from 1. | |
if ( isNull( end ) ) { | |
arguments.end = arguments.start; | |
arguments.start = 1; | |
} | |
var size = ( end - start + 1 ); | |
var range = []; | |
range.resize( size ); | |
for ( var i = 1 ; i <= size ; i++ ) { | |
range[ i ] = ( start + i - 1 ); | |
} | |
return range; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
/** | |
* I create an index of public methods bound to the CFMLX instance. | |
*/ | |
private struct function $createBoundMethods() { | |
return structFilter( this, ( key, value ) => isCustomFunction( value ) ) | |
.map( ( key ) => methodBind( this, key ) ) | |
; | |
} | |
/** | |
* I copy the polyfill-suffixed methods into non-suffixed names. | |
* | |
* Note: even if these names conflict with native methods in the current runtime, it's | |
* still safe to copy them in since they weren't _defined_ with conflicting names. This | |
* will only affect usage of these methods scoped to this CFC. Native methods aren't | |
* overwritten when polyfills are copied to parent `variables` scope (see the filtering | |
* logic within the constructor). | |
*/ | |
private struct function $mergeInPolyfills( string suffix = "polyfill" ) { | |
var suffixLength = suffix.len(); | |
var methodIndex = {}; | |
for ( var name in variables ) { | |
if ( name.right( suffixLength ) != suffix ) { | |
continue; | |
} | |
var runtimeName = name.left( -suffixLength ); | |
methodIndex[ runtimeName ] | |
= this[ runtimeName ] | |
= variables[ runtimeName ] | |
= variables[ name ] | |
; | |
} | |
return methodIndex; | |
} | |
} |
In my current exploration, I'm instantiating the cfmlx.cfc
ColdFusion component on every request (that needs it). In a more robust implementation, I could cache this instance within one of the Application.cfc
life-cycle methods. For now, though, I was just trying to keep everything self-contained.
Another future flexibility refactor would be to separate out the mechanics from the function definitions. Essentially make it easier to define the extensions and polyfills without having to worry about how everything is actually wired together. For that, though, I would have to have an extends
construct, which was unnecessary for the exploration.
At the end of the day, once I started writing e()
instead of encodeForHtml()
, it's hard to ever go back. But, I'm hoping that an explicit include (or module) is enough of an indicator to keep the "magic" of e()
without the confusion of where it came from or where I might go to see the implementation details.
Just because we can put everything in a CFC and rely on inversion of control (IoC), it doesn't mean that we have to. Not everything has to be the path of most resistance. ColdFusion was designed for developer joy. And, the use of cfinclude
to extend the runtime is joyful.
Want to use code from this post? Check out the license.
Reader Comments
Test -- trying to fix CFMail after recent ColdFusion update 21. See discussion here:
https://community.adobe.com/t5/coldfusion-discussions/cf2021-cfmail-error-after-update-21/td-p/15409646
Post A Comment — ❤️ I'd Love To Hear From You! ❤️