ColdFusion CFInvoke Eliminates The Need For Evaluate() When Dynamically Executing User Defined Functions
I have touched on this topic before, but I thought with all my recent posts on Object Oriented Programming in ColdFusion 8, it would be good to give a more fleshed out demonstration of how hardcore CFInvoke rocks. When it comes to Object Oriented Programming and the power of ColdFusion 8's OnMissingMethod(), we have things at our disposal like generic getters and setters and dynamic "behaviors." Often times, with this power comes the requirement to dynamically evaluate a user defined function.
In old-school programming, this kind of dynamic method execution would be powered by ColdFusion's Evaluate() method:
<cfset Evaluate( "THIS.#MethodName#()" ) />
While this type of execution is still required for things like built-in ColdFusion methods and dynamic Java object invocation, when it comes to User Defined Functions (anything created via a CFFunction tag) CFInvoke and CFInvokeArgument are all that we need. CFInvoke allows us to dynamically execute methods that use both named and ordered arguments.
To expand on this, let's define three ColdFusion user defined functions (UDFs) for display a person's name:
<cffunction
name="GetDisplayName"
access="public"
returntype="string"
output="false"
hint="I take a first and last name and display in standard order.">
<!--- Define arguments. --->
<cfargument
name="FirstName"
type="string"
required="true"
hint="I am the first name."
/>
<cfargument
name="LastName"
type="string"
required="true"
hint="I am the last name."
/>
<!--- Return display name. --->
<cfreturn "#ARGUMENTS.FirstName# #ARGUMENTS.LastName#" />
</cffunction>
<cffunction
name="GetInverseDisplayName"
access="public"
returntype="string"
output="false"
hint="I take a first and last name and display in inverse order.">
<!--- Define arguments. --->
<cfargument
name="FirstName"
type="string"
required="true"
hint="I am the first name."
/>
<cfargument
name="LastName"
type="string"
required="true"
hint="I am the last name."
/>
<!--- Return display name. --->
<cfreturn "#ARGUMENTS.LastName#, #ARGUMENTS.FirstName#" />
</cffunction>
<cffunction
name="GetInformalDisplayName"
access="public"
returntype="string"
output="false"
hint="I take a first and last name and display an informal name.">
<!--- Return informal name. --->
<cfreturn ARGUMENTS[ 1 ] />
</cffunction>
Each of these three UDFs operates in a different manor. The first two use name arguments. The last one does not have any define arguments and relies on ordered arguments to return its value. What we're gonna do is loop over all three method names and dynamically execute them using CFInvoke and CFInvokeArgument:
<!--- Loop over the display functions. --->
<cfloop
index="strMethod"
list="GetDisplayName,GetInverseDisplayName,GetInformalDisplayName"
delimiters=",">
<!--- Get the meta data for the current method. --->
<cfset objMetaData = GetMetaData( VARIABLES[ strMethod ] ) />
<!--- Invoke the given method. --->
<cfinvoke
method="#strMethod#"
returnvariable="strName">
<!---
Check to see if there are PARAMETERS for this method.
If there are none, then our target function does not
have defined arguemnts and we have to use index
arguments.
NOTE: We could *always* use INDEX arguments, but I
am performing this check to demonstrate the dynamic
nature of ColdFusion (woohooo!).
--->
<cfif ArrayLen( objMetaData.Parameters )>
<!---
There are parameters so use the named arguments
made available in our meta data.
--->
<cfinvokeargument
name="#objMetaData.Parameters[ 1 ].Name#"
value="Ben"
/>
<cfinvokeargument
name="#objMetaData.Parameters[ 2 ].Name#"
value="Nadel"
/>
<cfelse>
<!---
There are no parameters so we have to use the
index-based arguments.
--->
<cfinvokeargument name="1" value="Ben" />
<cfinvokeargument name="2" value="Nadel" />
</cfif>
</cfinvoke>
<!--- Output result. --->
<p>
<strong>#strMethod#</strong>: #strName#
</p>
</cfloop>
In the above demo, we are using CFInvoke to execute each method. For the two methods that are using named arguments, we grab the argument names using the GetMetaData() of that method. Technically, we could use the index-based arguments for each of these methods, but I used both to demonstrate the power and flexibility of ColdFusion. Running the above code, we get the following output:
GetDisplayName: Ben Nadel
GetInverseDisplayName: Nadel, Ben
GetInformalDisplayName: Ben
As you can see, when dealing with user defined functions, CFInvoke does exactly what Evaluate() used to do.
If they both work, which is better?
To me, situations like this are always a question of "intent". Is your intent to execute a line of code? Or, is your intent to dynamically execute a given method? The outcome of these might be the exact same; however, I feel like using CFInvoke demonstrates a greater understanding of the language and its objects as well as more clearly explains the intent of the action - we are using CFInvoke to INVOKE a method. To me, this is a more powerful statement than, "we are using Execute() to execute some code."
Want to use code from this post? Check out the license.
Reader Comments
About four months ago I did a BPM project using Inubit IS. I wanted a way to be able to call a single method in a web-service and by passing in a variable name (that happend to be the name of a method) call that method to do the task. All very "SOA-ish".
I created a gateway cfc with a single remote method that acted instantiated the component based on the argument passed to the function pretty much exactly as you describe in this blog entry.
What is great is that we had a single point of entry and we could very easily add new components if our BPM process needed to change, or completely rewrite a component if we discovered optimization points.
Glad to see that others use this methodology kind of like a justification of how I think :)
That's interesting... thanks for the post Ben. :) I remember Chris Philips (cfchris) over at DealerPeak a while back talking about having some difficulty figuring out how to work things with onMissingMethod when he wasn't sure if the method would be called with named or indexed arguments... I think he ultimately let it slide and didn't implement the onMissingMethod technique for what he was doing...
by comparison, when I went to implement onMissingMethod, I already had a solution for invoking by indexed arguments that was left over from work I'd done a long time ago on CF5. My answer was a strategic use of evaluate() by looping over the passed in array and building up a string like "args[1],args[2]" for the arguments, where "args" was the name of the array.
What I didn't realize when I started working with onMissingMethod was that you could use the number in the name attribute of cfinvokeargument ... which is why I was so confused when I got the error telling me the name was required, I thought (as I'm sure Chris did), what?! I can't just pass them in indexed? Imo if you leave the name attribute out, it should assume an indexed argument and just count the cfinvokeargument tags starting at 1... that to me would have been more intuitive... It just didn't occur to me that I could use the number in that attribute.
But... aside from lamenting the CF team's implementation details, thanks for making me look like a newb! :) I'll have to go tweak my lazy-loading library CFC now, because this is undoubtedly more efficient than my evaluate technique.
@Gary,
Sounds pretty cool. I love being able to turn a variable name into a method execution.
@Ike,
I didn't know about the index passing in CFInvokeArgument until last year. Sean Corfield actually suggested it as something to try. As it turns out, it works and it works well.
The ARGUMENTS scope is cool that way in that it allows for both named and ordered argument references. But, I agree with you: if you leave out the Name attribute, the ordered index should be assumed! That would make the most sense.
@Ben
If you're doing this locally inside the cfcomponent then cfinvoke isn't actually required, nor is evaluate. Using the below code works out to be about 14% faster for positional arguments and 16% faster for named arguments than using cfinvoke. cfinvoke is about 3x as slow a regular function call.
function invokeMethod() {
var method = "";
var i = "";
// need to handle named arguments differently due to ArgumentCollection quirks
if( structKeyExists(arguments,"_methodName") ) {
method = this[arguments._methodName];
structDelete(arguments,"_methodName");
} else {
method = this[arguments[1]];
for( i=1; i lt structCount(arguments); i = i + 1 ) {
arguments[i] = arguments[i+1];
}
structDelete(arguments,structCount(arguments));
}
return method(argumentCollection=arguments);
}
component.invokeMethod("add",1,2);
component.invokeMethod(_methodName="add",a=1,b=2);
If we know the argument count we can make this even more efficient, like calling a setter.
function setProperty(name,value) {
var method = this["set#name#"];
method(value);
}
This is about 36% faster than the fastest cfinvoke you can work up (using name="set#name#"). The cfinvoke method is about 2x as slow as a regular function call.
This is all possible because calling a function always binds it to the calling page's context. So we can exploit that by getting the function by accessing the this scope by name, and then invoking it directly since the context is still inside the component.
As far as function call overhead goes, I needed to call these functions about 10k times in a row to get statistically significant differences (getting the difference into the double digits, with totals over 100ms). So I certainly wouldn't worry about the performance difference all that much. It does demonstrate another approach though.
Well I've found a snag, so I figured I'd share it here... Not sure how many people will run into this, it probably depends a lot on your feelings about cfscript...
So here goes... when you create a function in cfscript in the latest version of CF (though there is talk of this changing in 9/Centaur), you don't get to declare the data type, but you do get to declare the name for any arguments that are required... Unfortunately, you can't use argumentcollection if a cfscript-based function has index-based optional arguments... Which means reverting to cfinvoke or one of the other techniques described here. Well that's all fine and good, until you have a cf-script based function that has a required argument...
<cfscript>
function thing(a) { ; }
</cfscript>
Then when you attempt to use cfinvoke with a numbered invoke argument like this:
<cfinvoke method="thing">
<cfinvokeargument name="1" value="yadda" />
</cfinvoke>
It produces an error stating that the required argument "a" was not provided... Apparently in the current version it won't convert that argument[1] to arguments.nameofargument if you wrote the function with cfscript. And since I've got a bunch of functions in cfscript because they've always worked, dating back to CF5 days, that means for me at least it's going to take a fair amount of work to get to cfinvoke. I'd like to eventually update the framework actually so that all the functions are written with cffunction, but I'm not sure I can justify spending the time to do that any time soon.
It is however still good to know. :)
@ike
Using the function pointer approach like in my post should work just fine.
name = "thing";
func = variables[name];
func( "yadda" );
You should also report this bug to Adobe. :)
@ike
Hmm, I just tested that (copied your exact code) and I'm not getting that error with cfinvoke. Do you have a test case for your issue?
This code reproduces the issue for me on CF8 / WinXP / Apache (though I expect cf8 is the only variable that matters really)
http://on.tapogee.com/cfinvoke.zip
@ike
That code sample causes no errors for me.
Make sure you've applied the CF8.0.1 update; it changed the way invocation of indexed arguments works.
@Elliott,
Good stuff. I forgot about using function pointers. Those rock as well. I think this just goes to show how dynamic ColdFusion is. With all the ways there are to invoke a method, it just goes to show that Evaluate() is really a last-resort.
@Ike,
I believe I tried that and it worked fine on my end. But, as Elliott points out, I am using the most up-to-date ColdFusion version.
Yep, that's probably what's wrong with my system. I probably haven't installed the updater for cf8 on my dev box yet...
though it still gives me pause about rolling out the alternative in the framework core because I suspect that issue may have existed with CF7 and I want to leave folks with a copy that will work (if *slightly* less efficiently) without needing to upgrade their server.
I say slightly because in that case it only even uses evaluate the first time the function is called as part of my new lazy-loading libraries (that I've mentioned before with pride). :) So you'd never run into a situation with that particular component where it was using evaluate with even close to the amount of repetition necessary to degrade performance. One o' these days, maybe I'll have time to update all the functions to cffunction. :)
@Ike,
I don't think any of us are trying to villainize Evaluate(); so, if you need to use it, then go for it.
@Ben - Oh I didn't get that impression... I was just sort of thinking out loud about it... I really would like actually to convert it to cfinvoke, so my comments about not doing that are all about my wanting to rather than any perception that people think I should.
Hi Ben,
I was trying to pass a list of values as an argument into a CFC method. I have a query in the method which is intended to retrieve the results by comparing a number (ex: 20095567)with the list of numbers send as an argument.
But for some reason the query is not being executed! Am i messing up with the scope of variables?
@Sam,
Are you saying the function is not executing? Or the query inside it is not executing?
Hi Ben,
Query inside is not executing!
I even tried dumping the data.....
@Sam,
What happens when you try to dump out the query result?
It is showing an output dumped with nothing in it
@Sam,
So, it sounds like the query is running, you're just not getting back the records you except.
Yes! That's true...i am not getting the query and ColdFusion is throwing me an error saying Error executing database query.
@Sam,
I am confused. How can you both be dumping out the query results AND getting an error executing the query. Something seems amiss.
As i mentioned you, the query gets dumped into a variable but just shows an empty structure in the output.
I tried to change the variable scopes as well, but it didnt work for me.
Is there is any problem sending a list of values wrapped into a variable (with variables scope) as an argument to the function.
HI Ben,
I have an issue using ajax. Hope you can answer it with ease.....
I am trying to fill a div tag dynamically when the page loads from an external file (may be .cfm or .htm) There is some dynamic content on my page as well.
Question is how do i make an ajax call. I know it can be done with <cfdiv> bind attribute. But i am not sure.....
Please help me out!!
@Sam,
I am sorry, I do not understand what you are saying regarding your query. I do not think I can help you - there is simply something I am missing.
As far as AJAX, simply search my site for AJAX posts - I have many of them that should help you:
http://www.google.com/search?q=site%3Abennadel.com+ajax
I am having a .cfm file and i wanted to load the file in a div content!