Photo from Chile

A Generic Decorator for Transfer - A Populate Method

Update: I've written a new blog post in my "How I Use Transfer" series which contains a much updated version of this method. So if you're interested in a Generic Decorator for Transfer, I suggest you check out this post, and the related posts.

I've been working with Transfer, on and off, for a few months now, and have come up with some methods that I've put in a generic decorator that I've found very useful. I thought I'd share them via my blog and get some feedback in the process.

The first one I want to discuss is a populate() method. This method is used to take data provided by a user (generally via a form post) and load that information into a Transfer Object, validating the data types along the way. This lets me avoid having to manually code a whole bunch of setXXX() statements, and also avoids the situation of trying to pass bad data into a setXXX() method. Note that this generic decorator is very much a work in progress. There are no promises that it will work perfectly as is, and I'm sure there's plenty of room for improvement. Oh, and this contains pieces of code that I've borrowed from a number of examples that I've seen on this list and elsewhere, but I didn't keep track of what came from whom, so please don't be offended if you see your code in here.

I am using a generic formbean to display data on any html form and also to accept data posted from any html form. I will go into the details of what my formbean looks like and how it works in a future blog post, but I mention it because it is material to this discussion.

So for now, just understand that when a form is posted I will have a formbean to work with, which contains the contents of the form scope, plus some other data that I automatically add to it on all requests (such as information about the current user).

Here's the complete code for the function. After that I'll walk through most of it to provide explanations:

view plain print about
1<cffunction name="populate" access="public" output="false" returntype="any" hint="Populates the TO with values from a formbean">
2    <cfargument name="FormBean" type="any" required="yes" />
3    <cfargument name="Errors" type="any" required="yes" />
4    <cfset var fn = 0 />
5    <cfset var varName = 0 />
6    <cfset var varValue = 0 />
7    <cfset var argName = 0 />
8    <cfset var argType = 0 />
9    <cfset var transferClassName = 0 />
10    <cfset var IsValid = true />
12    <cfloop collection="#this#" item="fn">
13        <cfset IsValid = true />
14        <cfif Left(fn,3) EQ "set" AND ArrayLen(getMetadata(this[fn]).parameters) EQ 1>
15            <cfset varName = Right(fn,Len(fn)-3) />
16            <!--- get the argument name and type --->
17            <cfset argName = getMetadata(this[fn]).parameters[1].name />
18            <cfset argType = getMetadata(this[fn]).parameters[1].type />
19            <!--- Update the LastUpdateTimestamp if found --->
20            <cfif varName EQ "LastUpdateTimestamp" AND argType EQ "Date">
21                <cfset setLastUpdateTimestamp(Now()) />
22            <!--- Code to deal with child transfer objects --->
23            <cfelseif argType EQ "" AND arguments.FormBean.varExists(Right(fn,Len(fn)-3) & "Id")>
24                <cfset transferClassName = GetTransferClassName(Right(fn,Len(fn)-3)) />
25                <cftry>
26                    <cfset varValue = GetTransfer().get(transferClassName,arguments.FormBean.getVar(Right(fn,Len(fn)-3) & "Id")) />
27                    <cfcatch type="any">
28                        <cfset IsValid = false />
29                    </cfcatch>
30                </cftry>
31                <cfif NOT varValue.getIsPersisted()>
32                    <cfset IsValid = false />
33                </cfif>
34                <cfif IsValid>
35                    <!--- Load the transfer object into the current TO --->
36                    <cfinvoke component="#this#" method="#fn#">
37                        <cfinvokeargument name="#argName#" value="#varValue#" />
38                    </cfinvoke>
39                <cfelse>
40                    <cfset ArrayAppend(arguments.Errors,"Configuration error: Trying to get a transfer object of class #transferClassName# with an Id of #arguments.FormBean.getVar(Right(fn,Len(fn)-3) & 'Id')# failed.") />
41                </cfif>
42            <!--- Add the value from the FormBean, if it exists --->
43            <cfelseif arguments.FormBean.varExists(varName)>
44                <!--- get the value from the FormBean --->
45                <cfset varValue = arguments.FormBean.getVar(varName) />
46                <!--- validate the datatype --->
47                <cfswitch expression="#argType#">
48                    <cfcase value="numeric">
49                        <cfif NOT isNumeric(varValue)>
50                            <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be numeric.") />
51                            <cfset IsValid = false />
52                        </cfif>
53                    </cfcase>
54                    <cfcase value="date">
55                        <cfif NOT isDate(varValue)>
56                            <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be a valid date.") />
57                            <cfset IsValid = false />
58                        </cfif>
59                    </cfcase>
60                    <cfcase value="boolean">
61                        <cfif NOT isBoolean(varValue)>
62                            <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be a boolean value.") />
63                            <cfset IsValid = false />
64                        </cfif>
65                    </cfcase>
66                </cfswitch>
67                <cfif IsValid>
68                    <cfinvoke component="#this#" method="#fn#">
69                        <cfinvokeargument name="#argName#" value="#varValue#" />
70                    </cfinvoke>
71                </cfif>
72            </cfif>
73        </cfif>
74    </cfloop>
75    <cfreturn arguments.Errors>

Whew, that's a bit of code. Let me explain a few parts.

First off, the function accepts two arguments, the formbean that contains the data and an array to which error messages will be added if any need to be reported back to the caller. At the end of the function that array is returned.

