Using A Proxy To Give Any Component Method-Chaining Abilities In ColdFusion

Posted August 6, 2009 at 6:40 PM by Ben Nadel

Tags: ColdFusion

When I was looking into using the CFQuery tag in CFScript using ColdFusion 9's new CFScript enhancement, it struck me that these ColdFusion service objects might be a good place to use method chaining. When working with these objects, we are building up values within a given CFC in order to call some sort of execution method on it. It seems that being able to chain the methods calls leading up that final execution would create a nice interface (ala jQuery).

At first, I started to think about extending the Query.cfc class, but then it occurred to me - using ColdFusion 8's OnMissingMethod() functionality (and some CF9 goodness), I can easily create a wrapper for any target CFC that will make the method calls on that target CFC chainable. Essentially, all the wrapper would have to do is proxy the method calls and return itself if the target method didn't return a value. To do this, I came up with the Chainable.cfc:

Chainable.cfc

  • <cfcomponent
  • output="false"
  • hint="I add chainability to my target component.">
  •  
  • <!--- Set properties. --->
  • <cfproperty
  • name="target"
  • type="any"
  • hint="I am the target component that is being made chainable."
  • />
  •  
  • <cfproperty
  • name="targetMethods"
  • type="string"
  • hint="I am the list of target methods that will be made chainable EVEN IF they return a result."
  • />
  •  
  •  
  • <cffunction
  • name="init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an initialized object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="target"
  • type="any"
  • required="true"
  • hint="I am the target component that is being made chainable."
  • />
  •  
  • <cfargument
  • name="targetMethods"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the list of target methods that will be made chainable EVEN IF they return a result."
  • />
  •  
  • <!--- Store the arguments. --->
  • <cfset this.setTarget( arguments.target ) />
  • <cfset this.setTargetMethods( arguments.targetMethods ) />
  •  
  • <!--- Return this object instance. --->
  • <cfreturn this />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onMissingMethod"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I invoke the given method on the target object and chain the results (if possible).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="missingMethodName"
  • type="string"
  • required="true"
  • hint="I am the name of the method that was invoked."
  • />
  •  
  • <cfargument
  • name="missingMethodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection used to invoke the given method."
  • />
  •  
  • <!--- Invoke the given method on the target object. --->
  • <cfinvoke
  • component="#this.getTarget()#"
  • method="#arguments.missingMethodName#"
  • argumentcollection="#arguments.missingMethodArguments#"
  • returnvariable="local.result"
  • />
  •  
  • <!---
  • Check to see if we have a result. We want to chain
  • the given method if the result is NULL or if the
  • given method was in the list of target methods.
  • --->
  • <cfif (
  • isNull( local.result ) ||
  • listFindNoCase(
  • this.getTargetMethods(),
  • arguments.missingMethodName
  • )
  • )>
  •  
  • <!---
  • The results we got back from the target
  • invocation were either NULL or blacklisted.
  • Return THIS object for method chaining.
  • --->
  • <cfreturn this />
  •  
  • <cfelse>
  •  
  • <!---
  • The result we got back from the target invocation
  • was a valid result. Return it.
  • --->
  • <cfreturn local.result />
  •  
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the Chainable.cfc class doesn't really define any methods at all; it simply proxies calls to the target using OnMissingMethod(). Then, if the target method doesn't return a value, Chainable.cfc returns itself. As I was writing this though, it occurred to me that a lot of times a method will return a value that is not critical. For example, ArrayDelete() returns a boolean as to whether or not an array item was actually deleted. While this is good information to know, I built in the ability to make these sorts of methods chainable as well. The TargetMethods property of Chainable.cfc is a list of methods that will be chained even if they return a value.

