Ask Ben: Creating ColdFusion Templates On The Fly

Posted September 21, 2009 at 9:42 AM

Tags: ColdFusion, Ask Ben

Hi Ben, frequent reader, first time writer. I'd be surprised if this hasn't been asked yet, but can't seem to find the answer anywhere. I'm trying to figure out a way to allow an application to build basic Coldfusion pages on the fly. i.e. this is a simple CMS that I'm building for someone, where actual page templates are 10 lines long and have a few CF tags and CF includes in them. Ideally, I'd like to allow the files to be created as soon as they name them, rather than (currently) having to physically create the pages. I have tried cffile and cfsavecontent, but it tried to process the Coldfusion tags, rather than include them in the created cfm file like I'd like. Have you come across anything like this? Thanks!

When it comes to creating ColdFusion code that writes ColdFusion code, the biggest trip-up, as you are seeing, is that your newly written ColdFusion code has a tendency to want to execute rather than being stored as text. In my experience, there are two ways to get around this: One is to work from a template into which you replace variable values; and Two, is to escape your ColdFusion code as you craft it, then unescape it before you write it to disk. Each of these methods has a use case, so I wouldn't say that one is better than the other. In general, picking a methodology comes down to how "templatable" your code is.

For these demos, I am going to keep it extremely simple; we are going to create a ColdFusion file that sets a name value and then includes a display file which outputs the name in a very "Hello World" type way.

Let's explore the templating technique first. In this approach, we are going to start off with an existing ColdFusion file that has place holders for the values we want to set. In this example, I created a "template.cfm" to hold our root template:

template.cfm

 Launch code in new window » Download code as text file »

  • <!--- Set the person's name. --->
  • <cfset name = "$$name$$" />
  •  
  • <!--- Include the display file. --->
  • <cfinclude template="./display.cfm" />

As you can see, this ColdFusion file has a place holder for our name variable. This place holder is defined by "$$name$$". Now that we have this, we can read in the file contents, replace the target variables, and write the new code back to disk:

 Launch code in new window » Download code as text file »

  • <!---
  • Read in the template file as a text file.
  •  
  • NOTE: You do not want to CFInclude thie file as that will
  • evaluate the ColdFusion tags in the process.
  • --->
  • <cfset personTemplate = fileRead(
  • expandPath( "./template.cfm" )
  • ) />
  •  
  • <!---
  • Now that we have the person's display template as a text
  • file, we want to replace out the variables with our known
  • values. In our case, we are simply going to replace out
  • the variable $$name$$ with the appropriate value.
  • --->
  • <cfset personTemplate = replace(
  • personTemplate,
  • "$$name$$",
  • "Molly",
  • "all"
  • ) />
  •  
  • <!---
  • Now that have our template updated with the appropriate
  • names, we can write the new ColdFusion code to a physical
  • file where it can be used.
  • --->
  • <cfset fileWrite(
  • expandPath( "./molly.cfm" ),
  • personTemplate
  • ) />

Here, we are reading in the template file contents and replacing the variable "$$name$$" with the value "Molly." Then, we write the new code back to the disk with at the file "molly.cfm". When we open up this file, we see:

molly.cfm

 Launch code in new window » Download code as text file »

  • <!--- Set the person's name. --->
  • <cfset name = "Molly" />
  •  
  • <!--- Include the display file. --->
  • <cfinclude template="./display.cfm" />

As you can see, our value "Molly" was replaced into the templated content in the appropriate place.

The template technique works really well if the code you are outputting is fairly static. If you really need to create some dynamic code, however, then we need to take a slightly more complex approach in which we actually define the future code using ColdFusion code. At this point, we hit the problem you were facing above - the ColdFusion code we try to output gets executed rather than saved for later. To cope with this problem, we need to escape the ColdFusion code that we want to execute at a later time. To keep this as readable as possible, I use the following escape combinations:

  • #val# becomes <%=val=%>
  • < becomes <%
  • > becomes %>

Not only does this prevent the escaped ColdFusion code from executing, in my particular editor (HomeSite), code that has <% .. %> notation is highlighted in yellow (it thinks it is ASP / PHP code I believe). This makes it extremely clear which code is your "here and now" code and which code is your "future" code.