I'm looping through all of the functions that exist in the Transfer Object and if I find any that start with the letters "set" and accept one argument, then I treat that as a candidate for population and do some more processing:

view plain print about
1<cfloop collection="#this#" item="fn">
2    <cfset IsValid = true />
3    <cfif Left(fn,3) EQ "set" AND ArrayLen(getMetadata(this[fn]).parameters) EQ 1>

I extract the name of the field from the name of the function (varName) and determine the name of the argument that the function expects as well as the type of the argument:

view plain print about
1<cfset varName = Right(fn,Len(fn)-3) />
2<cfset argName = getMetadata(this[fn]).parameters[1].name />
3<cfset argType = getMetadata(this[fn]).parameters[1].type />

Next I do some processing that's specific to my application. You may find it useful to do some of your own application-specific processing in here instead of what I've coded. Most of my tables have a LastUpdateTimestamp field which I want to be updated whenever any changes are written to the database. So I'm looking for a method that matches that field, and if I find one I call it passing in Now():

view plain print about
1<cfif varName EQ "LastUpdateTimestamp" AND argType EQ "Date">
2    <cfset setLastUpdateTimestamp(Now()) />

Child Transfer Objects of the current object may have been selected via a <select> tag on the form, and therefore I may have the Id of the child Transfer Object in my form bean. If that's the case then I need to get the child Transfer Object in order to pass it into the method.

This next block of code looks for a function that expects a Transfer Object as an argument and also checks to see whether a corresponding Id field exists in the formbean. If so, it gets the corresponding transferClassName via a function that also exists within the generic decorator. That function (GetTransferClassName) maps field names to transfer classes (e.g. User=user.user, Payment=order.payment, etc.). It then attempts to get the child Transfer Object, and checks to see if a persisted Transfer Object is returned (because GetTransfer().get() may not find an existing object and therefore may just return a new object). If all is OK, it loads the child Transfer Object into the current Transfer Object. If there are any problems it reports an error back to the caller via the Errors array:

view plain print about
1<cfelseif argType EQ "" AND arguments.FormBean.varExists(Right(fn,Len(fn)-3) & "Id")>
2    <cfset transferClassName = GetTransferClassName(Right(fn,Len(fn)-3)) />
3    <cftry>
4        <cfset varValue = GetTransfer().get(transferClassName,arguments.FormBean.getVar(Right(fn,Len(fn)-3) & "Id")) />
5        <cfcatch type="any">
6            <cfset IsValid = false />
7        </cfcatch>
8    </cftry>
9    <cfif NOT varValue.getIsPersisted()>
10        <cfset IsValid = false />
11    </cfif>
12    <cfif IsValid>
13        <!--- Load the transfer object into the current TO --->
14        <cfinvoke component="#this#" method="#fn#">
15            <cfinvokeargument name="#argName#" value="#varValue#" />
16        </cfinvoke>
17    <cfelse>
18        <cfset ArrayAppend(arguments.Errors,"Configuration error: Trying to get a transfer object of class #transferClassName# with an Id of #arguments.FormBean.getVar(Right(fn,Len(fn)-3) & 'Id')# failed.") />
19    </cfif>

If neither of the two previous cases processed the given function, then I just need to check my formbean for a corresponding variable, and if one is found I validate the datatype. If the validation fails I send an error message back to the caller in the Errors array (yes, these messages could be friendlier), otherwise I call the method to populate the current Transfer Object with the value from the formbean:

view plain print about
1<cfelseif arguments.FormBean.varExists(varName)>
2    <!--- get the value from the FormBean --->
3    <cfset varValue = arguments.FormBean.getVar(varName) />
4    <!--- validate the datatype --->
5    <cfswitch expression="#argType#">
6        <cfcase value="numeric">
7            <cfif NOT isNumeric(varValue)>
8                <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be numeric.") />
9                <cfset IsValid = false />
10            </cfif>
11        </cfcase>
12        <cfcase value="date">
13            <cfif NOT isDate(varValue)>
14                <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be a valid date.") />
15                <cfset IsValid = false />
16            </cfif>
17        </cfcase>
18        <cfcase value="boolean">
19            <cfif NOT isBoolean(varValue)>
20                <cfset ArrayAppend(arguments.Errors,"The contents of the " & varName & " field must be a boolean value.") />
21                <cfset IsValid = false />
22            </cfif>
23        </cfcase>
24    </cfswitch>
25    <cfif IsValid>
26        <cfinvoke component="#this#" method="#fn#">
27            <cfinvokeargument name="#argName#" value="#varValue#" />
28        </cfinvoke>
29    </cfif>

And that's it! So far, after quite a bit of fiddling, it seems to be working for me, which makes processing of html forms almost automatic. Plus I don't have to worry about the Transfer police busting down my door for using setMemento().

I'd be keen to hear what others think of this method, and any suggestions you may have for improving it.

Interesting approach, Bob!

The only comment I would make is that you could have done the same thing via the Transfer meta data, but it would just be a slightly different implementation, rather than the CF meta data.

Otherwise, some interesting code!
# Posted By Mark Mandel | 9/26/07 8:29 PM
Hmm, I haven't looked into Transfer's meta data much. I'll have to do that and see whether any improvements can be made to this method.

# Posted By Bob | 9/26/07 8:34 PM