Decorating Eye for the Transfer Guy

This post is more than 2 years old.

Today I get to talk about my favorite Transfer topic (although events may supplant it) - Decorators. No, I'm not talking about the people who come in and hang wallpaper or rearrange your furniture. Rather, decorators in Transfer allow you to extend and build upon the TransferObjects automatically created for you when you get data via Transfer.

If you remember, every time you get a Transfer object, like so:

<cfset emp = application.transfer.get("employee", 1)>

Transfer creates a CFC based on the XML definition of the employee type. There are times though when you may want to add a bit of business logic to this CFC. As a simple example, consider the act of getting a name from our employee. We have a first name and a last name, but what if we wanted a simple way to just get a name, perhaps based on: "Lastname, First".

In order to do this we create a decorator. Begin by modifying the XML to tell Transfer where the decorator will exist:

<object name="employee" table="employees" decorator="empdir.model.employee">

Remember empdir was defined in Application.cfc as a root level mapping for my sample application. I created a new folder named model and dropped a CFC in there named employee. At the simplest level, a decorator CFC can look like this:

<cfcomponent extends="transfer.com.TransferDecorator" output="false"> </cfcomponent>

Note that I extend transfer.com.TransferDecorator. This is the only requirement. Now if we want to add getName to our decorator, we can do:

<cffunction name="getName" access="public" returntype="string" output="false"> <cfreturn getLastName() & ", " & getFirstName()> </cffunction>

Pretty simple, right? Here is a slightly more advanced example. Converting the DOB into an age value:

<cffunction name="getAge" access="public" returntype="numeric" output="false"> <cfreturn dateDiff("yyyy", getDOB(), now())> </cffunction>

Using it is just as simple as calling any of the 'normal' methods built into the TransferObject:

<cfset emp = application.transfer.get("employee", 1)>

<cfoutput>#emp.getName()# is #emp.getAge()# years old.<br/></cfoutput>

This returns:

Camden, Raymond is 35 years old.

Along with overwriting get methods, you can overwrite set methods as well. In order to actually store the value you are overriding, you need to call getTransferObject() first. Here is a good example I'll steal from the docs:

<cffunction name="setHomePageURL" access="public" returntype="void" output="false"> <cfargument name="url" type="string" required="Yes"> <cfif not findNoCase("http://", arguments.url)> <cfset arguments.url = "http://" & arguments.url> </cfif> <cfset getTransferObject().setHomePageURL(arguments.url)> </cffunction>

The setHomePageURL method defined here looks for a missing http://. If it doesn't exist in the passed in value then it will be added to the value. Notice then the last call uses getTransferObject() to "hook" into the real TO and call the real setHomePageURL.

For my employee decorator I decided to try something interesting. I want usernames to be unique. So why not do a security check when a username is set? I will follow these rules:

If no other employee has the same username, we are good.
If another employee has the same username, and isn't me, then we have an error.

Let's look at how I built this.

<cffunction name="setUsername" access="public" returntype="void" output="false"> <cfargument name="username" type="string" required="true">
&lt;!--- see if another user with this name exists... ---&gt;
&lt;cfset var t = getTransfer()&gt;
&lt;cfset var olduser = t.readByProperty("employee", "username", arguments.username)&gt;

&lt;!--- if it doesn't exist, we are good ---&gt;
&lt;cfif not olduser.getIsPersisted()&gt;
	&lt;cfset getTransferObject().setUsername(arguments.username)&gt;
&lt;!--- if it did exist, but was me, then its ok too ---&gt;
&lt;cfelseif olduser.sameTransfer(getTransferObject())&gt;
	&lt;cfset getTransferObject().setUsername(arguments.username)&gt;
&lt;!--- ok, throw an error! ---&gt;
&lt;cfelse&gt;
	&lt;cfthrow message="An employee with this username already exists."&gt;
&lt;/cfif&gt;

</cffunction>

There is a lot going on here, so I'll tackle it line by line. I begin by calling getTransfer(). This method exists in decorators and gives me access to the main Transfer factory. I then do a read check using readByProperty. Remember that if readByProperty will return a virgin object if it doesn't exist. One of the special methods we can then use is getIsPersisted(). If this is false, it means olduser didn't actually exist, so we didn't find a match.

If it did exist, we can use another built in method, sameTransfer(), that compares two TransferObjects to see if they match.

If the above two checks fail, we throw an error. This was just a quick example and probably does not represent the best way to do validation in Transfer.

A cool example that my coworker Sean came up was this:

<cffunction name="delete" returntype="void" access="public" output="false"> <cfset getTransfer().delete(this) /> </cffunction>

<cffunction name="save" access="public" returntype="void" output="false"> <cfreturn getTransfer().save(getTransferObject())> </cffunction>

What does this do? Well remember how we use the Transfer Factory to save and delete objects? Well with these two methods in our decorator we can now do:

<cfset thirdemp = application.transfer.get("employee", 15)> <cfset thirdemp.setLastName("Random#randRange(1,1000)#")> <cfset thirdemp.save()>

