Form Handling and Validation with ColdBox, ColdSpring, and Transfer (Part 3)

This post is a continuation of another post where I am discussing form handling and validation.

Please see part 2 before you read this post.

Now, and this is where things get interesting, I call the validate() method from the TransferObject "itemBean". What you say? validate() method? There is not validate() method in a TransferObject.

I used a Transfer decorator to add a validate() method to my itemBean. I also added a populate() method. Here, have a look:


<cfcomponent output="false" extends="transfer.com.TransferDecorator">
    <!--- This struct is used to give the form fields common names for placing into the error messages --->
    <cfset variables.sFormFields = {
            itemid = "Item ID",
            itemName = "Item Name",
            startDate = "Start Date",
            endDate = "End Date",
            descr = "Description",
            link="link"
            }
    /
>

        
    <cffunction name="validate" access="public" returntype="void" output="false">
        <cfargument name="formData" type="struct" required="true" hint="" />
        <cfargument name="result" type="model.common.result" required="true" hint="Used to store errors" />
        
        <cfset var data = arguments.formData />
        
        <!--- Check to make sure item name has a value --->
        <cfif NOT StructKeyExists(data, "itemname") OR NOT Len(data.itemName)>
            <cfset result.setError("You must enter an Item Name") />
        </cfif>
        
        <!--- Check to see if Data is populated --->
        <cfif NOT StructKeyExists(data, "startdate") OR NOT len(data.startDate)>
            <cfset result.setError("You must enter a Start Date") />
        </cfif>
        
        <!--- Check to make sure that the end date is after the start date --->
        <cfif StructKeyExists(data, "startdate")
                AND StructKeyExists(data, "endDate")
                AND IsDate(data.startDate)
                AND IsDate(data.endDate)
                AND DateDiff('d', data.startDate, data.endDate) LT 0
        >

            <cfset result.setError("Start Date must be before End Date") />
        </cfif>
        
        <cfset populate(arguments.formData, arguments.result) />
    </cffunction>    
        
    <cffunction name="populate" access="public" returntype="void" output="false" hint="Decorator function will return an struct of error or success messages">
        <cfargument name="formData" type="struct" required="true" hint="" />
        <cfargument name="result" type="model.common.result" required="true" hint="Used to store errors" />
        
        <!--- Get a memento struct for getting the current properties of the object --->
        <cfset currentState = getMemento() />
    
        <!--- Loop over the collection that was passed in for calling set() methods for each--->
        <cfloop collection="#formData#" item="data">
            <!--- Check the memento to see if the property exists in the object, if it does, try calling the set method --->
            <cfif StructKeyExists(currentState, data)>
                <cftry>
                    <!--- Call the set method for the property --->
                    <cfset Evaluate("set#data#('#StructFind(formData,data)#')") />
                
                    <cfcatch>
                        <!--- If the set method fails, find out why --->                
                        <cfswitch expression="#cfcatch.type#">
                            <!--- Datatype Error (Date) --->
                            <cfcase value="date">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be a date")>
                            </cfcase>
                            
                            <!--- Datatype Error (Number) --->
                            <cfcase value="numeric">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be a number")>
                            </cfcase>
                            
                            <!--- Datatype Error (UUID) --->
                            <cfcase value="uuid">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be a UUID")>
                            </cfcase>
                            
                            <!--- Datatype Error (Boolean) --->
                            <cfcase value="boolean">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be True or False")>
                            </cfcase>
                            
                            <!--- Datatype Error (Binary) --->
                            <cfcase value="binary">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be a Binary File")>
                            </cfcase>
                            
                            <!--- Datatype Error (guid) --->
                            <cfcase value="guid">
                                <cfset result.setError("#StructFind(variables.sFormFields, data)# must be a GUID")>
                            </cfcase>
                            
                            <!--- In any other case, somethign is very wrong --->
                            <cfdefaultcase>
                                <cfset result.setError("There was a problem with the record, please try again")>
                            </cfdefaultcase>
                        </cfswitch>
                    </cfcatch>
                </cftry>
            </cfif>
        </cfloop>
    
    </cffunction>
    
</cfcomponent>

Now, I am not going to explain every piece of this. Please take a look and tell me what you think. Essentially, the validate method is called and is passed the form data and the result object(for storing errors). Validate() goes though MY business logic. In this case it checks to make sure that there is an item name, and that the startdate is before the enddate, etc. If there are any errors, they are added to the result object using result.setError().

Next, the validate() method passes the formdata and result object on to the populate() method. The populate() method first uses the getMemento() function to get a struct of all of the bean/TrO's properties. It needs this to loop through the formData to look for values to set.

It then loops through the struct and tries to call set() on any values that match. So if it finds a form field called "eventName" and there is a property in the bean/TrO called "eventName" it will call setEventName(value) and pass in the value of eventName that was in the form data.

