Menu

As simple as possible, as complex as necessary

The simplicity of jQuery style generic getters and setters in ColdFusion Part 1

24 May 2011

One of the reasons I've struggled a bit with ColdFusion ORM (other than just being slow to learn), has been the need to make it fit with certain development practices I've grown to value. One I was keen not to sacrifice is the use of jQuery style getters and setters. As you no doubt know, in jQuery you can get and set properties such as form input values like so:

$( "input#name" ).val( "Tibbles" ); // Set
var name = $( "input#name" ).val(); // Get

I find doing the same in CF makes for more readable code and saves 3 characters on every call compared to the getProperty/setProperty convention.

<cfscript>
	myCat = New Cat();
	myCat.name( "Tibbles" );
</cfscript>
<cfoutput>
	NAME: #myCat.name()#
</cfoutput>

Like many techniques which have proven themselves over time, this came from Hal Helms who posited the idea in a comment on Ben Nadel's blog back in 2008, in a discussion about "generic" (a.k.a. implicit and synthesised) getters/setters using CF8's new onMissingMethod() handler. Hal and I both posted code, but here's what I ended up using.

<cfcomponent name="Base" output="false" hint="I'm a base 'transient' object. All transient objects should extend me and call my init method">

	<cffunction name="init" output="false">
		<!--- 
			Create a separate container for the properties of each instance.
			Because the struct is in the variables scope it can't be accessed or changed directly: only via our get/set methods
		--->
    <cfset variables.instance	=	{}>
    <cfreturn this>
  </cffunction>
	
	<cffunction name="get" returntype="any" output="false"
			hint="I'm a generic Getter: pass me the name of a property and if it's in the instance variables struct I'll return the current value">
		<cfargument name="propertyName" required="true">
		<cfif StructKeyExists( instance,arguments.propertyName )>
			<cfreturn instance[ arguments.propertyName ]>
		</cfif>
		<!--- Can't find that property --->
		<cfthrow type="Base.NonExistantProperty" message="Cannot find ""#arguments.propertyName#"" in the instance data of #GetMetaData( this ).name#">
	</cffunction>
	
	<cffunction name="set" returntype="void" output="false"
			hint="I'm a generic Setter: if the property you specify exists I'll set it to the value you supply">
		<cfargument name="propertyName" required="true">
		<cfargument name="newValue" required="true">
		<cfif NOT StructKeyExists( instance,arguments.propertyName )>
			<!--- Can't find that property --->
			<cfthrow type="Base.NonExistantProperty" message="Cannot find ""#arguments.propertyName#"" in the instance data of #GetMetaData( this ).name#">
		<cfelseif NOT StructKeyExists( arguments,"newValue" )>
			<!--- You haven't given me a new value to set! --->
			<cfthrow type="Base.NewValueNotSupplied" message="Please supply a new value to the Set method">
		</cfif>
		<cfset instance[ arguments.propertyName ]	=	arguments.newValue>
	</cffunction>

	<cffunction name="onMissingMethod" output="false"
			hint="I take over whenever a method is invoked that doesn't exist. If the non-existent method name matches an instance property, I'll call the get() or set() function depending on whether a new value has been passed or not.">
		<cfargument name="missingMethodName" required="true">
		<cfargument name="missingMethodArguments" type="struct" required="true">
		<cfif StructKeyExists( instance,arguments.missingMethodName )>
			<!--- OK, the name of the method matches an instance property so let's call the generic getter or setter --->
			<cfif StructIsEmpty( arguments.missingMethodArguments )>
				<!--- No new value specified so just get --->
				<cfreturn get( arguments.missingMethodName )>
			</cfif>
			<!---We've got a new value so do a set --->
			<cfset set( arguments.missingMethodName,arguments.missingMethodArguments[ 1 ] )>
		<cfelse>
			<!--- Property not found so must be a genuine missing method--->
			<cfthrow type="Base.NonExistantMethod" message="The method ""#arguments.missingMethodName#"" is not defined in component #GetMetaData( this ).name#">
		</cfif>
	</cffunction>
	
</cfcomponent>

To enable generic jQuery getters/setters in a component it would just need to extend the above base CFC and have its instance properties defined...

<cfcomponent extends="Base" displayname="Cat" output="false">

	<cffunction name="init" output="false">
		<cfscript>	
			// call the Base init method to create the instance struct
			super.init();
			// define the properties/defaults of each instance
			instance.gender = "Male";
			instance.name = "";
			instance.personality = "";
			return this;
		</cfscript>
	</cffunction>

</cfcomponent>

We can then write code such as...

<cfscript>
	myCat = CreateObject( "Component","Cat" ).init();
	myCat.name( "Tibbles" );
	myCat.personality( "timid" );
</cfscript>
<cfoutput>
	NAME: #myCat.name()#
	<br>GENDER: #myCat.gender()#
	<br>PERSONALITY: #myCat.personality()#
</cfoutput>

...which outputs...

NAME: Tibbles
GENDER: Male
PERSONALITY: timid

If you need to protect or control a particular variable to prevent invalid values or otherwise customise what happens, you simply write a method with that property's name. For example, to make sure the gender can't be set to anything other than male or female, we can add a "gender" method to the Cat.cfc (so gender() is no longer "missing", meaning onMissingMethod() will not fire).

<cfcomponent extends="Base" displayname="Cat" output="false">

	<cffunction name="init" output="false">
		<cfscript>	
			super.init();
			instance.gender = "Male";
			instance.name = "";
			instance.personality = "";
			return this;
		</cfscript>
	</cffunction>
	
	<cffunction name="gender" output="false">
		<cfargument name="value" required="false">
		<cfscript>
			if( NOT StructKeyExists( arguments,"value" ) )
				return instance.gender;
			if( ListFindNoCase( "Male,Female",arguments.value ) )
				instance.gender = arguments.value;;
			return;
		</cfscript>
	</cffunction>
</cfcomponent>

Then if we try the following...

<cfscript>
	myCat = CreateObject( "Component","Cat" ).init();
	myCat.gender( "Hmm, not really sure" );
	myCat.name( "Tibbles" );
	myCat.personality( "timid" );
</cfscript>

...the gender value will be ignored and we'll keep the default.

NAME: Tibbles
GENDER: Male
PERSONALITY: timid

A better way of doing this in CF9

This code has worked well on CF8 and 9, but failed as soon as I made the switch to ORM. Fortunately I've managed to resolve the issues and in doing so have come up with an even better way of doing this in CF9 whether or not ORM is enabled, which I'll explore in my next two posts.

Comments

  • Formatting comments: See this list of formatting tags you can use in your comments.
  • Want to paste code? Enclose within <pre><code> tags for syntax higlighting and better formatting and if possible use script. If your code includes "self-closing" tags, such as <cfargument>, you must add an explicit closing tag, otherwise it is likely to be mangled by the Disqus parser.
Back to the top