In this demo, rather than reading in the "template.cfm" code base, we are going to create it from scratch using CFSaveContent and escaped ColdFusion code:

 Launch code in new window » Download code as text file »

  • <!---
  • Store the variable values that we want to put into our
  • new ColdFusion template.
  • --->
  • <cfset name = "Tricia" />
  •  
  • <!---
  • Create the new content for our ColdFusion page. Because we
  • want to WRITE ColdFusion code and NOT EXECUTE ColdFusion
  • code, we need to escape our CF Tags. So, in the following
  • content, our ColdFusion tags will look more like ASP tags.
  •  
  • NOTE: Notice that the # marks below ARE evaluated in order
  • to complete the template.
  • --->
  • <cfsavecontent variable="templateCode">
  • <cfoutput>
  •  
  • <%!--- Set the person's name. ---%>
  • <%cfset name = "#name#" /%>
  •  
  • <%!--- Include the display file. ---%>
  • <%cfinclude template="./display.cfm" /%>
  •  
  • </cfoutput>
  • </cfsavecontent>
  •  
  • <!---
  • Now that we have our new ColdFusion code in memory, we need
  • to write it to disk. But, before we do that, we need to
  • unescape the code so that it will propertly execute later on.
  • --->
  •  
  • <!---
  • While I didn't have any escaped HASH marks in the current
  • code, I would normally escape them with something like:
  •  
  • <%=CF_VARIABLE=%>
  •  
  • ... which we need to turn into ...
  •  
  • #CF_VARIABLE#
  •  
  • As such, let's unescape these to single hash values. I am
  • going to these ones first so they don't interfear with the
  • other escaped ColdFusion tags.
  • --->
  • <cfset templateCode = reReplace(
  • templateCode,
  • "<%=|=%>",
  • "##",
  • "all"
  • ) />
  •  
  • <!--- UNEscape opening bracket. --->
  • <cfset templateCode = replace(
  • templateCode,
  • "<%",
  • "<",
  • "all"
  • ) />
  •  
  • <!--- UNEscape closing bracket. --->
  • <cfset templateCode = replace(
  • templateCode,
  • "%>",
  • ">",
  • "all"
  • ) />
  •  
  • <!---
  • Now that our code is fully UNEscaped, we can write it
  • out to the disk.
  • --->
  • <cfset fileWrite(
  • expandPath( "./tricia.cfm" ),
  • templateCode
  • ) />

As you can see, this is a bit more work, but it gives us a lot more flexibility in the code we need to generate. Because we are escaping the ColdFusion code within the CFSaveContent buffer, it does not execute right away. But, that is not to say that standard ColdFusion code in the CFSaveContent buffer will not execute; if you look at my example, you'll notice that the hash marks we left in (#name#) did execute right away, putting the value "Tricia" into our code buffer value.

When we run this code, we produce the following template, tricia.cfm:

tricia.cfm

 Launch code in new window » Download code as text file »

  •  
  • <!--- Set the person's name. --->
  • <cfset name = "Tricia" />
  •  
  • <!--- Include the display file. --->
  • <cfinclude template="./display.cfm" />
  •  

One thing you'll notice here is that all the tabbing in the code creation file gets left in the generated code file. You can, of course, start to tweak the tabbing and line breaks in the code that generates your template; but, at that point, you have to ask yourself where the readability is most important: in the file you generated? Or, in the file that generated the code? That's not a rhetorical question - it really depends on how you are going to use the generated file. If it is a file that will be updated manually after generation, then readability in the generated file is most important; however, if it is never touched again, then I would say readability in the generator is the most useful.

All code generation that I have ever seen operates off of one (of both) of these two techniques. Like I said above, each is good in its own way and selecting a technique really depends on how static your code can be. The more static it is, the easier a template is to work with; the more dynamic it has to be, the more you will have to mix escaped and unescaped ColdFusion code files. I hope this helps!

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page



Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Sep 21, 2009 at 10:16 AM // reply »
6 Comments

@Ben a method I've found for doing a similar thing is to Evaluate and then immediately DE the content.

So for example:

