coldfusion 9 dynamically call method - coldfusion

I'm attempting to build a method call from strings that have been passed into an object that refer to another object.
normally when calling an object we write the code like this:
application.stObj.oNewsBusiness.getNews(argumentCollection=local.stArgs);
However what I have done is created an array that contains the object name, the method name and the argument collection.
<cfscript>
local.stArgs = {};
local.stArgs.nNewsID = 19;
local.stArgs.sAuthor = "John";
local.aData = [];
local.aData[1] = local.stArgs;
local.aData[2] = "stObj.oNewsBusiness";
local.aData[3] = "getNews";
</cfscript>
however i am struggling to recombine all this to be a method call.
UPDATE using suggestion but still with issue
While cfinvoke seems to work for:
<cfinvoke component="#application.stObj.oNewsBusiness#" method="#local.sMethod#" argumentcollection="#local.stArgs#" returnvariable="local.qData"></cfinvoke>
it doesn't work when doing something like:
<cfscript>
local.stArgs = local.aData[1];
local.sObject = local.aData[2];
local.sMethod = local.aData[3];
</cfscript>
<cfinvoke component="application.#local.sObject#" method="#local.sMethod#" argumentCollection="#local.stArgs#" returnvariable="local.qData"></cfinvoke>
it generates an error:
Could not find the ColdFusion component or interface application.stObj.oNewsBusiness

CFInvoke is generally used to handle dynamic method calls.
http://help.adobe.com/en_US/ColdFusion/9.0/CFMLRef/WSc3ff6d0ea77859461172e0811cbec22c24-7e0a.html
CFInvoke has an argumentcollection attribute so you can pass your arguments in the way you are used to.

Dan is correct CFInvoke is the way to go
<cfinvoke component="#mycomponentname#" method="get" arg1="#arg1#" arg2="#arg2#" arg3=..>

<cfinvoke component="application.#local.sObject#" method="#local.sMethod#"argumentCollection="#local.stArgs#" returnvariable="local.qData"></cfinvoke>
from your update won't work because there are no # signs around the component variable.
You could do
<cfset local.componentName = "application." & local.sObject>
<cfinvoke component="#local.componentName#" method="#local.sMethod#"argumentCollection="#local.stArgs#" returnvariable="local.qData"></cfinvoke>
There's probably an inline way of combining application. with the variable on the cfinvoke call, but I don't know off the top of my head.
Edit: Dan Wilson's comment does it better in an inline way.

Related

Invoking function with dynamic array of arguments

If I have
<cfset arr_arguments = ["a","b","c"]>
<cfunction name="someFunction">
<cfargument name="someArgumentOne">
<cfargument name="someArgumentTwo">
<cfargument name="someArgumentThree">
</cffunction>
Is there any way to invoke someFunction with the arguments arr_arguments, similar to someFunction("a","b","c")? I of course know I can use argumentCollection to pass a (keyed) structure to a function, but I am specifically asking about passing in an (keyless) array. In JS this could be easily done with someFunction.apply(this,arr_arguments), but in coldfusion I just can't find any way of doing this.
Unnamed arguments are passed into a function as a structure with numeric keys matching the positions of the arguments defined in the function arguments. So instead of passing the named arguments, you can convert your array to a struct with numeric keys, and then pass the struct in with argumentCollection:
<cfset arr_arguments = {"1"="a","2"="b","3"="c"}>
<cfset someFunction(argumentCollection=arr_arguments)>
You can easily convert an array to a struct with numeric keys like this:
<cfset args = {}>
<cfloop from="1" to="#arrayLen(arr_arguments)#" index="i">
<cfset args[i] = arr_arguments[i]>
</cfloop>
In Coldfusion 10, you can use the invoke function to do this. It appears to be an undocumented way to pass an array of arguments in order. Example in cfscript:
invoke('', 'someFunction', ['a','b','c']);
The first argument of invoke is the component name (or an empty string for UDFs). The second is the function name, and third is an argument array. Note that both component and function names must be passed as strings.
I tested this both with and without defined argument names and the order of arguments was preserved.
Ok if you are looking to call a function with an arbitary array of arguments the best option I can suggest is to make a wrapper function. See below example code
<cfscript>
function testMe(
required string arg1,
required string arg2
) {
writedump(arguments);
}
function invokeFunction(
required inFunction,
required array functionArguments
) {
var stcArguments = {};
// Introspect to get the method parameters
var arrFunctionArguments = GetMetaData(arguments.inFunction).parameters;
// Now figure out what we are iterating to
var numArgumentsToParse = Min(ArrayLen(arguments.functionArguments),ArrayLen(arrFunctionArguments));
// Populate the arguments structure
for (var i=1;i<=numArgumentsToParse;i++) {
stcArguments[arrFunctionArguments[i].name] = arguments.functionArguments[i];
}
// And try to call it
return arguments.inFunction(
argumentCollection = stcArguments
);
}
invokeFunction(testMe,[1,2]); // Works fine
invokeFunction(testMe,[1,2,3,4]); // Just skips 3 and 4
// invokeFunction(testMe,[1]); // Errors due to not enough arguments
// invokeFunction(fakeFunctionName,[1]); // Errors due to undefined function
</cfscript>
This will of course error if either of the following happen
One or more arguments are not of correct type
The function doesn't actually exist
Not all required arguments are passed in
But you can handle that outside. This will use introspection to figure out the argument names, populate the structure accordingly, and call the requested function.
Here's a UDF which will do that for you:
<cfscript>
function test_function(a,b,c){
writeOutput("arguments.a"&arguments.a&"<br>");
writeOutput("arguments.b"&arguments.b&"<br>");
writeOutput("arguments.c"&arguments.c&"<br>");
return arguments.a & "::" & arguments.b & "::" & arguments.c;
}
function invoke_positional(func,arr){
var metaData = GetMetaData(func);
var args={};
for(var pos=1;pos<=ArrayLen(arr);pos++){
args[metaData.parameters[pos].name]=arr[pos];
}
return arguments.func(argumentCollection=args);
}
data = ["StringOne","StringTwo",22];
result = invoke_positional(test_function,data);
</cfscript>
<cfoutput>
<p>
Returned: #result#
</p>
</cfoutput>
invoke_positional works by using the UDF's metadata to build up a named set of parameters and then uses them with argumentCollection=
So, one option I have found out to work is to create an arguments object separately from the actual real arguments scope and simply add the values to it using a numeric index.
<cfset args_input = createObject("java","com.naryx.tagfusion.cfm.engine.cfArgStructData").init()>
<cfloop from="1" to="#arraylen(arr_arguments)#" index="i">
<cfset args_input[i] = arr_arguments[i]>
</cfloop>
<cfset someFunction(argumentCollection=args_input)>
If you encounter this situation using adobe coldfusion I would advice you to try
<cfset correctClass = arguments.getClass().getName()>
To get the string to pass to the createObject. Or if that doesn't work just have a dedicated function to return an empty arguments object
<cfset args_input = this.generateArgumentObject()>
and
<cffunction name="generateArgumentObject">
<cfreturn arguments>
</cffunction>
Oh well, all I know for sure is that it will work for sure in OpenBD and based on a post I saw in the past on Ben Nadel's blog I am pretty sure this should work in Adobe ColdFusion as well.

