5

Sorry about the question phrase. I couldn't find better way to describe it. But my problem is as follows:

I have 3 cfc's namely settings.cfc, prices.cfc and helpers.cfc. These cfc's extend 4th cfc controller.cfc. The helper.cfc is as follows:

<cfcomponent extends="Controller">
    <cffunction name="formatCurrency">
        <cfset formattedCurrency = 1 />    
        <cfreturn formattedCurrency>        
    </cffunction>
    <cffunction name="processTemplateVariables">
       <cfargument name="templateText" default="defaultText" >
       <cfset formatCurrency() />
       <cfreturn formattedCurrency >        
    </cffunction>
</cfcomponent>

The settings.cfc has a setApplicationVariables method which we use to set the application level variables. In this cfc, i have created an object of helpers.cfc and put that object into the application scope. The settings.cfc is as follows:

<cfcomponent extends="Controller">
   <cffunction name="setApplicationVariables">    
      <cfset application.helpers = createObject("component","controllers.Helpers") />  
   </cffunction>
</cfcomponent>

The settings.cfc gets invoked on application start which in turn creates a object of helpers.cfc and put it into the application scope.

We create a reference to the method ProcessTemplateVariables in the controller.cfc as follows:

<cfcomponent extends="Wheels">
   <cfset getFormattedCurrency = application.helpers.processTemplateVariables >
</cfcomponent>

In the prices.cfc, we use this reference to call the function processTemplateVariables, which it does. But it does not call the function formatCurrency that is called inside from the processTemplateVariables and it throws error "variable formatCurrency is undefined".

But if i use the application.helpers.processTemplateVariables(templateText="someText"), it works.
It also works, when i use cfinvoke as bellow:

<cfinvoke method="processTemplateVariables" component="controllers.helpers" templateText="someText" returnvariable="content">

The prices.cfc is as follows:

<cfcomponent extends="Controller">
    <cffunction name="index">
        <!--- does not work, throws 'the formatCurrency() variable is undefined' --->
        <cfdump var="#getFormattedCurrency("someText")#"><cfabort>
        <!--- works --->    
        <cfinvoke method="processTemplateVariables" component="controllers.helpers" templateText="someText" returnvariable="content">
        <!--- works --->
        <cfset application.helpers.processTemplateVariables("someText") />   
    </cffunction>
</cfcomponent>

I am not sure why using reference is not working. Sorry about the earlier confusion but your comments made me dig deeper and i could found out that it was reference that was culprit. Is there any way to make this work with reference, that would be cool?

Tushar Bhaware
  • 2,525
  • 1
  • 16
  • 29
  • 1
    In your `cfobject` call you're passing in an argument. Does the error still occur in your `createObject` if you pass in that argument? – Matt Busche Dec 12 '14 at 13:48
  • 1
    (Edit) In addition to Matt's comment: Shot in the dark, but any chance you added/changed the `formatCurrency()` method in the component *after* storing the instance in the application scope? That could cause the "undefined" error you are seeing, if it were invoking a function that did not exist at the time you created the instance. If that is the case, using cfinvoke (by "name") *would* work because it creates a new instance each time, so it would be using the latest code. – Leigh Dec 12 '14 at 13:56
  • 1
    Matt raises a good point. I understand the above is meant to be an abbreviated example. However, there are some discrepancies ie cfinvoke is supplying an argument that is not defined in the sample function signature. Can you post the *actual* code and function signatures you are using when the error occurs? – Leigh Dec 12 '14 at 14:08
  • @MattBusche, In the process of making question easier to understand, I forgot to put the argument while using object from the application scope and thanks for noticing that.yes, the error still occurs if i pass the argument to the object from the application scope. I have updated the question accordingly. – Tushar Bhaware Dec 12 '14 at 14:26
  • @Leigh, The formatCurrency() method was already in the component. – Tushar Bhaware Dec 12 '14 at 14:34
  • @Leigh, Yes, This is an abbreviated example. That problem occurred at office, now i am at home so i don't have access to the actual code. I will try to write the example code in such way that it can reproduce problem . – Tushar Bhaware Dec 12 '14 at 14:41
  • Okay, but are you positive you did not make changes and that the component invoked the function correctly - *at the time you created the instance*? The reason I am harping on it is that what you describe is exactly what would happen if you made changes to the component but forgot to recreate the instance in the application scope - and "variable ...undefined" is exactly the error you would get if you invoked a function that could not be found for some reason ie misspelling, etcetera. – Leigh Dec 12 '14 at 14:51
  • @Leigh, Yes, I am positive about that. I didn't make any changes. There are no spelling errors and function invoked correctly. I even restarted the application few times, on application start, we recreate the instance of the helpers.cfc. – Tushar Bhaware Dec 12 '14 at 14:59
  • @TusharBhaware - Okay, sounds like you have checked the obvious things. I will check later for your new repro case. – Leigh Dec 12 '14 at 15:08
  • When this happens to me it's always because I forgot to reinit the object into the application scope. Try restarting CF :) – Mark A Kruger Dec 12 '14 at 15:14
  • @MarkAKruger, I did reinit the object in the application scope but as i dig deeper while making repro case, i found that i was barking at wrong tree. I have updated the question accordingly. can you please take a look? – Tushar Bhaware Dec 12 '14 at 16:17
  • @Leigh, I have updated the question. While making repro case, i found out that i was looking at wrong place. It was reference of object is causing problem. I have updated the question accordingly. Can you please take a look? – Tushar Bhaware Dec 12 '14 at 16:19
  • @TusharBhaware - Yeah, I do not think you can do that. IIRC, each function is compiled into an individual class. In other words, it is disconnected from the other functions. The parent component stores all of the functions in the variables scope. So the functions only have access to each other when used *within an instance* of the component. When your code creates a reference to the function - what you are getting is completely *disconnected* from the parent instance ie `application.helpers`. So it does not have access to any of the other functions. Hence why you get an "undefined" error. – Leigh Dec 12 '14 at 17:39
  • @Leigh, Oh, this explains why i am getting error. Wow! i did not know that each function is compiled into an individual class. Can you write your comment as answer so i can mark it as accepted and also it will help to make this discovery as permanent as answer. Also can you provide me some references on this so i can read up on this topic. I tried to google it but i couldn't find any thing relevant. Another thing when you wrote IIRC in your comment, did you mean 'IF i read correctly' because i couldn't find anything relevant to CF when i googled it. – Tushar Bhaware Dec 12 '14 at 18:30
  • @TusharBhaware - Yep, I meant "If I recall correctly" :) *each function is compiled into an individual class* More specifically, into inner classes. You can see the inner class names if you dump out the function name ie `#application.helpers.formatCurrency#`. I remember reading a more articulate explanation about this issue .. somewhere ;-) Let me see if I can dig it up. Then I will post an answer. – Leigh Dec 12 '14 at 19:42