<cfset name = "Tricia" />
<cfset templateCode=Evaluate(DE('<cfset name = "#name#" />
<cfinclude template="./display.cfm" />'))>
<cfset fileWrite("#uploadpath#/test.cfm" ,templateCode) />

This avoids the necessity for finding and replacing escaped var's

:)


Sep 21, 2009 at 10:38 AM // reply »
3 Comments

Thanks for the post Ben! This is definitely a big help, and I'll look into it. I was able to also achieve what I wanted using Java to write out the file. Trevor's blog post her was a big help: http://www.burnette.us/blog/index.cfm/2006/1/30/Using-Java-Instead-of-cffile-to-Write-to-Disk


Sep 21, 2009 at 10:39 AM // reply »
35 Comments

I just went through this exact same thing. I think this is where reMatch really shines nicely. No matter what your variable holder is (mine was {var}) you can use a regular expression to return an array of all your "variables" in the document. If your variables are named the same as your replacement variables your replace function can be pretty easy. Here is a quick write up on reMatch and how I was using it.

http://www.danvega.org/blog/index.cfm/2009/9/17/reMatch-returns-an-array-of-matches


Sep 21, 2009 at 10:43 AM // reply »
6,516 Comments

@Tom, @Steve,

While the use of string concatenation works for delayed evaluation of the code you generate, I'd be cautious about using it as it does not lend itself well to readability / maintainability; that is the beauty of using CFSaveContent - very readable.

@Dan,

Word up - regular expressions are awesome for this kind of thing.


Sep 21, 2009 at 5:28 PM // reply »
9 Comments

Based on the original question it seems that Steve only needs to create CF templates so that pages from CMS can have proper URLs.

I would suggest to use mod_rewrite or similar approach to rewrite pretty much any URLs in the site to a page handler which can be traditional CF page. This is much cleaner specially when moving site from server to server (e.g. testing to production).

Also I think it's necessary to point out that writing CF templates on the fly can be a massive security whole. If for example on this code:

<cfset name = "$$name$$" >

value is taken directly from the client then malicious user could set it to something like

nothing"> <cfquery>drop everything</cfquery><cfset name="still nothing

I guess You get the point...

Hope this helps.

Tero


Sep 21, 2009 at 5:51 PM // reply »
40 Comments

XML and XSL work great together for generating cfm's and cfc's and practically any file you want.


Sep 21, 2009 at 5:58 PM // reply »
6,516 Comments

@Tero,

You make some excellent points. In the long run, using some URL rewriting would probably be the best way to go, especially if you ever need to change up your strategy.

As far as a security hole, that would only be if the data written to the template was user-data; while that is possible, the askee gave me some code samples that they were working on and the data being written was more along the lines of a database-generated ID.

But, most excellent points!

@Hatem,

My only concern with XSLT is that it's not friendly :) I've used it a bunch, but it just never is fun. Plus, I find the output control to be very tedious.


Sep 22, 2009 at 10:53 AM // reply »
3 Comments

In ColdFusion 9 this can be done easily by using VFS.

http://www.coldfusionjedi.com/index.cfm/2009/7/25/Very-simple-very-ugly-CMS-built-with-ColdFusion-9


Sep 22, 2009 at 10:55 AM // reply »
6,516 Comments

@Sumit,

If you're gonna plug Ray's post, then I'm gonna have to plug mine as well ;)

http://www.bennadel.com/blog/1650-Learning-ColdFusion-9-The-Virtual-File-System-RAM-Disk-.htm


Sep 22, 2009 at 11:09 AM // reply »
3 Comments

@Ben,

hahaha. I think I missed your post on that. But, I should have known better to think you didn't write on that.


Oct 17, 2009 at 12:03 AM // reply »
1 Comments

@Ben,
Thanks for this...really helped with an issue I was having with static cfm pages and changing navigation menus.


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 20, 2009 at 11:32 PM
Five Months Without Hungarian Notation And I'm Loving It
I've used headless camel case for years for not only ColdFusion variables, but also SQL tables and fields... pretty much everything involving code. I also subscribe to the "don't abbreviate and clea ... read »
Nov 20, 2009 at 11:00 PM
Five Months Without Hungarian Notation And I'm Loving It
@Marcel, Yeah, I always err on the side of longer but more readable variable names. As for the camel casing of CF methods and the headless camel casing of custom items, I get around this by always ... read »
Nov 20, 2009 at 10:56 PM
Five Months Without Hungarian Notation And I'm Loving It
I use the following and love it: my.namespace.MyComponents.functionMethodsOrUDF() CONSTANT_VALUES_OR_PROPERTIES One thing I always try is to CamelCaseBuiltInColdFusionFunctions() so others can tell ... read »
Nov 20, 2009 at 5:38 PM
Learning ColdFusion 8: CFImage Part I - Reading And Writing Images
Hi Ben, Great article. I've been looking around to see if ColdFusion image engine can programatically create the following "wrap around" effect: http://www.creativepro.com/article/photoshop-s-she ... read »
Nov 20, 2009 at 5:35 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
@Dave: I talked to Gert he suggested: <cfhttp method="get" url="http://{some cf website}" result="stuff" addtoken="yes" /> Note the addition of cfhttp attribute addtoken. That should persist y ... read »
Nov 20, 2009 at 5:23 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
@Todd, Ahh, gotcha, yeah that makes sense. ... read »
Nov 20, 2009 at 5:17 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
Ben, sorry if I didn't make this clear. You can make it work like that if you want, just put <cfset session.foo = 1> (and <cfset application.foo = 1>) in your OnRequestStart() and it reve ... read »