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 23, 2013 at 11:06 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Are you talking about As Number: YES As String: YES As Java: YES? If so, that's with 3 different ways of referencing the constant 1, not users.id[1]. Query object references(*) are what seem ... read »
May 23, 2013 at 9:55 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dan, According to the CF Admin, I'm running Java "1.6.0_45". As far as the DB column, in the database it's an INT. I'll see if I can dig into what CF sees it as. @WebManWalking, But h ... read »
May 23, 2013 at 9:49 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, I think the problem is that we're used to loose typing in ColdFusion, like JavaScript. If a value is a number but it's needed in an expression to be a string, noooo problem. I've encountered ... read »
May 23, 2013 at 9:47 AM
ColdFusion QueryAppend( qOne, qTwo )
You rock! Thank you, thank you, thank you!!! ... read »
May 23, 2013 at 5:19 AM
Ask Ben: Print Part Of A Web Page With jQuery
How to print also the background color of table cells and table lines ... read »
May 23, 2013 at 3:55 AM
Javascript Array Methods: Unshift(), Shift(), Push(), And Pop()
very interesting and helpful too. ... read »
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 »
InVision App - Prototyping Made Beautiful With Prototyping Tools