Using "var this" inside remote CFC methods

I've inherited a project where there are a number of remote CFC's opened up for some Ajax requests and inside most methods in the CFC have the following:
<cfset var this.response = true />
Now I've never seen the var and this scope used together like this so I'm really not sure what to make of it so I guess my questions is:
Are there any issues with how this was coded? If so, are they major enough that I should put in
the effort to update all the CFC's to something like <cfset var
req.response = true />?
Here is a quick example of what I'm seeing:
<cfcomponent>
<cffunction name="check_foo" access="remote" returnformat="plain">
<cfargument
name = "isfoo"
type = "string"
required = "false"
default = "nope"
hint = "I check the string for foo"
/>
<cfscript>
/*setup new response*/
var this.response = false;
/*check for foo*/
if( !findnocase( "foo", arguments.isfoo ) ) {
/*no foo!*/
this.response = false;
}
return this.response;
</cfscript>
</cffunction>
</cfcomponent>
.
Updates:
Based on the feedback/answers below I've replace all instances of var this. Thanks again to everyone that helped out!
.
update: upon checking your dump, the "this" in var this is still this this scope, not local.this.
It is setting the response to the this scope, and it works in this case because because the CFC is instantiated every time it's being invoked remotely. However, it'd be best to rename this into something else to ensure thread-safety in case the method is invoked by other CFC as public method.
Using var this is the same as using this.
Dumping the local scope will include local variables as well as the Arguments and This scopes. (Can't find this documented; but I get this result in a bare cfc, and you got it in your screenshots.)
Because your function is access="remote" you'll be getting a new instance of the cfc on every call, and therefore a bare This scope. So those are "safe", but still a bad idea.
If there is any use of var this in non-remote functions then you will be getting undesired persistence and may suffer race conditions that result is invalid data.
Relevant CF documentation:
"Methods that are executed remotely through Flash Remoting and web services always create a new instance of the CFC before executing the method."
"Variable values in the This scope last as long as the CFC instance exists and, therefore, can persist between calls to methods of a CFC instance."

How do I call a second function within the same CFC in the APPLICATION scope?

I am using ColdFusion 9.0.1.
Let me start by stating that I may not be asking the right question. Since each function works independently and fails only when one function calls another, I am thinking that the problem is in how the function is called.
I am creating an application variable that contains a structure. The structure contains the reference to an object, orders.cfc.
if (not isDefined("APPLICATION.AppInfo") or not isStruct(APPLICATION.AppInfo)) {
APPLICATION.AppInfo = structNew();
APPLICATION.AppInfo.objOrders = createObject("component", "globaladmin.orders");
}
I am able to successfully access the methods in the orders.cfc like this:
OrderItemList = APPLICATION.AppInfo.objOrders.orderItemList(URL.Customer);
I have methods in the orders.cfc that call other methods in the order.cfc, kind of like this (faked for simplicity):
<cffunction name="orderItemList">
<cfscript>
LOCAL.RandomNumber = getRandomNumber();
return LOCAL.RandomNumber;
</cfscript>
</cffunction>
<cffunction name="getRandomNumber">
<cfscript>
LOCAL.SomeNumber= randRange(0,10);
return LOCAL.SomeNumber;
</cfscript>
</cffunction>
I get this error:
Entity has incorrect type for being called as a function. The symbol you provided getRandomNumber is not the name of a function.
I figured maybe I can't reference a function within the same CFC without creating an object first, so I do this:
<cffunction name="orderItemList">
<cfscript>
LOCAL.RandomNumber = APPLICATION.AppInfo.objOrders.getRandomNumber();
return LOCAL.RandomNumber;
</cfscript>
</cffunction>
Then, I'd get this error:
Either there are no methods with the specified method name and argument types, or the method getRandomNumber is overloaded with arguments types that ColdFusion can't decipher reliably. If this is a Java object and you verified that the method exists, you may need to use the javacast function to reduce ambiguity.
How should I call a second function within the same CFC?
The first thing I would try is var scoping your all your variables within your functions:
<cffunction name="orderItemList">
<cfscript>
var RandomNumber = getRandomNumber();
return RandomNumber;
</cfscript>
</cffunction>
<cffunction name="getRandomNumber">
<cfscript>
var SomeNumber= randRange(0,10);
return SomeNumber;
</cfscript>
</cffunction>
If that doesn't solve the problem, let me know and we can explore further.
edit
Okay, now that the local scope issue is resolved, try this:
<cffunction name="orderItemList">
<cfscript>
LOCAL.RandomNumber = THIS.getRandomNumber();
return LOCAL.RandomNumber;
</cfscript>
</cffunction>
<cffunction name="getRandomNumber">
<cfscript>
LOCAL.SomeNumber= randRange(0,10);
return LOCAL.SomeNumber;
</cfscript>
</cffunction>

Variable name in the method of ColdFusion Object

I am trying to set a variable in a cffunction.
The result is this:
<cfset local.layouts.appLayout = '../../app/layouts' & local.appController.new()>
The above code works. In the local.layouts.appLayout structure it assigns the return of the new method in the appControler. That is what I need it to do.
My problem is that I need to dynamically assign the method portion of that statement. I have another variable coreRoute.action that equals "new" in that function but I cannot seem to get the syntax right.
I have tried this:
<cfset local.layouts.appLayout = '../../app/layouts' & local.appController.coreRoute.action()>
That does not work and I can see why. I have also tried this:
<cfset local.layouts.appLayout = '../../app/layouts' & local.appController & #coreRoute.action# & '()'>
I have tried many variations of this syntax and I just cannot get it right.
Anyone have any ideas about how to do this. I am stuck.
Thanks in advance for any help.
UPDATE: With Todd Sharp's help I ended up using this and it worked great:
<cfinvoke component="#local.appController#" method="#coreRoute.action#" returnvariable="local.act">
<cfset local.layouts.appLayout = '../../app/layouts' & local.act>
You should look into using <cfinvoke> for dynamic method invocation. Try a Google search for "coldfusion dynamic method invocation" - here's one of the top results:
http://www.bennadel.com/blog/1320-ColdFusion-CFInvoke-Eliminates-The-Need-For-Evaluate-When-Dynamically-Executing-User-Defined-Functions.htm
In addition, if you want to do it entirely in script, you can, using this approach:
dynFn = this["foo" & bar];
dynFn(stuff);
This is in a cfc, if you're doing it from outside the cfc or not using a cfc at all, just change "this" to wherever your method is.

How do I run a static method on a CFC without using cfinvoke?

How do I invoke a static method on a CFC without using cfinvoke? I know that I can do this:
<cfinvoke component="MyComponent" method="myStaticMethod' arg1="blah" returnvariable=myReturnVar>
I would like to be able to invoke this method the same way I would a UDF:
<cfset myReturnVar = MyComponent.myStaticMethod(blah)>
This, however, does not work. Is there syntax that I am messing up or is this just not possible?
not possible, since there's no "static method" in ColdFusion.
The <cfinvoke> line in your question is the same as:
myReturnVar = CreateObject("component", "MyComponent").myStaticMethod(arg1="blah");
You need to create the object first.
<cfset MyComponent = createObject("component","MyComponent") />
<cfset myReturnVar = MyComponent.myMethod(blah) />
I know this is a really old post but here's an updated answer for more modern CF/Lucee supporting static constructs for anyone who might stumble across this like I did :-)
based off of:
https://modern-cfml.ortusbooks.com/cfml-language/components/static-
component MyComponent{
static {
appendToArgValue : "text"
}
public static function myStaticMethod( arg1 ){
return arg1 & static.appendToArgValue;
};
}
MyComponent::myStaticMethod("blah")
Lucee documentation: https://docs.lucee.org/guides/lucee-5/component-static.html