Comprehensive ColdFusion Component OnMissingMethod() Testing
I love the OnMissingMethod() functionality that was introduced in ColdFusion 8. However, I fear that I only ever understood it from an object-external standpoint. From my original testing, I believe that I came to the conclusion that OnMissingMethod() only works from outside of the given object and not from within it. However, after some discussion with Peter Bell, I was made to think that it did, in fact, work from within an Object, but only when used in certain ways.
To be 100% certain, I decided to run some quick tests that exercised the ColdFusion 8 OnMissingMethod() functionality in every way different way that I could think of. I created a simple ColdFusion component that had an OnMissingMethod() and Test() method, that latter of which would try to leverage the former:
<cfcomponent
output="false"
hint="I test onMissingMethod functionality.">
<cffunction
name="OnMissingMethod"
access="public"
returntype="any"
output="true"
hint="I handle messages that do no have defined handlers.">
<!--- Define arguments. --->
<cfargument
name="MissingMethodName"
type="string"
required="true"
hint="I am the name of the missing method."
/>
<cfargument
name="MissingMethodArguments"
type="struct"
required="true"
hint="Ihe arguments that were passed to the missing method."
/>
<!--- Output missing method notification. --->
<p>
Missing method handler:
<strong>#ARGUMENTS.MissingMethodName#</strong>
</p>
</cffunction>
<cffunction
name="Test"
access="public"
returntype="any"
output="true"
hint="I test on missing method functionality.">
<!--- Try unscoped internal test. --->
<cftry>
<cfset Unscoped_DoesNotExist() />
<cfcatch>
<p>
<em>Unscoped test failed.</em>
</p>
</cfcatch>
</cftry>
<!--- Try THIS-scoped internal test. --->
<cftry>
<cfset THIS.This_DoesNotExist() />
<cfcatch>
<p>
<em>THIS-scoped test failed.</em>
</p>
</cfcatch>
</cftry>
<!--- Try VARIABLES-scoped internal test. --->
<cftry>
<cfset VARIABLES.Variables_DoesNotExist() />
<cfcatch>
<p>
<em>VARIABLES-scoped test failed.</em>
</p>
</cfcatch>
</cftry>
</cffunction>
</cfcomponent>
As you can see, the Test() method of my ColdFusion component attempts to leverage the OnMissingMethod() using unscoped, THIS-scoped, and VARIABLES-scoped method calls.
Then, I created a simple test page that performed an external-OnMissingMethod() control and then executed the Test() method on the ColdFusion component:
<!--- Create our test object. --->
<cfset objTest = CreateObject( "component", "Object" ) />
<!---
Try external test (we know this will work, and am using
it as a control case).
--->
<cftry>
<cfset objTest.External_DoesNotExist() />
<cfcatch>
<p>
<em>External test failed.</em>
</p>
</cfcatch>
</cftry>
<!--- Execute internal tests. --->
<cfset objTest.Test() />
When I run the above code, I get the following output:
Missing method handler: External_DoesNotExist
Unscoped test failed.
Missing method handler: This_DoesNotExist
VARIABLES-scoped test failed.
The external test worked as expected. But, what's good to know is that the THIS-scoped method also worked internally in conjunction with the OnMissingMethod() functionality. And people scoffed at me for scoping my internal method calls - shame on you! ;). So, if we can think of the THIS scope as the public-reference to a ColdFusion component, then I suppose this makes perfect sense; referencing the THIS scope is tantamount to referencing the object from an external point of view, which is the way we've always known OnMissingMethod() to work.
Sweet! No more mystery.
Want to use code from this post? Check out the license.
Reader Comments
Great post, Ben! Thanks for pulling this thread...
And the "Arnold" add is particularly timely, given Maria's presence at MAX earlier this week...
@Ron,
Thanks man. I thought it was time to sort out the confusion in my head. That's cool that Maria is there - I was following some blog posts on it.
Funny, I was just today lamenting the fact that I could not use onMM for internal calls, and then, a couple of hours later I see your post.
I'm going to have to go back into my component and see if i can get it working.
Thanks for the tip!
@Bob,
No problem. Glad this helps more than just me :)
I never really understood why onMM behaves differently from inside or outside. Are there solid coding principles or philosophies behind the decision? Was it maybe an oversight or perhaps difficult to implement because it would affect other things? I would love to know.
@Ben, Yeah, the only way oMM() works in Adobe ColdFusion internally is if you use the THIS scope. I don't really love that from a clarity/clutter perspective, I prefer doXXX() within an object to THIS.doXXX(). Interestingly, this works "properly" in Railo. Not tried it in OpenBD.
Maybe they'll change the implementation for Centaur . . .
The reason for this is actually very simple. I'm pretty sure I've posted about this a number of times...
CFCs are just proxied pages. CreateObject() returns a TemplateProxy that wraps the CFPage that is your actual code.
When you call a method inside a CFC without qualifying it with this, such as "getFoo()" it's no different than calling a regular function in a .cfm file.
However, when you call the function as "this.getFoo()" or from the outside as "myObject.getFoo()", instead what happens is that it calls a method on the TemplateProxy for invoking a method, which in turn calls the function on the proxied page.
OnMissingMethod handling exists in the invoke() function on the TemplateProxy, thus it only works from outside or through the this scope.
The this scope is just the TemplateProxy.
This feature request of allowing it randomly would potentially work, but it'd involve some magic.
// pseudo code
function CreateObject( type, name ) {
if( type eq "component" ) {
obj = new CFPage(name);
obj.setIsComponent(true);
return new TemplateProxy(obj);
}
}
Then the invoke() method of the CFPage could have a check for isComponent() that calls OnMissingMethod if the Page is actually a component and the function doesn't exist.
Just remember that "objects" in CF are really not the same concept as Objects in most other languages. The idea that objects are actual pages behind a proxy is kind of unheard of. Perl is the only other language I can think of that uses a similar concept.
@Peter,
I don't mind this so much. Personally, I like scoping my method calls. It feels more explicit to me; and, in a funny way, makes me feel more powerful (like some drill Sargent - I tell my methods where they need to exist, where they're expected to exist, and they better not go and change on me.... YES DRILL SARGENT!)
@Elliott,
As always, your explanations are quite enlightening.
@Elliott: Thanks for that explanation. I kinda sorta knew it but you made it really clear. So the technical challenge is there, but just like in your pseudo code it seems like there are ways to get around it. Hopefully they do. Maybe they could make onMM a page context feature rather than a component feature. That way you could even use it on a regular cfm pages and solve the original issue.
Yeah, the ability to use onMissingMethod() from within the component was the missing ingredient in the function libraries I've always had in the onTap framework.
I always had this vision of utility libraries that load incrementally on-demand instead of having to load them aggressively the way you might with something like the Model-Glue Helpers or a utility.cfc. Even using an IoC framework to instantiate a singleton for a utility CFC always seemed to me heavy-handed and not very flexible.
CF8 finally gave me a way to make my "lazy-loading library" a reality by using onMissingMethod and allowing functions that depend on each other to make their calls via the "this" scope.
Boy does that make a difference because in practice, prior to CF8, I always just loaded everything aggressively, which I hated because that meant over 100 files that loaded when the framework loads and most of them wouldn't be used on the first request.
It turns out that just loading the libraries was the #1 cause of the up-front performance hit for running the onTap framework, so onMissingMethod() finally eliminated it and made it load with speed comparable to Fusebox.
I haven't bench-marked it, but I've been really happy with it. :)
I've contemplated the possibility of doing something similar with DataFaucet also for performance reasons... where for example the active record object has an install method, but it doesn't get used very often, so if I can have it lazy-load that method then that would reduce the object's memory footprint, and I may do it with a few other public methods as well. I just haven't gotten around to trying or implementing that with any of the DataFaucet objects. ;)
@Ike,
That's a really interesting concept; I have never even considered lazy-loading methods. Hmmm.
Thanks Ben. :)
Of course one of the things that some folks will not like about the lazy-loading method approach is that if you dump out the CFC or if you use getMetaData() for IoC or somesuch, none of the lazy-loaded methods will show up.
I would expect that in most cases that would be okay, although it might also mean that you'll need to be a little more careful to document the component so people understand where to find the lazy-loaded functions, the necessary naming convention ('cause that's the only way you'll get them to lazy-load), etc.
In the case of the framework's libraries all that documentation had already been done of course because the idea of incremental, on-demand loading of utility methods had always been the long term goal and thorough documentation of them had always been part of the plan. It was part of the impetus behind defining the SPEC xml dialect, which really should be its own project, but hasn't really been aggressively developed or maintained because it's just not been as high on my list of priorities as other aspects of software development.
So in practice although I did have to make a few changes to the functions when I made the change, it mostly eliminated the need for me to specify the functional dependencies anymore. So I think where the documentation used to list "see also" and "requires" next to each function, now I think it just lists "see also". That's because it's not really so important anymore what methods rely on each other, although you still can optimize the library by pre-loading dependencies if you want to. Fewer calls to onMissingMethod() will certainly mean fewer processing cycles and so if you have a method X that depends on library methods A,B and C, it makes sense for performance to add a quick call to load them when your X method loads.
Speaking of which... here's another potential issue and something you may want to blog about... what happens with onMissingMethod() and the SUPER scope? This is somewhat pertinent to the DataFaucet components because although I might like to lazy-load the install() method, I know that there are already implementations where install() is overridden with a custom install method that says basically install some other stuff first and then call super.install(). If super is called through the proxy, that would continue to work fine, but if it's called in the page object that may be a barrier. My gut says the latter will be true, which is liable to be a problem for me. Although I'm sure I can find a workaround... maybe I'll just have the public function check for the existence of a private function and use that instead of onMissingMethod() in that case.
As an update to this, you should look at how it behaves using cfinvoke. I've got a situation where I have a variable passed into my function and I want to call another function internal to that object that has the same name as the variable. Since I'm trying to call a dynamically named method, I am using cfinvoke. But now I'm trying to catch situations where the function it wants to call doesn't exist and using onMissingMethod. The problem is that cfinvoke doesn't seem to like any sort of scoping.
<cfset myvariable = 'test'>
<cfinvoke method="this.#myvariable#" returnvariable="thereturnedthing" />
throws an error saying that this.test isn't the name of a function instead of hitting the onMissingMethod function.
As a short follow up to my own comment, I also note that you can't use cfinvoke method="this.functionname" even if functionname is a function if that function is set to private. If the function is public, this.functionname works fine using cfinvoke.
I can't decide if I think that is a bug or not. I would think that cfinvoke this.function should be exactly the same as calling this.function() And unless I'm wrong, this.function() should work within the same component even if function() is set to private.
@Judah,
Interesting. There is another wrinkle to think about: what happens if you define the "component" attribute of CFInvoke:
<cfinvoke component="#THIS#" method="#myvariable#" />
I have no tried this, but this is fundamentally different than calling:
method="this.#myvariable#"
... I think??
@Ben - I just laid out a test case for your pondering and the answer is that it seems to be the same as calling the component from outside the object.
Here's my test component:
<cfcomponent name="test" output="false">
<cffunction name="init" access="public" output="false" returntype="test">
<cfreturn this/>
</cffunction>
<cffunction name="theproxymethod" access="public" output="false" returntype="struct">
<cfargument name="methodname" type="String" required="true">
<cfinvoke component="#this#" method="#methodname#" returnvariable="returnvar">
</cfinvoke>
<cfreturn returnvar>
</cffunction>
<cffunction name="foo" access="public" output="false" returntype="struct">
<cfset returnvar = StructNew()>
<cfset returnvar.thisitem = "foo">
<cfreturn returnvar>
</cffunction>
<cffunction name="bar" access="private" output="false" returntype="struct">
<cfset returnvar = StructNew()>
<cfset returnvar.thisitem = "bar">
<cfreturn returnvar>
</cffunction>
<cffunction name="onMissingMethod" access="public" output="false" returntype="struct">
<cfargument name="MissingMethodName" type="string" required="true">
<cfset var errors = StructNew()>
<cfset errors.name = arguments.MissingMethodName>
<cfreturn errors>
</cffunction>
</cfcomponent>
Now, testobj = CreateObject("component","test")
And try using the proxy method to call the private function, the public function and a missing function.
response1 = testobj.theProxyMethod("foo");
response2 = testobj.theProxyMethod("bar");
response3 = testobj.theProxyMethod("shouldntmatch");
I get that response1 (real method but public) is a struct where thisitem = foo
response2 (real method but set to private) is a struct where NAME=bar and response3 (missing method) is a struct where NAME=shouldntmatch
So it looks like in this case the onMissingMethod is being called but it is being called for private functions as well as for truly missing methods.
@Judah,
Very interesting! So, the THIS scope gives access to the private method. Hmmm, I am pretty sure that doesn't work if you call it directly via THIS.Bar()... at least I don't think so. I'll have to double check that.
@Ben No, I don't think that the THIS scope gives access to the private function. I think it should though. Maybe.
When I use my proxy function to dynamically call another method within the same component, what is the origin of the request? I guess it is outside the component, because that's what called the proxy function. The reason I wanted my functions private in that component was that I wanted to force the access to go through my proxy so that outside access didn't try to make any guesses or supposition about what methods I had in that component. Just send the request to the proxy method and let it figure out what the right thing to do with it is. That seems like good encapsulation to me.
But when I call the private function through my proxy method using the THIS scope of cfinvoke it doesn't really access the function from within the component, instead it looks like a request from outside and therefore triggers my onMissingMethod handler instead, just like if I called the private method manually from outside the component.
Makes me wonder if you really can proxy private methods within a component to selectively expose them to the outside world.