Tuesday, January 13, 2009

ColdFusion mixins within singleton objects

Sometimes it's nice to be able to use ColdFusion mixins to include dynamic data at runtime inside CFCs. In the following example, I'm using a CFC to generate a database-driven form, with each form potentially containing multiple templates.


<cfcomponent output="false">

<cffunction name="displayForm" access="public" output="false" returntype="string">
<cfargument name="formID" required="true" type="numeric" />

<cfset var local = StructNew() />

<!--- get the templates associated with the form --->
<cfquery name="local.getTemplates" datasource="#application.datasource#">
select file_path
from form_templates
where form_id = <cfqueryparam value="#arguments.formID#" cfsqltype="cf_sql_integer" />
order by sort_order asc
</cfquery>

<!--- capture the generated html --->
<cfsavecontent variable="local.html">

<!--- loop over the templates and display (include) each template --->
<cfoutput query="local.getTemplates">

<cfinclude template="#local.getTemplates.file_path#" />

</cfoutput>

</cfsavecontent>

<cfreturn local.html />

</cffunction>

</cfcomponent>


Hopefully that's straightforward enough. This works just fine at first, until you store the component in the application scope as a singleton and a developer doesn't properly scope a variable inside one of the included templates, thereby creating a public variable that could bleed into other requests.

The solution is pretty simple: rather than using the cfinclude tag to include the mixin, use a helper function that mimics the functionality but traps the variables.

Doing this requires creating an include helper and an include proxy. The helper is created as a singleton and its purpose is to create instances of the proxy, which is in charge of including the original file.


<cfcomponent displayname="Include Helper" output="false">

<cffunction name="template" access="public" output="true" returntype="any">
<cfargument name="template" required="true" type="string" />

<cfreturn createObject("component","includeProxy").init(arguments.template) />

</cffunction>

</cfcomponent>



<cfcomponent displayname="Include Proxy" output="false">

<cffunction name="init" access="public" output="true" returntype="void">
<cfargument name="template" required="true" type="string" />

<cfinclude template="#arguments.template#" />

</cffunction>

</cfcomponent>


With the include components creating, I can now replace <cfinclude template="#local.getTemplates.file_path#" /> with #helpers.include.template(local.getTemplates.file_path)# and the template variables are now properly hidden from my component.

2 comments:

  1. An alternative, although slightly hackier, way of doing it is to temporarily "redirect" the component variables scope: http://gist.github.com/95715

    By var-ing a new 'variables' scope it effectively localises any variable usage (scoped or unscoped) within both the function call and any included files.

    ReplyDelete