The other night, I was laying in bed, thinking about ColdFusion code (as I often times find myself doing), and it suddenly occurred to me that I wasn't sure of the expected behavior of ColdFusion custom tags that execute within a ColdFusion component context. Anyone who has created a user defined function (UDF) library or used free-standing functions knows that UDFs get bound to the calling page, not to the defining page. Unless, of course, the UDF is being called as a class method on a ColdFusion component; in that case, the UDF is bound to the component instance, not to the calling context. But what happens when you execute a ColdFusion custom tag within a ColdFusion component? What is the CALLER scope bound to? And, how do method invocations via the CALLER scope work?
To test this, I created a simple ColdFusion component that had a public method, a private method, and private variable:
<cfcomponent output="true" hint="I am here to test the binding of CALLER to a ColdFusion component."> <!--- Set a local variable to the component. ---> <cfset variables.name = "Sarah" /> <cffunction name="sayHello" access="public" returntype="void" output="true" hint="I say hello."> <!--- Say hello. Use the loacl NAME variable. ---> Hello, my name is #variables.name#! <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="privateMethod" access="private" returntype="void" output="false" hint="I am just a private method for CFDump."> <cfreturn /> </cffunction> <!--- Now, execute a custom tag that will invoke the sayHello() method in the CALLER context. ---> <cf_sayhello /> </cfcomponent>
As you can see, this is very simple; the public method, sayHello(), makes use of the private variable, name, in order to output a greeting. As part of the pseudo constructor, we are then executing a ColdFusion custom tag (sayhello.cfm) which will invoke the sayHello() method. The private method is there just to help us figure out which scopes are being bound (a private method will not show up on a public-scope binding).
The sayhello.cfm ColdFusion custom tag then turns around and invokes the sayHello() method on its Caller scope (and various combinations of caller-based paths):
sayhello.cfm (ColdFusion Custom Tag)
<!--- Define a local variable to the custom tag. This will not collide with the caller's variables scope, but since it is named the same as a variable in the caller context, it will allow us to figure out where the method invocation is being bound. ---> <cfset variables.name = "customTag" /> <!--- Invoke the sayHello() method on the CALLER scope. If the caller scope were just a standard CFM template, then the function would execute with a local, custom-tag context. However, in this case, the caller context is a CFC - where will the function bind?? ---> <cfset caller.sayHello() /> <br /> <br /> <!--- Now, try to invoking the method by explicitly calling the variables scope associated with the caller context. ---> <cfset caller.variables.sayHello() /> <br /> <br /> <!--- Now, try invoking the method by explicitly calling the this scope associated with the caller context. ---> <cfset caller.this.sayHello() /> <br /> <br /> <!--- Now, let's CFDump out the caller scope to see what the actual binding is. ---> <cfdump var="#caller#" label="CALLER Scope" /> <br /> <!--- And, let's output the caller-bound varaibles scope to see what the actual binding is. ---> <cfdump var="#caller.variables#" label="CALLER.Variables Scope" /> <br /> <!--- And, let's output the caller-bound this scope to see what the actual binding is. ---> <cfdump var="#caller.this#" label="CALLER.This Scope" /> <!--- Exit out of the tag. ---> <cfexit method="exitTag" />
As you can see, the first thing we are doing is defining a custom-tag-local variable, name, with the value "customTag". This will help us determine where the sayHello() method is being bound. Then, we try to execute the sayHello() method on various caller-based objects: caller, caller.variables, caller.this. When we instantiate the Greeting.cfc ColdFusion component, thereby running the above ColdFusion custom tag, we get the following page output:
Now this is some very interesting stuff! The first thing we can see is that some of our class method invocations actually acted like unbound user-defined functions; that is, their execution was bound to their calling context (the custom tag), and not to the ColdFusion component instance as one might expect:
caller.sayHello() :: NOT bound to CFC.
caller.variables.sayHello() :: NOT bound to CFC.
caller.this.sayHello() :: Bound to CFC.
Furthermore, from our CFDump output, we can see that there is a clear distinction between the CALLER scope the ColdFusion component itself. The Caller scope is mysterious and powerful beast. I have blogged before about the special way in which the Caller scope treats keys for getting and setting values. As such, it's not too surprising that the caller scope doesn't map to a ColdFusion component even when the target context is a ColdFusion component.
When you look at the CFDump, you'll notice that the ColdFusion custom tag has access to the private method - privateMethod() - of the ColdFusion component when it outputs the variables scope. However, variables-scope-based invocations do not bind to the CFC. The only way that we could get the method to bind to the CFC is when we invoke it via "caller.this"; unfortunately, when we do that, as you can see in our last CFDump, we no longer have access to the private method.
From this, I can see two important take-aways: 1) a ColdFusion custom tag, when executed from within a ColdFusion component, can gain access to both the public and private methods of the given component, and 2) although the custom tag can access both the public and private methods of the CFC, it cannot act as though it were a ColdFusion component mixin. In short, a custom tag acts like a completely separate, encapsulated object which happens to have the ability to access unbound private methods.
I suppose it would help to starting thinking about the Caller scope more like a "bridge" to another context and less like a reference to an existing object or scope.
Want to use code from this post? Check out the license.