Validating And Documenting Complex Object Structures With CFParam In Lucee CFML 5.3.7.47
At InVision, we generate our transactional emails by including a CFML template into a <CFSaveContent>
buffer; and then, using that buffer as the body
attribute of a CFMail
tag. And, since the definition of that template feels somewhat "far away" from the context in which it is being consumed, I've gotten into the habit of parameterizing the template variables using CFParam
tags. This way, it's intensely obvious which variables are being used in the template; and, if I accidentally forget to define a variable during refactoring, the template will blow-up when I go to test it. Part of what makes this easy to do is the fact that the CFParam
tag can validate complex object structures. I don't often use it this way, so I thought it would be interesting to share in Lucee CFML 5.3.7.47.
At a high-level, the CFParam
tag looks at a variable reference and can ensure that it exists and that it has a given type. So, for example:
<cfparam name="favoriteColor" type="string" default="ff3366" />
This looks at the favoriteColor
variable, checks to make sure that it is of type String
; and, if it doesn't exist, it defines and defaults the variable to be ff3366
. If the variable didn't exist and I omitted the default
attribute, ColdFusion would raise a runtime exception.
Now, in the vast majority of cases, the name
attribute in the CFParam
tag contains a simple variable reference. However, it can contain just about any kind of reference, including deep object and array references. Which means, we can get it to validate object properties and array indices.
To see what I mean, let's look at a ColdFusion custom tag that accepts a project
attribute where project
is a complex Struct that contains other Structs and Arrays. We can use the CFParam
tag to validate and document much of that complexity:
<cfscript>
// For safety and DOCUMENTATION (regarding which values are needed within this
// template), validate the general structure of the attributes.
param name="attributes.project" type="struct";
param name="attributes.project.id" type="numeric";
param name="attributes.project.name" type="string";
param name="attributes.project.createdAt" type="date";
param name="attributes.project.owner" type="struct";
param name="attributes.project.owner.id" type="numeric";
param name="attributes.project.owner.name" type="string";
param name="attributes.project.screens" type="array";
// We can't validate screen structures unless we have at least one screen.
if ( attributes.project.screens.len() ) {
param name="attributes.project.screens[ 1 ].id" type="numeric";
param name="attributes.project.screens[ 1 ].name" type="string";
param name="attributes.project.screens[ 1 ].clientFilename" type="string";
}
// .... consume attribute values ....
dump( attributes.project );
</cfscript>
As you can see, we're using the CFParam
tag here to validate both Structs and Arrays, including their various properties and indices.
Now, when I go to consume this CFML template as a module with attributes, the inputs are validated at runtime:
<cfscript>
project = {
id: 1,
name: "Public Site Redesign",
createdAt: createDate( 2021, 1, 15 ),
owner: {
id: 4,
name: "Ben Nadel"
},
screens: [
{
id: 101,
name: "Home Page",
clientFilename: "home@2x.png"
}
]
};
module
template = "./MyTag.cfm"
project = project
;
</cfscript>
Right now, everything works as expected. But, imagine that I was going to refactor this workflow, and I accidentally left-out the clientFilename
property in the screens collection. If I were to run that code against the same CFML template, I'd get this runtime error:
As you can see, the CFParam
tag correctly validated this deep-object/array reference:
attributes.project.screens[ 1 ].clientFilename
Of course, it only validated the first index. But, it's probably a good bet that all the array indices are defined in the same manner. And, at the end of the day, this is more about documentation and safety than it is about an exhaustive test.
I Don't Do This All The Time
To be clear, I don't do this all the time - using the CFParam
tag to validate template variables. But, sometimes, it gives me a sense of comfort that there's a place that acts as the gate-keeper to the template body. In fact, I kind of think of the CFParam
tag-block much like the Arguments collection in a Function: it defines and validates the "call signature".
As I mentioned above, I do find this particularly helpful in my HTML Email templates. Since these templates don't change often - and they're not right next to the code that consumes them - having the breadth of values parameterized at the top really helps when trying to remember what data is needed for the email.
Anyway, if nothing else, this may just demonstrate that the CFParam
tag is perhaps more flexible than maybe you realized it was.
Want to use code from this post? Check out the license.
Reader Comments
I love
cfparam
and use them often. I think it's so helpful to know which variables are expected in the page below, especially since my team tends to write long, imperative code segments.I was super excited to read the title. I was hoping you'd show me a syntax for CFParam like this...
But maybe that's not anymore readable :-/
@Chris,
That actually reminds a bit of how I do things on the TypeScript side. One of the most awesome things about TypeScript is that it has structural types. So, the "type" concept isn't hard-coded to a given reference; instead, it's tied to the shape of an object.
So, in my Angular files, I'll often have something like this:
Anyway, not really here nor there, but what you said made me think of the way TypeScript uses interfaces.
@Ben
Exactly! I haven't done any TypeScript in a while, but that's precisely what I'm talking about. Would be nice if CFML moved along in that direction as well. CFScript definitely has a JS feel to it in many cases :)
@Chris,
It would be interesting. I definitely do love the fact that my ColdFusion and my JavaScript are converging on a particular look-and-feel. In fact, just the other day, I had written an algorithm in ColdFusion that one of my team-members ported over to JavaScript in a different repo; and, we basically just removed the
argument
andfunction
attributes and the rest of it just worked :D It was very cool.