cfproperty, onMissingMethod and data type validation

September 16, 2008

I've been playing with getting and setting variables using the onMissingMethod function of ColdFusion 8 components. I've never been a fan of this technique as it means that you don't have an API for your class so you (and any other developers in your team) need to open up the cfc to see what values can be manipulated, however I can also see the benefits of not having to type accessors for every variable. Anyway I decided that there must be a happy medium and this is what I've come up with...

myClass.cfc

<cfcomponent extends="dynamicAccessors">
  
  <!--- define properties that can be manipulated via an accessor --->
  <cfproperty name="FooNumber" 
    type="numeric" 
    hint="I can be manipulated via the getFooNumber and setFooNumber methods" />
  <cfproperty name="FooString" 
    type="string" 
    hint="I can be manipulated via the getFooString and setFooString methods" />
  <cfproperty name="FooUuid" 
    type="uuid" 
    hint="I can be manipulated via the getFoodUuid and setFoodUuid methods" />
  <cfproperty name="FooAny" 
    hint="I can be manipulated via the getFooAny and setFooAny methods" />
  
  <cfset variables.instance = {} />
  
  <cffunction name="init">
    <cfreturn this />
  </cffunction>

</cfcomponent>

dynamicAccessors.cfc

<cfcomponent hint="I am an abstract class that provides dynamic getters and setters. I should not be called directly but inherited using the extends attribute of cfcomponent">

  <cffunction name="onMissingMethod"
    access="public"
    hint="I act as a generic getter and setter for the class properties."
    description="Usage getXYZ() or setXYZ(value) where XYZ is the property name" 
    returntype="any">
    <cfargument name="missingMethodName" type="string" />
    <cfargument name="missingMethodArguments" type="struct" />
    
    <!--- the first time we get or set a property we need to build a structure of properties and types --->
    <cfset var propertyName = "" />

    <cfset getAccessors() />

    <cfswitch expression="#Left( arguments.missingMethodName, 3 )#">
      <cfcase value="get">
        <cfset propertyName = ReplaceNoCase( arguments.missingMethodName, "get", "" ) />
        <cfif StructKeyExists( variables.instance, propertyName )>
          <cfreturn variables.instance[ propertyName ] />
        <cfelse>
          <cfthrow message="Unknown property '#propertyName#'. Cannot get value." />
        </cfif>
      </cfcase>
      <cfcase value="set">
        <cfset propertyName = ReplaceNoCase( arguments.missingMethodName, "set", "" ) />
        <cfif StructKeyExists( variables.instance.properties, propertyName )>
          <!--- check data type --->
          <cfif IsValid( variables.instance.properties[ propertyName ], arguments.missingMethodArguments[1] ) >
            <cfset variables.instance[ propertyName ] = arguments.missingMethodArguments[1] />
          <cfelse>
            <cfthrow message="Property '#propertyName#' expects the '#variables.instance.properties[ propertyName ]#' data type." />
          </cfif>
        <cfelse>
          <cfthrow message="Unknown property '#propertyName#'. Cannot set value." />
        </cfif>
      </cfcase>
      <cfdefaultcase>
        <cfthrow message="unknown method" />
      </cfdefaultcase>
    </cfswitch>
  </cffunction>

  <cffunction name="getAccessors"
    hint="I build a structure of public properties and data types." 
    access="public">
    
    <cfset var properties = "" />
    <cfset var index = "" />
    
    <cfif StructKeyExists( variables.instance, "properties" )
      AND IsStruct( variables.instance.properties )>
      <cfreturn variables.instance.properties />
    <cfelse>
      <!--- properties struct has not been created yet --->
      <cfset variables.instance.properties = {} />
      <cfset properties = getMetaData(this).Properties />
      
      <cfloop array="#properties#" index="index">
        <cfif IsDefined("index.type")>
          <cfset StructInsert( variables.instance.properties, index.name, index.type ) />
        <cfelse>
          <cfset StructInsert( variables.instance.properties, index.name, "any" ) />
        </cfif>
      </cfloop>
      
      <cfreturn variables.instance.properties />      
    </cfif>
    
  </cffunction>

</cfcomponent>

You can test out the code using this script.

<cfset Test = CreateObject( "component", "com.myClass").init( ) />

<cfdump var="#Test.getAccessors()#" label="Class Properties that can be set" />