If the set() fails, the cfcatch will catch the error and look for the cause in cfcatch.type. If it finds a match, it sets an appropriate error message into the result object. If not, it sets a generic error message.

You'll notice that I created a struct in my pseudo-constructor that contains all of the form field names for my form. Each form field is assigned a "display name" that I choose. This is so that when I am setting my error messages, I can use user-friendly names instead of form field names. I could not think of a better way to do this.

So, after validate() and populate() are executed, we go back to the event handler items.editPost().

The event handler checks the result object to see if any errors were returned:


<!--- If the item has errors, add them to the message box and return the user to the form --->
        <cfif result.hasErrors()>
            <cfset getPlugin("messageBox").setMessage("error","", result.getErrors()) />
            <cfset Event.setLayout('Layout.Admin')>
            <cfset Event.setView("itemForm") />
        <cfelse>
            <!--- If there are no errors, persist the bean and return the user to the item list --->
            <cfset itemService.save(itemBean) />
            <cfset setNextEvent("itemList") />
        </cfif>

If any errors were returned, then the messagebox plugin is called and the error array is set to it and the form is redisplayed.

If there were no error messages, then the service layer's save() method is called, the object is persisted to the database and the user is rerouted to the item list to see their newly added item.

Wow, this is, my longest post ever. I hope you had the patience to make it this far. If you did, thank you.

I'd love to hear opinions on this. Like I said, this is an experiment, it probably has gaping holes in it that I am not seeing.

Comments
Brian Kotek's Gravatar Jason, this is looking pretty good! There are a few things that I would do a bit differently but overall I think this is definitely going in the right direction.

I personally have abstracted the validation out into their own set of objects. I define (actually generate) XML that defines the validation rules for specific business objects. So that covers things like required, numeric, etc. I generate this from database metadata and then can go in and add more rules to the XML if I need to. So when I call validate() on my business object, it actually calls validatorFactory.getValidator(objectType).validate(this, result). I get back a populated result object with errors if there were failures. This way, the validation logic is kept out of my actual BO, and since most of it is generic it is reusable across all BOs.

Second, I let the service and the BO do the work, so that my Controller stays really dumb. Essentially the controller just does result = service.save(data). The creation of the result, the population of the BO, and the validation all happens within the service. So in the service it does something like:

result = resultFactory.getInstance();
bo = gateway.getBO(objectType);
bo.populate(result);
bo.save(result);
return result;

This way when I populate the BO I can catch errors related to invalid types (since Transfer types all of its arguments). Then when I call save it internally calls validate() automatically and populates the result object with errors if there are any. If things are good, it saves it and sets isSuccess(true) on the result. The result gets returned and then the controller either redisplays the form or redirects to the success page.

Anyway I realize that what I'm describing is a bit more complex than what you have shown here, but I've found it to work pretty well in most situations. Again, depending on how complex or reusable you need this to be, what you've shown here looks very good. The minor changes that I've described aren't altering anything fundamental, just moving some things around and trying to have as much of this as possible happen "automatically" instead of requiring explicit calls in the Controller to trigger.

Great post!
# Posted By Brian Kotek | 8/14/08 8:24 AM
Jason Dean's Gravatar @Brian, Thanks for taking the time. I love your comment and ideas. I will definitely look at trying to implement something like that going forward.

Moving things into the service layer makes great sense, and it is something that I overlook fairly often. It is also an easy change that I can make right away.

the validatorFactory is, like you said, more complex; but I am going to look at this more closely. I have tended to "nervous" about object factories, but I think I am making them more complicated (in my head) than they really are.

Again, thanks for taking the time.
# Posted By Jason Dean | 8/14/08 8:58 AM
ike's Gravatar I'm surprised that Transfer doesn't have a built-in feature for doing that little dance between the memento and your input struct from the user... the DataFaucet ORM does. So all that extra, would have just been handled for you, you wouldn't have had to code any of it.

I obviously take a different approach than Brian... my validators are separate, but they're part of the form, which is actually kind of similar to his validator factory in a way... it's just more generic, not requiring a separate object for each business-object class. And then when I go to validate the form, it's just <cf_validate form="#myform#"><cfset item.update(data) /></cf_validate> ... This way since my validation isn't hidden inside the update(), I can put other things inside the cf_validate tag, like often I want to relocate the user after a successful save, so I'll call the method to do that or I may need to wire up some additional configuration for the object I just saved, which may involve saving other objects or calling webservices or whatever. All things I don't necessarily want inside my business object. I'm not trying to dis Brian or anything, just offering an alternative suggestion. :)