This "feels" really natural to me. Sean actually built this into a separate CFC called a BaseDecorator that the other decorator's extended.

One more cool thing decorators can do - configuring. If you want to run custom code when Transfer creates an instance of as TransferObject, you can simply add a configure method to the decorator. For example:

<cffunction name="configure" access="private" returntype="void" output="false"> <cfset setLastName("anon")> <cfset setFirstName("anon")> </cffunction>

This will set the name of a new employee to anon, anon. Not terribly exciting but rather simple to use.

I'll leave you with another link from the main docs: How to Encrypt User Passwords Using a Decorator. This is a good example of using decorators and handling encryption.

I've updated the download zip to include the new code. As always, comments are welcome!

Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Will Wilson posted on 11/26/2008 at 2:04 PM

Hi Ray,

Do you know how to reset transfers cache? I know you mentioned it does it automatically after an Insert or Update but when I make a modification to my decorator it doesn't pick up the change. Only way I've found so far is to reboot CF

Comment 2 by Raymond Camden posted on 11/26/2008 at 5:00 PM

If you use my application, then you have to reload Transfer. Just add ?reinit=1 to the URL. This will rerun onApplicationStart.

Comment 3 by Francois Levesque posted on 11/26/2008 at 5:09 PM

@Will,

Are you using an IoC (ColdSpring, LigthWire) or MVC (ColdBox, Model-Glue, etc) that could be caching your transfer objects?

Comment 4 by Nathan Mische posted on 11/26/2008 at 6:51 PM

In the delete and save examples from Sean, is there any reason why delete uses this and save use getTransferObject()?

Comment 5 by Will Wilson posted on 11/28/2008 at 4:10 AM

oh lol its as easy as that! thanks :)

Comment 6 by Raymond Camden posted on 11/28/2008 at 8:22 PM

You know I'm not sure. Let me ask him.

Comment 7 by Sean Corfield posted on 11/28/2008 at 8:48 PM

They were written at different times and I was probably writing getTransferObject() a lot when I wrote save() because I'd been working in decorators all day long. save() could just as easily use THIS.

Comment 8 by Dan O&aposKeefe posted on 12/15/2008 at 4:24 AM

But what do these 2 functions do differently than the native save() and delete() funtions of the TO?

Comment 9 by Raymond Camden posted on 12/15/2008 at 5:14 AM

save and delete() don't exist in the TO.

http://docs.transfer-orm.co...

Comment 10 by Dan O&aposKeefe posted on 12/15/2008 at 5:39 AM

So the only difference is:

<cfset thirdemp.save()>

versus:

<cfset t.save("thirdemp")>

if t represents getTransfer()

??

Comment 11 by Raymond Camden posted on 12/15/2008 at 5:44 AM

Yep. It isn't a huge thing, but for me, using ob.save() felt more natural, and it was less typing as well. t would normally be in RAM so you would have application.t.save(ob)

Comment 12 by Dan O&aposKeefe posted on 12/15/2008 at 7:02 PM

Thanks, just wanted to make sure I was not missing something.

Comment 13 by Scott Brady posted on 8/18/2009 at 10:12 PM

Ray, you mentioned "Sean actually built this into a separate CFC called a BaseDecorator that the other decorator's extended." I'm trying to get that to work, but it's not. I have a BaseDecorator that extends "transfer.com.TransferDecorator" and then my object's decorator extends that BaseDecorator. Is that the correct implementation?

When I do that, I'm getting "Element TRANSFEROBJECT is undefined in INSTANCE. " on line 52 of \transfer\com\TransferDecorator.cfc

Comment 14 by Raymond Camden posted on 8/18/2009 at 10:15 PM

That sounds right to me - can you use pastebin and share the code?

Comment 15 by Scott Brady posted on 8/18/2009 at 11:44 PM

Actually, I found the problem. I had an init() function in my BaseDecorator, which was, presumably, overwriting the init() in the TransferDecorator component. It's fixed.

Comment 16 by Raymond Camden posted on 8/18/2009 at 11:45 PM

The init() in your CFC could call the init() in Transfer decorator with the Super scope.

Comment 17 by Scott Brady posted on 8/18/2009 at 11:55 PM

True. But the init's not doing anything anyway (it's kind of our default init() ) :)

Comment 18 by Drew posted on 9/10/2009 at 8:37 PM

I use a generic table structure for my database where every table has
CreationDate
CreatedBy

Is there a way I can globally set a custom method in transfer to default to a specified string for the CreatedBy field? I know I can do this via decorators, but didn't want to write a decorator for each and every table I'm using to achieve this.

Comment 19 by Drew posted on 9/10/2009 at 8:44 PM

Looks like my google skills are slacking today, I found my answer here:
http://tracker.transfer-orm...
The answer being no, you can't do this

Comment 20 by Scott Brady posted on 9/10/2009 at 8:46 PM

What we did (which is something Ray suggested ... somewhere) is we have a BaseDecorator that every decorator extends. The BaseDecorator then has the config options for setting those standard defaults.

Our table definitions use either their own decorator or they just use the BaseDecorator as the decorator (so you don't have to write a new Decorator for each table)