<cfset Test.setFooString ( 'Loreum Ipsum' ) />
<cfset Test.setFooUuid ( CreateUUID() ) />
<cfset Test.setFooAny ( Test ) />

<cfoutput>
getFooUUID = #Test.getFooUUID( )#<br />
getFooString = #Test.getFooString( )#<br />
</cfoutput>

<!--- try to set an invalid property --->
<cfset Test.setFooBar ( 'ABC123' ) />

<!--- try to set an invalid data type --->
<cfset Test.setFooUuid ( 'ABC123' ) />

So what is going on? Well I've created an abstract class called dynamicAccessors.cfc which myClass.cfc inherits (so it has all the methods from dynamicAccessors). I use the cfproperty tag in myClass.cfc to define the instance variables that get be either set or retrieved with a get call. The onMissingMethod just checks to see if you are calling getXYZ or setXYZ. If you are trying to set the value then it will check that the instance variable exists and if it does makes sure that the correct data type is being passed.

If you are concerned about the performance hit that you might take by checking that instance variables exist and then validating the data type, then don't forget that you can always have a different dynamicAccessors.cfc on your live website which just get and sets variables without the checks. This is a similar concept to disabling the CFC type check in your CFIDE, meaning that you get the benefit of type checking in development and performance in your live environment.

I've set the access attribute of the getAccessors method to public so that you can call it for convenience. If you use the component explorer you will have a pretty good and API.

Example CFC documentation

aliaspooryorik.com.myClass
Component myClass


hierarchy: WEB-INF.cftags.component
      aliaspooryorik.com.dynamicAccessors
            aliaspooryorik.com.myClass
path: D:\Inetpub\wwwroot\aliaspooryorik\com\myClass.cfc
properties: FooAny, FooNumber, FooString, FooUuid
methods: init
inherited methods: getAccessors, onMissingMethod

* - private method

Property Hint Type Req. Implemented In Default Value
FooAny I can be manipulated via the getFooAny and setFooAny methods     myClass -
FooNumber I can be manipulated via the getFooNumber and setFooNumber methods numeric   myClass -
FooString I can be manipulated via the getFooString and setFooString methods string   myClass -
FooUuid I can be manipulated via the getFoodUuid and setFoodUuid methods uuid   myClass -

 

init
init ()

Output: enabled

 


4 comments

  1. Imo, this is also an improvement over using the accessors and mutators as an indication of what properties that particular CFC has - because - it's much more compact. You can see the entire list of properties at the top of the CFC without any scrolling and without any other visual distractions (function bodies and duplication, since you've got a getX and a setX for each property) and without having to resort to an external tool like CF's CFC Explorer to see them.

    The objects in the Members onTap plugin I wrote for the onTap framework are similarly documented (with cfproperty tags), although I don't include validation in those components. I just don't see a need. They work and they're documented. If you didn't read the documentation and you put a string in a numeric property, oh well... RTM.

    Comment by ike – September 16, 2008
  2. @Ike, I don't use the CFC Explorer very much as I tend to cfdump the component to see the accessors, which is why I made the getAccessors() method public. The output from <cfdump var="#object.getAccessors()#" /> is much cleaner than <cfdump var="#object#" />. I may change the getAccessors method to return a query object instead of a struct so that I can include the usage hint as well.

    Comment by John Whish – September 17, 2008
  3. I've never actually cared for the CFC explorer myself. I have a doc.cfc I developed for the onTap framework that does what the CFC explorer does, but does it better. It shows you not just the functions and properties of the current class, but of all the classes it extends and it tells you which class each given property or method was inherited from. Saves you from having to hunt through the tree to see what's going on in your class hierarchy. :)

    Although in my own case I rarely use those docs merely because I developed all those components and for the things I use regularly I remember most of the methods. It does come in handy from time to time tho.

    And I can definitely see where dumping the accessors would be visually easier to process than dumping the entire object. I may go back and tweak the framework components with something similar. :)

    Comment by ike – September 17, 2008
  4. @Ike, doc.cfc sounds cool! Yeah, being able to dump the accessors is much easier to read than dumping the whole class. It'd be cool if I inspired a feature of the onTap framework :)

    Comment by John Whish – September 18, 2008

Leave a comment

If you found this post useful, interesting or just plain wrong, let me know - I like feedback :)

Please note: If you haven't commented before, then your comments will be moderated before they are displayed.