Monday, November 17, 2008

Implementing Transfer Observers across all your decorators, the easy way!

In our framework, we use Transfer "Decorators" as a common way to extend the functionality of the generated objects that are based on the configuration XML.

For this system, we have a record (media) that represents each user upload of images. For example, we allow multiple profile images to be uploaded, and you can select your current profile image from the "gallery" of your previous uploads. In addition, however, you can delete these images. If you're familiar with transfer, you know it's as easy to delete the database record as saying oMedia.delete(). That removes the object from the database and from Transfer's cache. In this case, however, we also need to remove the actual files from the webserver.

One possible solution was to create a manager service, and make sure that all media deletions go through it. However, this removes some of the intuitive nature of Transfer providing a delete() function for you to use. With Transfer Observers (or "Transfer Event Model"), you can easily extend your decorators to do this on the normal delete() call.

After my initial creation of a mediaObserver.cfc, which I register with Transfer as an observer, I realized I was going down the wrong path. The problem with this is that all observers registered with Transfer get called on each of these seven events:

observerAfterNew,
observerBeforeCreate,
observerAfterCreate,
observerBeforeUpdate,
observerAfterUpdate,
observerBeforeDelete,
observerAfterDelete


The problem with that? Naming my file MediaObserver has nothing to do with my Media records. The events in the observer get called for any object that is getting ready to be created, updated, deleted, etc. So the first thing I did was to rename the file as TransferObserver.cfc. This is a more generic name, obviously, and reflects more accurately what the file is.

So...the question remains at this point: How do I get it to fire an event specifically for my Media records being deleted? Each of the functions you place in the observer receive a single argument when called: transfer.com.events.TransferEvent

From that event, you can do:

arguments.event.getTransferObject()


This gives you the actual Transfer object being modified for this event. That still doesn't tell us this is the Media object, though. But we can do this:

arguments.event.getTransferObject().getClassName()


That returns, in my case, "media.Media". Now I know that this is the object I want rather than, say, an invoice, product or customer object. So I could now write:

<cfif arguments.event.getTransferObject().getClassName() IS "media.Media">
  <cfset arguments.event.getTransferObject().deleteMyFiles()/>
</cfif>


After writing this, though, it occurred to me that every time I wanted to have a decorator respond to a Transfer event, I would have to modify my TransferObserver.cfc to respond to a new class name. It seemed that I could perhaps do this in such a way that I could just add a function, with a specific naming convention, to each decorator, as needed, and it would just work. And you can. Here is the format now for every function I added to the TransferObserver.

<cffunction name="actionBeforeDeleteTransferEvent" returntype="void" access="public" output="false" hint="I am called BEFORE a Transfer object has been deleted from the database.">
  <cfargument name="event" type="transfer.com.events.TransferEvent" hint="The event object" required="true" />
  <cfset var stLocal = structNew() />
  <cfif structKeyExists(arguments.event.getTransferObject(), "observerBeforeDelete")>
    <cfset arguments.event.getTransferObject().observerBeforeDelete() />
  </cfif>
</cffunction>


What is this? The Transfer observer looks to see if there is a function named "observerBeforeDelete" in the Transfer object (or the decorated Transfer object, since this function naming convention is not part of Transfer's generated file format). If the function exists, it calls it. It's that simple, really.

If you'd like to download the entire TransferObserver file, along with a portion of the Coldspring file showing how to set it up. I've linked it here on my site as a tiny zip file (1.2K). Enjoy!

Just a note: Performing a structKeyExists() on a Coldfusion component, as in structKeyExists(object, "functionName") is not a guaranteed way of ensuring that "functionName" is indeed a function. If you have certain property types (like object.propertyName = true), then that "propertyName" is also a structure key. The correct way is to use the getMetaData() function, then you can ascertain for sure that what you are looking for is actually a function. However, the getMetaData() is a slow performing method and would decrease performance significantly to implement. The structKeyExists() should work just find, because you probably won't have a property of the object using this naming convention. And you can actually use a different decorator naming convention for these event-related functions, too. The cffunction names in the TransferObserver.cfc must, however, remain as in my example.

- Will Belden
November 17, 2008

1 comment:

aguli said...

Veryy nice post