With Chainable.cfc defined, I can now wrap my Query.cfc instances making their methods chainable:

  • <cfscript>
  •  
  • // Create a new chainable query object. To do this,
  • // we have to create an instance of our Chainable
  • // component and passour new query instance to it.
  • getGirl = new Chainable( new Query() );
  •  
  • // Set the properties end execute. Notice that these
  • // methods calls are all being chained in one expression.
  • girls = getGirl
  • .setSQL("
  • SELECT
  • id,
  • name,
  • hair
  • FROM
  • girl
  • WHERE
  • name = :name
  • ")
  • .addParam(
  • name = "name",
  • value = "Tricia",
  • cfsqltype = "cf_sql_varchar"
  • )
  • .execute()
  • .getResult()
  • ;
  •  
  • // Output the resultant girl query.
  • writeOutput( "Girl: " & girls.name );
  •  
  • </cfscript>

As you can see, once the Query.cfc instance is wrapped in our Chainable.cfc instance, all native Query.cfc methods are now proxied through Chainable.cfc, making it completely chainable. When we run the above code, we get this output:

Girl: Tricia

Not everyone likes method chaining; and, in some cases, it can certainly make code difficult to read. But, I think that at times, it can really clean things up. In cases like this, for example, where several different method calls all relate to each other and are building up to a single execution, I think it is a really nice option.




Reader Comments

Aug 6, 2009 at 7:21 PM // reply »
153 Comments

This is a very nice hack. Solid thinking, indeed.


Aug 6, 2009 at 7:32 PM // reply »
11,243 Comments

@Rick,

Thanks my man.


Aug 7, 2009 at 9:00 AM // reply »
5 Comments

Hey Ben,

Have you done any performance testing with this? Just curious...

Thanks!


Aug 7, 2009 at 9:22 AM // reply »
11,243 Comments

@Jared,

There will be some overhead since there is a proxy object and additional logic. However, from everything that I've been told, they built OnMissingMethod() functionality to be extremely fast because it was designed specifically to add logic around method calls.


Aug 7, 2009 at 9:23 AM // reply »
5 Comments

Awesome! Thanks!


Aug 14, 2009 at 12:29 PM // reply »
25 Comments

Very clever Ben!


Aug 17, 2009 at 2:11 PM // reply »
11,243 Comments

@Jared, @Hussein,

Thanks guys. I love chaining.


Aug 24, 2009 at 4:41 AM // reply »
2 Comments

nice article


Nov 20, 2009 at 8:52 AM // reply »
1 Comments

You just can't stop abusing the onMissingMethod feature, can you Ben? ;)


Nov 20, 2009 at 1:00 PM // reply »
11,243 Comments

@DeepDown,

I'll never stop.... NEVER :)


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 22, 2013 at 5:35 PM
Script Tags, jQuery, And Html(), Text() And Contents()
This is still an issue 2 years later. jQuery is supposed to remediate these cross browser issues, no? I have been unable to find any statement from the jQuery team calling this behavior "by de ... read »
May 22, 2013 at 12:44 PM
Ask Ben: Query Loop Inside CFScript Tags
In cf10, if you call a function that has: local.result = {}; local.result.msg = ""; local.svc = new query(); local.svc.setSQL("SELECT * FROM..."); local.obj = local.svc.exe ... read »
May 22, 2013 at 12:29 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben: What version of Java are you using? Also, did you test users.id to see what Java reports as the data type? I wonder if it's not a Java primitive data type, but getting returned as something ... read »
May 22, 2013 at 11:47 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dana, Awesome - so it looks like this bug was fixed in ColdFusion 10. Thanks so much for double-checking that. ... read »
May 22, 2013 at 11:37 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
When I c&p and run on cf10, I get: Selected User IDs: 1,4 User 1 selected: YES - YES User 2 selected: NO - NO User 3 selected: NO - NO User 4 selected: YES - YES User 5 selected: NO - ... read »
May 22, 2013 at 11:27 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Tom, Good thought, but no dice. Both of these still exhibit the same behavior: users.id[ users.currentRow ] users[ "id" ][ users.currentRow ] It's just something whacky happening with ... read »
May 22, 2013 at 11:07 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
Could your problem be that "users.id" is actually an ARRAY, not a single value? Perhaps try it again with "users.id[1]" (I only have CF8 here at work). ... read »
May 22, 2013 at 7:52 AM
Nested Views, Routing, And Deep Linking With AngularJS
Hi, Just a quick thank you. As it happens, for my own purposes, the pending ui-router work being done in native angular is likely the one I'll adopt, but your exploration, code and documentation of ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools