I ran into something a bit strange the other day when I was playing around with a dynamic Application.cfc proxy. I tried, in the pseudo constructor of the Application.cfc (the space before the CFFunction tags), to delete the OnRequest() event method. While this appears to work if you dump out the Application.cfc object, it does not prevent the events from being triggered. Take a look at this example:
<cfcomponent output="true" hint="I define the application and listen for application level events."> <!--- In the pseudo constructor, we are going to try and delete the following methods. ---> <cfset StructDelete( THIS, "OnRequestStart" ) /> <cfset StructDelete( THIS, "OnRequest" ) /> <!--- ASSERT: At this point, we should have deleted the OnRequest() and OnRequestStart() event listeners. We should only have the OnRequestEnd() event listener left. ---> <!--- Dump out this reference to see if the methods were successfully deleted from the Application.cfc. ---> <cfdump var="#THIS#" label="Application.cfc THIS Scope" /> <cffunction name="OnRequestStart" access="public" returntype="boolean" output="true" hint="I run before a template is executed."> <p> I am the OnRequestStart() event listner. </p> <!--- Return out. ---> <cfreturn true /> </cffunction> <cffunction name="OnRequest" access="public" returntype="void" output="true" hint="I execute the page template."> <p> I am the OnRequest() event listner. </p> <!--- Include passed in page. ---> <cfinclude template="#ARGUMENTS[ 1 ]#" /> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="OnRequestEnd" access="public" returntype="void" output="true" hint="I run after a template has been executed."> <p> I am the OnRequestEnd() event listner. </p> <!--- Return out. ---> <cfreturn /> </cffunction> </cfcomponent>
Notice that at the top of the Application.cfc we are deleting the OnRequestStart() and the OnRequest() event listener methods. This should leave just the OnRequestEnd() method. This should also mean that the OnRequestStart() and OnRequest() methods won't fire (seeing as they have been deleted). However, when we run this page, here is the output that we get:
Notice that in the CFDump of our Application.cfc instance, the OnRequestStart() and OnRequest() have, in fact, been deleted. Realize that this is also the very first thing that displays meaning that this execution has been completed first. Notice also, and here is the oddity, that the page output also contains the text from the OnRequestStart() and OnRequest() event methods, indicating that those two methods have been executed.
It seems that you can't alter the method configuration of the Application.cfc from the pseudo constructor. I am not saying that you should do this, I am saying you can't do this. What's really odd, however, is that you can, from the OnRequestStart() method, successfully delete the OnRequest() event method. This is how we can dynamically cope with web service calls. However, the OnRequestStart() event method executes after the pseudo constructor, so it really makes no sense as to why the delete method gesture would be more successful later on in the execution path.
I wonder whether the CF engine itself assumes the core methods when it finds an instance of Application.cfc. Probably just re-creates any of them that are lacking at runtime? In other words, doesn't the OnRequestStart() action happen even if the Application.cfc hasn't defined an override for that method?
Just thinking out loud ...
The events definitely occur whether or not the Application.cfc has listeners. However, based on the output of the individual functions, it is clear that not only are the events being triggered, but my original functions are there to listen for it.
Aha, I had missed that detail. How very very interesting indeed ...
I didn't test this, but I wonder if this is true for every type of CFC, or if this is something specific to Application.cfc?
You have to delete the event handlers from both the this and variables scopes.
<cfset StructDelete( THIS, "OnRequestEnd" ) />
<cfset StructDelete( THIS, "OnRequest" ) />
<cfset StructDelete( VARIABLES, "OnRequestEnd" ) />
<cfset StructDelete( VARIABLES, "OnRequest" ) />
Your results seem to be the same as if you attempted the following in onRequestStart.
<cfset StructDelete( THIS, "OnRequestEnd" ) />
<cfset StructDelete( THIS, "OnRequest" ) />
Darn, Jason beat me to it! Yes, for some odd reason you must delete the methods from *both* this scope and variables scope...
I discovered this when trying to solve the onRequest() intercepting remote CFC invocations problem. I never blogged it but Ray Camden did somewhere.
When I have dealt with CFC calls, I never deleted from the VARIABLES scope, just the THIS scope. Pluse, the VARIABLES scope is private, the system shouldn't have access to it.
Let me test....
You are right, deleting from VARIABLES scope did complete the job. This has got to be a bug. The VARIABLES scope is a private scope and its contents should not affect third-party calls.
Ben, I ran into a very similar situation a while back while trying to do mixins. My problem wasn't in deleting but in overriding, but the result was the same. Here's my take on it, since all the cool kids are adding links to their own blog posts these days: http://mxunit.org/blog/2008/04/adventures-in-mocking-part-1.html
the first 90% is all boring testing crap, but the last part is where i talked about my run-in with jonny law (i.e. this vs. variables).
God bless CF for letting us do this stuff!
Very interesting. I did not know this:
....when functions call each other internally, they're calling the version of the function that lives in the variables scope. And when you, dear programmer, call a function on an object directly, you're calling it in this scope.
Good to know. Thanks!
@Marc, et al,
Here's what's really strange! If you change the Access of the event listeners to private, then they do not fire. They are not available in the THIS scope any longer, only in the VARIABLES scope. This must mean that the ColdFusion server is, and this makes sense, calling the public version of the event listeners on the Application.cfc.
Therefore, it makes no sense that the VARIABLES scope should have any influence on the methods at all in our case.
I think this must definitely be a bug in the public / private implementation of methods in CFCs.
it would indeed be interesting to hear from a cf engineer about how this stuff all works under the hood.
I figured out what's happening...
Your Application.cfc is really wrapped inside a coldfusion.runtime.AppEventInvoker which extends coldfusion.cfc.CFCProxy.
At first I thought it was the isMethodPresent() method on the AppEventInvoker, which only returns false if we delete from both variables and this scopes.
(you can test this by cfdumping: application.getEventInvoker().isMethodPresent('onRequest') in the onRequestStart() handler.)
Then I thought to look at the getMethod() method of the CFCProxy, and sure enough, it has the same problem.
Then I tried invoke() in the CFCProxy, and found that I could invoke methods only defined on the variables scope there too!
Then I moved on to the coldfusion.runtime.TemplateProxy's methods:
getMethods(): This returns the methods defined in the variables scope, ignores the this scope. Totally ignores public/private.
resolveMethod(): This returns the method until we delete it from both variables and this scopes. Respects public/private.
So last, I tried adding a method to the this and variables scopes manually.
<cfset this.method = test> or <cfset variables.method = test>
And you can do the below reguardless of if you assigned to the this or variables scopes:
o = createObject("component","Test.cfc");
And it's not just invocation, it's accessing the variable itself. <cfset testMethod = o.test> works either way as well, BUT this only works when the variable "test" is a method. If we did <cfset variables.test = "foo">, then o.test would throw an undefined variable exception!
So a method assigned to the variables scope is *not* private.
Looks like the answer lies in the weird distinction between the two scopes, the way it handles methods, property look up, and how that all boils down into the CFCProxy, and by extension the Application.cfc
And wow is this part of the CF engine is super complicated, sigh.
(Apologies for the exceptionally long response)
Wow! I am more shocked that you were able to figure all of that than anything else. The findings are a bit strange too. I guess, I will just leave the "magic" up to the ColdFusion server :)
But seriously, how did you figure all that out?
There's a lot of techniques for figuring out how the CF server works. From forcibly throwing errors and looking at stack traces, getMetaData(), getClass(), cfdump, google, and a lot of time.
One trick I know a lot of people don't realize is that getMetaData() on a non-CF type returns it's java.lang.Class.
So we can do getMetaData(variables).getName() and that gives us coldfusion.runtime.VariableScope, and now we have the class of that scope, and can manually create it with createObject() to play with.
If you're interested in more, I'm doing a presentation at CFUnited about more of the details of the CF engine, and how the whole thing works. :)
Awesome. I'll definitely be there.
As a quick correction to the above incase someone reads it and think there was a mistake:
<cfset this.test = method> or <cfset variables.test = method>
I had reversed "method" and "test" in the assignments.
The issues are the same though, it was just a typo in the blog response.
@Elliott, I'm curious as to whether you've bounced that session idea off the CF team folks - you're sailing very close to the "decompile" prohibitions in the CF license I suspect... (but it sounds like a fascinating session so I'll almost certainly attend - see you next week!).
I've given a very similar presentation at a user group before with no objections, and I do plan to run it over with an engineer before hand. I know at least 2 of the engineers are attending the session as well.
To be honest, I really have no interest in discussing how to decompile compiled CF code, or the CF engine, nor will I be going anywhere near that in the session. My goal is not to infringe on Adobe's IP, but rather empower CF developers with some knowledge about what really happens to make issues (like the one in this blog post), or www.bennadel.com/blog/1210-Dynamically-Evaluating-Image-Functions-In-ColdFusion-8.htm less mysterious and empower developers with knowledge to solve issues like the one I fielded some time ago about resetting the cfhtmlhead buffer.
It's also come in handy for other things. For instance I created CF bindings for JRuby allowing ruby code embedded in CF to call CFC methods or CF functions (while making the code look like natural ruby), and access all the scopes. I'd not really planned to talk about that specifically in the session (this is a CF conference after all, not a ruby one), but I'd be happy to share that if you're interested too.
In any case all the things I'll be discussing are easily figured out through google, stack traces, reflection, and a lot of tinkering.
See you there!
@Elliott, thanks for the clarification. Sounds like a fascinating session. I've added it to my CFUNITED schedule!
Me too :)
As an addendum to my above conclusion, turns out I was wrong.
I've found the behavior is more complicated than I thought.
At first I thought all methods added to the variables scope were not private, but this is not the case. The result I found above was because of where I had the code that assigned variables.method = func>.
Turns out if you do that in the *body* of a <cfcomponent> then the method is public, even though you only added it to the private variables scope.
However, if you do that assignment in a *method* instead, then the method will be private!
Apologies for anyone I confused. You *can* still make methods private by assigning them only to the variables scope, but only if it happens inside a method.
It also seems to relate to access of the function...
<cfset variables.func1 = publicFunc>
<cfset variables.func2 = privateFunc>
func1 is public, even though you only added it to the variables scope.
func2 is private, because the function you assigned to it was private.
There's a serious can of worms here.
Thanks for the update. This is good stuff to know.
@Elliott, I suspect the access checks are based on the metadata of the function and have nothing to do with the (dynamic) scope in which the reference is stored...?
I wait that it helps, excuse for not being in English, but who knows brief (http://www.whycf.com), but my two cents are in http://www.porquecf.com.br/blog/index.cfm/2008/8/31/Exclua-o-mtodo-durante-outro-mtodo--Applicationcfc
I had already published this in 2006, these properties are really interesting.
Excuse the disorder, but I am still arranging the servant migration.
Was it a bird? a plane? No ... it was Spren, Corfield and Nadel going Way the hell over my head ... :)
Over my head a bit too ;)
But how do you resolve bypassing the onRequestStart errors? I tried the StructDelete method, but my apps are still failing.
Interesting discussion. I have been playing around with similar issues whan I discovered I could assign another component to a components THIS scope (this = createObject()) and found that I could actually run the methods of component B inside the component A after the assignement, while at the same time the origial methods were, well, lost. And then the instanciated component A reverted back to being itself again afterwards...
Anyway, for the remote calls it seem to be important to set access to OnRequest as 'private'. Public or, as the Adobe docs suggest, leave out that property, does not work for me.
That sounds very odd. I can't remember if I've ever tried to override the THIS scope.
Oops, setting the onRequest to 'private' is basically the same as removing it.
Any idea why that method breaks remote calls to cfc's?
I guess it breaks because you're not really including a CFC to execute - you're executing a method on a component; so, the include concept breaks.
<cfset this = createObject("component","b")>
<cfreturn 'I am A'>
<cfset this = createObject("component","b")>
<cfreturn 'I am B'>
<cfset a = createObject("component","a")>
@Stefan, the docs for Application.cfc are very clear as to why onRequest() cannot be used with remote calls (a lot of people haven't read that section of the docs which is why I mention it).
onRequest() is a way for you to completely handle the request, whether it is a page request or a remote call, but for remote calls there is no way you can correctly pass back a result that the caller can use (because onRequest() is only passed the "targetPage" - the path to the CFC - and is not passed the method name or arguments, or the format of the result that needs to be returned).
Nice articulation of why OnRequest() and CFCs don't jive. Thanks.
Still, CF should know it can not handle CFC's and simply ignore them, let them pass without being intercepted and destroyed.
very interesting. one thing i noticed is that we are calling the variables 'private' but i think it would be called 'protected' as pseudo-contructed variables.values are shared across the entire component/cfc, not a specific method. at least this is true when they are created inside the cfc but outside any method. or maybe they are private if created within a specific method?
@William, you are correct. For some reason Adobe (well, Allaire or Macromedia) decided to use 'private' for what is more like 'protected' variables in other languages... but then several other languages use 'this' to refer to both public and private variables so... :)