The simplicity of jQuery style generic getters and setters in ColdFusion Part 2
As I mentioned in my previous post on jQuery-style getters/setters in CF, the technique won't work with ORM in CF9. The problem is that properties in ORM objects need to be defined using cfproperty
(or the script equivalent) which causes them to be stored directly in the variables scope of the CFC rather than isolated in an "instance" struct or other specific container. Also the properties defined aren't actually available in the variables scope until a value for each has been set.
This makes it harder to map from the "property name as a function call" to the get/set functions as I had been doing. I tried various workarounds of increasing complexity but none was satisfactory.
I was about to resign myself to abandoning my beloved jQuery style and putting up with the automatic getProperty/setProperty methods provided by CF9 (cue extensive Find & Replace), when the thought occurred that there must be a way of simply manipulating that built-in functionality to make them respond to jQuery style function calls. Sure enough, after a bit of trial and error, I got it working... and working better than my previous code... and in a non-ORM enabled context as well.
Base.cfc
<cfcomponent name="Base" output="false" hint="I'm a base 'transient' object. Transient objects should extend me, call my init method and have accessors=true">
<cfscript>
function init()
{
return this;
}
function get( required string propertyName )
hint="I'm a generic Getter: pass me the name of a property and if exists and is gettable, I'll return the current value"
{
try
{
// Get a reference to the CF generated generic getter for this property
local.get = this[ "get" & arguments.propertyName ];
}
catch( any exception )
{
if( exception.message CONTAINS "is undefined" )// Couldn't find the property or it has getter=false
Throw( message="The property ""#arguments.propertyName#"" is not defined or not gettable in component #getMetaData( this ).name#",type="Base" );
rethrow;
}
// Execute the CF generated getter via the reference
return local.get();
}
void function set( required string propertyName, required any newValue )
hint="I'm a generic Setter: if the property you specify exists and is settable, I'll set it to the value you supply"
{
if( IsNull( arguments[ 1 ] ) )
Throw( message="Please pass a property name and a value",type="Base" );
local.arg1 = arguments[ 1 ];
if( NOT IsValid( "variableName",local.arg1 ) )
Throw( message="First argument must be the name of the property to set.",type="Base" );
if( IsNull( arguments[ 2 ] ) )
Throw( message="Please supply a value.",type="Base" );
local.arg2 = arguments[ 2 ];
try
{
// Get a reference to the CF generated generic setter for this property
local.set = this[ "set" & local.arg1 ];
// Execute the setter with the passed new value
local.set( local.arg2 );
}
catch( any exception )
{
if( exception.message CONTAINS "is undefined" ) // genuine missing method
Throw( message="The property ""#local.arg1#"" is not defined or not settable in component #getMetaData( this ).name#",type="Base" );
rethrow;
}
}
function onMissingMethod
(
required string missingMethodName
,required struct missingMethodArguments
)
hint="I take over whenever a method is invoked that doesn't exist. If the non-existent method name matches a property, I'll call the get() or set() function depending on whether a new value has been passed or not."
{
try
{
if( StructIsEmpty( arguments.missingMethodArguments ) )
// No arguments, return get()
return get( arguments.missingMethodName );
// arguments mean set()
set( arguments.missingMethodName,arguments.missingMethodArguments[ 1 ] );
}
catch( any exception )
{
if( exception.message CONTAINS "is undefined" ) // genuine missing method
Throw( message="The method ""#arguments.missingMethodName#"" is not defined in component #getMetaData( this ).name#",type="Base" );
rethrow;
}
}
</cfscript>
</cfcomponent>
We still have our 3 methods: get(), set() and onMissingMethod(), but they now plug in to the defined cfproperties and generated getters/setters of the components which extend Base.cfc.
Cat.cfc
<cfcomponent extends="Base" accessors="true" displayname="Cat" output="false">
<!---define the properties of each instance--->
<cfproperty name="gender">
<cfproperty name="name">
<cfproperty name="personality">
<cfscript>
function init()
{
super.init();
return this;
}
function setGender( required string value )
{
if( ListFindNoCase( "Male,Female",arguments.value ) )
variables.gender = arguments.value;
}
</cfscript>
</cfcomponent>
The first improvement to notice is that in order to protect the gender property (to prevent invalid values), we only have to override the "set" method. If we called the method just "gender()" as in the previous post, it would work, but have to handle both get and set operations. By specifying "setGender" we're simply overriding the CF generated setter with our own, which will be called by onMissingMethod()
when a value is passed to gender(). If no value is passed then the generic get() will be called. There's no need to define that as getting the value doesn't need protecting. Simpler. Nicer.
Secondly, unlike the previous code, we can also protect variables from being get or set without having to specify a method at all: by simply using the built-in CF9 getter="false"
and/or setter="false"
attributes of cfproperty
.
If we add an "owner" property but decide I am going to own all the cats round here, we can prevent anybody else being set as an owner.
<cfcomponent extends="Base" accessors="true" displayname="Cat" output="false">
<cfproperty name="gender">
<cfproperty name="name">
<cfproperty name="personality">
<cfproperty name="owner" setter="false">
<cfscript>
function init()
{
super.init();
variables.owner = "Julian";
return this;
}
</cfscript>
</cfcomponent>
Trying to set the owner using our jQuery-style method will throw the same exception as would calling setOwner.
<cfscript>
myCat = New Cat();
myCat.name( "Tibbles" );
myCat.owner( "Jeremy" );
</cfscript>
<cfoutput>
NAME: #myCat.name()#
<br>OWNER: #myCat.owner()#
</cfoutput>
"The property "owner" is not defined or not settable in component Cat."
A downside of this "CF9" approach is that in a non-ORM context, any defaults you set on the properties will be ignored, although there is an apparently straightforward workaround.
In the final post I'll mention some extra enhancements that can be added. In the meantime, if you see any flaws/typos/problems in, or possible improvements to the code - especially anything that makes it simpler - please do let me know.