2 Answers2

2

Update:

This blog entry (by Adam Cameron) has a better description. To summarize:

.. it pulls the method out of the CFC, so it will be running in the context of the calling code, not the CFC instance. Depending on the code in the method, this might or might not matter.

In your specific case it does matter. The function has a dependency on formatCurrency, which does not exist in the "context" of the calling page, and that is why you get an "undefined" error.


(From comments)

Yeah, I am pretty sure you cannot do that. Each function is compiled into an individual class: specifically a static inner class. (You can see the inner class names if you dump out the function name without parenthesis ie #application.helpers.formatCurrency#) In other words, it is disconnected from any specific instance - and by extension - the other functions.

When you create an instance of the component, all of the functions are stored its variables scope. So when you invoke "processTemplateVariables" - from within the instance - it has access to the other functions via the component's variables scope. When your code creates a reference to that function, what you are actually getting is completely disconnected from the parent instance ie application.helpers. So it won't have access to any of the other functions. Hence why you get an "undefined" error.

Leigh
  • 28,765
  • 10
  • 55
  • 103
1

The limitation is understood well from the answer by Leigh.

To focus on your requirement here: using a short name alias for the functions, You can still use your original code, with a little trick of adding all the dependent functions as a reference, so that they are available inside the controller.cfc scope similar to the getFormattedCurrency.

Edit 1:

<cfcomponent extends="Wheels">
   <cfset getFormattedCurrency = application.helpers.processTemplateVariables />
   <cfset formatCurrency = application.helpers.formatCurrency />
</cfcomponent>

Now, for the fun part, it's quite possible to access a function from another cfc and still preserve all the dependencies. I was surprised as well when I recalled an amazing post by Bennadel here. This is simply amazing. But to warn you this practice is discouraged. So I took up with your setup and went ahead anyway. And it all worked like a charm with no issues encountered so far(but I'm quite sure there could arise some complications). The point of problem with original usage was that the function has a dependency on formatCurrency, which does not exist in the "context" of the calling page as educated by Leigh in his answer. So what if you can just copy scoped objects or even the functions from another component to you controller.cfc, sounds weird and amazing at the same time but it's possible using the <cfinclude> tag inside the controller.cfc (note: not encouraged) based upon the idea what Bennadel used for his example.

Edit2:

Your controller.cfc should look something like this:

<cfcomponent extends="Wheels">
   <!--- Placed inside the controller's scope itself, outside every other function --->
   <cfinclude template="helpers.cfc" />
   ....<rest of your code>....
   ...........
</cfcomponent>

Note that you don't even need to create a short-hand alias for the functions now. All the components and views can directly use the function name as you intended. No need to point out that there would be chances of Naming collision if any other component extending the Controller.cfc has a function with the same name as any of the function inside the component library that is being imported. But those can be solved by following more specialization for the functions into multiple components OR as simple as using a prefix as a part of the coding standard to the function names to avoid any such future scenarios.

Anurag
  • 1,018
  • 1
  • 14
  • 36
  • Interesting approach. but we had decided to go with a different approach. Sorry for a late reply. I have been away from SO for long time. – Tushar Bhaware Mar 23 '15 at 16:41