I personally like marrying the validation to the form for most things because with the exception of a webservice, I don't like doing extra validation (or having the system do extra validation) during operations that occur between components (i.e. no user involved). Imo if the system is performing some activity that doesn't involve a user like a nightly import, etc. then it shouldn't call any kind of validation when it performs that save/update because you've already tested all that code and the data should be trusted, so the extra work of validating in the absence of a user is a drain. Wrap a try-catch around it and log any errors for later correction.

Anyway I hope my comments here made sense. It's late and I think I need sleep. :)
# Posted By ike | 8/15/08 10:50 PM
Bob Silverberg's Gravatar Great post Jason. This is very similar to what I'm doing. I like your approach of calling the object's setters and then catching and trapping the errors - I'm testing for validity first, and then calling the setters only if the data is valid, but I think your method is better. One advantage is that it allows a custom setter to be called, which may in itself do some cleansing of the data which could affect the data's validity. So your approach is more flexible. I also like your translation of fieldnames to user-friendly labels. That is something else that my current method is missing.

In terms of improvements, I agree with most of what Brian had to say (who doesn't? ;-), it looks like there are definitely opportunities for you to move some of your business object specific code into more abstract objects. If you don't want to go the whole XML route (I haven't yet) you may want to think about simply defining your business rules in your concrete decorator (in this case the Item decorator), and then creating a generic validate() method in an abstract decorator which contains the implementation of the tests for those rules. Your populate method looks like it's already generic enough to be moved into an abstract decorator.

I also notice that you've used getMemento() again, which is a bit of a no-no. I use TransferMetadata to accomplish the same thing, with the added benefit of dealing with manytoone and parentonetomany composed objects.
# Posted By Bob Silverberg | 8/19/08 10:17 PM
Jason Dean's Gravatar @Bob - Wow. Thank you for the great comments.

1. The idea of calling the setters, even if the data is not otherwise valid, was Brian's (of course). That way, we are not potentially giving the end user two rounds of error messages on the same field.

2. I definitely like the idea of an abstract validator object. I don't know the best way to go about it (XML or otherwise) yet. But I am definitely going to be looking further into it.

3. Yeah, Mark Mandel pointed out the faux pas of using get/setMemento(). I am going to look at other solutions, starting with your suggestions.

Thanks again
# Posted By Jason Dean | 8/19/08 10:48 PM
Jason Dean's Gravatar @Bob - Oh, and thanks for the idea of moving populate() into an abstract decorator. I had kind of thought I could do something like that, I will have to add a little more for error handling, but I think it could work. It will be one of my next projects (and posts), I think.

I also need to set aside the 3-4 hours to get through your Transfer series ;) No doubt I am duplicating efforts.
# Posted By Jason Dean | 8/19/08 10:52 PM
Paul Ling's Gravatar Thanks Jason for an excellent series!

I've abstracted the populate method to a base class decorator and is working nicely! Just to improve upon your populate() method as I worked with it... here are a couple ways I think would improve it.
1) loop through the currentState instead of the formData so we are only interested in the properties of the current transfer object we are trying to populate.
2) I change the dynamic Evaluate statement to use cfinvoke instead because the when the form fields has characters such as a single quote it will break the Evaluate statement when it would be appropriate to have. For example a restaurant name can be: "Joe's Diner". Here is how I ended up with the code:
<cfinvoke component="#this#" method="set#data#">
<cfinvokeargument name="#data#" value="#StructFind(arguments.formData,data)#" />
</cfinvoke>
3) An interesting note problem when switching to this method is that the getMemento() seems to be using the java Hashmap and causes the structure keys to be case sensitive when using the StructKeyExists because CF upper cases all form fields. So what I did was to convert the "data" to uppercase immediately after the cfloop statement like this: <cfset data = UCase(data) />

Let me know what you think and if you have a better way to address the problem with special characters that break the Evaluate statement. BTW, does anyone know if the getMemento will be here to stay? It is working for me with the latest version Transfer 1.1.

Thanks again for the great posts as I've enjoyed reading and learning from them!
# Posted By Paul Ling | 1/16/09 2:04 AM
Jason Dean's Gravatar @Paul, Thanks for the comment. I like your ideas. Looping over the currentState definitely makes more sense. Also, I had not thought of the Evaluate() issue. Thanks for pointing that out.

Now for the issue with getMemento(). As far as I know, getMemento() is still undocumented and its use is discouraged. Its implementation is subject to change (as it did between Transfer 1.0 and Transfer 1.1) and can cause your logic to break (as it did for my application when I upgraded from 1.0 to 1.1).

Instead, you should consider looking at Bob Silverberg's post on 'Using Transfer Metadata to Create a Memento' @ http://tinyurl.com/62h8du
# Posted By Jason Dean | 1/16/09 9:25 AM
Paul Ling's Gravatar Thanks Jason for the hint to Bob's post! I've implemented it in the decorator's abstract class and works perfectly!
# Posted By Paul Ling | 1/17/09 1:15 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner