Generic Invocation Wrapper For Core ColdFusion Functions
I happen to love ColdFusion's CFInvoke and CFInvokeArgument tags. They allow user defined functions (UDF) and ColdFusion component functions to be dynamically invoked using variable values. They don't need to be used all that often; but, when the time comes, these two tags can really make life easy. When it comes to core ColdFusion functions, however, these two tags can't help us. Right now, if you want to execute core ColdFusion functions dynamically, you have to fall back on the Evaluate() method. Now, there's nothing wrong with Evaluate(); but, it is neither as easy to use nor is it as flexible as CFInvoke. As such, I wanted to create a component-based proxy to allow CFInvoke-based execution of core ColdFusion functions.
The idea is straightforward: it's still powered by the traditional Evaluate()-based approach; but, it is made generic through the use of ColdFusion's onMissingMethod() function. I have a single component, FunctionProxy.cfc, that has a single method, onMissingMethod(). The onMissingMethod() method contains the code that invokes the requested method using Evaluate(). And since onMissingMethod() will accept any method name thrown at it, it allows all core ColdFusion functions to be invoked by-proxy.
FunctionProxy.cfc
<cfcomponent
output="false"
hint="I am a generic ColdFusion function wrapper for use with reflection-type invokation.">
<cffunction
name="onMissingMethod"
access="public"
returntype="any"
output="false"
hint="I provide ordered-argument-based invocation of core ColdFusion method.">
<!--- Define arguments. --->
<cfargument
name="missingMethodName"
type="string"
required="true"
hint="I am the name of the core ColdFusion method."
/>
<cfargument
name="missingMethodArguments"
type="any"
required="true"
hint="I am the arguments collection used to invoke the missing method."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Create an array into which we can store the collection
of index-based argument references.
--->
<cfset local.orderedArguments = [] />
<!--- Build up the argument references. --->
<cfloop
index="local.argumentIndex"
from="1"
to="#arrayLen( arguments )#"
step="1">
<cfset local.orderedArguments[ local.argumentIndex ] = "arguments.missingMethodArguments[ #local.argumentIndex# ]" />
</cfloop>
<!---
Evaluate and return the method execution. We are
getting the page-based function reference since it
appears that not all core ColdFusion methods can be
invoked directly with Evaluate().
--->
<cfreturn Evaluate(
"GetPageContext().GetPage().#arguments.missingMethodName#" &
"( "
& arrayToList( local.orderedArguments, ", " ) &
" )"
) />
</cffunction>
</cfcomponent>
As you can see, the onMissingMethod() method is programmatically building a snippet of code in the form of:
Evaluate( "function( x, y, z, .... )" )
You'll notice, however, that I am not simply using the name of the target function; rather, I am using the function reference made available within the current page context. I am only doing this because I found out a while back that Image-based functions don't play well with Evaluate(). That might be an old bug, since fixed; but for now, until I re-test, I figured I'd rely on the more universally proven approach.
With this FunctionProxy.cfc ColdFusion component in place, we can now use CFInvoke and CFInvokeArgument to dynamically execute core ColdFusion functions. In the following demo, I am using three different proxy-approaches:
<!--- Create an instance of the ColdFusion proxy. --->
<cfset proxy = createObject( "component", "FunctionProxy" ) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Invoke the RandRange() function with CFInvoke. --->
<cfinvoke
returnvariable="value"
component="#proxy#"
method="randRange">
<cfinvokeargument name="1" value="10" />
<cfinvokeargument name="2" value="20" />
</cfinvoke>
<!--- Output the random value. --->
<cfoutput>
Random Value: #value#
</cfoutput>
<br>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<br>
<!--- Create a struct for use with argumentCollection. --->
<cfset args = {} />
<cfset args[ "1" ] = "10" />
<cfset args[ "2" ] = "20" />
<!---
Invoke the RandRange() function with CFInvoke. Notice that
this time, we are using argumentCollection instead of
multiple CFInvokeArgument.
--->
<cfinvoke
returnvariable="value"
component="#proxy#"
method="randRange"
argumentCollection="#args#"
/>
<!--- Output the random value. --->
<cfoutput>
Random Value: #value#
</cfoutput>
<br>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<br>
<!---
This time, I'm just going to invoke it without andy fancy
reflection (as I might a standard method).
--->
<cfset value = proxy.randRange( 10, 20 ) />
<!--- Output the random value. --->
<cfoutput>
Random Value: #value#
</cfoutput>
As you can see, FunctionProxy.cfc allows us to dynamically invoke core functions using both individually defined arguments (CFInvokeArgument) and a collection of arguments (ArgumentCollection). Because the core ColdFusion functions all rely on ordered arguments, you have to name the arguments using their invocation index. When we run the above code, we get the following output:
Random Value: 16
Random Value: 18
Random Value: 10
If you think the use cases for CFInvoke are few and far between, the use cases for dynamically executing core ColdFusion functions are even more rare. But, if you do need to use a more reflection-based approach to core function invocation, the ability to leverage CFInvoke on top of the core functions can be super useful. Ideally, what I'd like to see down the road is CFInvoke's functionality extended to include core ColdFusion functions.
Want to use code from this post? Check out the license.
Reader Comments
That's truly twisted. I salute you.
@Rick,
Now I just have to come up with a reason to use it :) I know I've wanted it in the past, but it's always so hard to remember for what. Yesterday, I only wanted it for some R&D in CF9... then I remembered that my CF9 is bugging on out on me.
Great!
Hey Ben
I'm kind of a fan of that last way of getting the result: ie.
<cfset value = proxy.randRange( 10, 20 ) />
instead of using cfinvoke. It seems cleaner and simpler to me.
Is there any down side of using that.
Cheers
Marty
@Martin,
The point wasn't just to simply invoke the method. After all, if you just want to call the method using it's pre-defined name, then you could simply call it directly:
randRange( 10, 20 )
The point of the proxy is to *allow* you to be able to invoke the method using CFInvoke if you wanted to.
Jeebus ColdFusion is cool. I was going to do this when you tweeted about it @Ben but I was too busy working ;)
@Martin - the problem with proxy.randrange() style is that you can't do proxy.#whateverCFfunctionIwant#() for syntax reasons. Say for instance at runtime you want to use either ArrayMax or ArrayMin depending on criteria X - with cfinvoke you can skip a cfif block or even encapsulate the logic with a function, like cfinvoke method="#MinOrMax()#"
@Darren,
Yeah, CF is super awesome :)
I like your ArrayMax/ArrayMin example. Thinking back, I think one of the places I really wanted this was allowing someone to toggle between the "Case" and "NoCase" functions (ex. reReplace() / reReplaceNoCase()).
@Ben, @Darren
Thanks - that's just what I needed to know.
Cheers
Marty