Simple Object Oriented Form Handling
This entry has been updated over at the revised Object Oriented Form handling entry.
Some thoughts on handling forms using object oriented techniques. In particular, how to handle the creation of new data, editing, validating and saving it to a database.
Trying to keep things simple ...
These notes mostly describe the creation of a form 'helper' object, and the process for initialising, validating and saving form data. When I discuss about how the helper is used, I try not to make too many assumptions about how your application might be set up.
Hopefully these ideas should nicely fit into non-oo applications as well as more oo applications.
Let's start with an example form
To make this idea a little more interesting, lets assume that we have a form that captures data that ultimately needs to be saved in a couple of database tables, rather than just a single table.
Suppose we need to capture information about a person, including their home and postal address. So our form may have the following fields
First Name
Last Name
Home Address Line 1
Home Address Line 2
Home Suburb
Home State
Home Postcode
Post Address Line 1
Post Address Line 2
Post Suburb
Post State
Post Postcode
The First Name and Last Name are stored in a Person table
The Home Address and Post Address are stored as two separate records in an Address table.
So when we save a record, we are creating 3 new records for a new person, and updating 3 records when editing a person.
Overview of our Helper object
For each form we can create a Helper object to do a lot of the work for us. The great thing about the helper is that it is aware of the entire form so is able to handle all form wide operations.
The helper knows about every field in the form - including hidden fields, and also knows how to:
a) Read all of the form fields from the database (used during an edit)
b) Collect submitted form fields (used before a save)
c) Validate the entire form (used before a save)
d) Save all of the form data
Suppose our helper in this case is call PersonHelper and has the following functions available
init() - sets all internal form fields to an initial state (eg blank)
readData(personId) - reads the person, home address and post address from the database
collectData(form) - collects data from a submitted form
validate() - returns an array of error messages
save() - saves the person, home address and post address
getData() - returns a Struct containing all of the form field data.
Before we look at how our helper is actually implemented, let's look at how it might be used ...
New and Edit Form actions
For a New action we need to initialise the form with blank values.
For an Edit action we need to initialise the form with data from the database
After this initialisation the rest of the process is identical for both New and Edit:
1. User updates the form fields and clicks 'Save'
2. Form is validated
3. If there are errors
4. Display the form prefilled with data just submitted
5. Go back to step 1
6. Else
7. Save the form
8. End if
So really, the only different with New and Edit is how the form is initialised, the rest of the process is identical.
Initialising a New form
Whenever a new blank form needs to be displayed, we can use the helper to set all of the form fields to their initial state for us.
<cfset helper = createObject("component","PersonHelper").init()>
<!--- This is a normal struct with all the field data --->
<cfset data = helper.getData()>
Then this data is passed on to the form display code.
Initialising an Edit form
If an edit is required, this means that the person Id has been provided allowing us to read all of the form data from the database.
<!--- Create the helper, read from the database and get out the initialised form data --->
<cfset helper = createObject("component","PersonHelper").init()>
<!--- Initialise the helper with data from the database --->
<cfset helper.readData(personId)>
<!--- Get the data out --->
<cfset data = helper.getData()>
Then this data is passed on to the form display code.
Displaying the form
So at this point we have been provided with a Struct called 'data' containing the form data.
<!--- Our form always tries to save the person when submitted. --->
<form action="index.cfm?event=savePerson>
<!--- Hidden Id field. For a new forms, this will be an empty string.
For edit forms this will be the persons id. --->
<input type="hidden" name="personId" value="#data.personId#">
First Name <input name="firstName" value="#data.firstName#">
Last Name <input name="lastName" value="#data.lastName#"><br>
Home Address Line 1 <input name="homeAddressLine1" value="#data.homeAddressLine1#"><br>
Post Address Line 1 <input name="homeAddressLine1" value="#data.homeAddressLine1#"><br>
<!--- And so on for the rest of the fields --->
<input type="submit" value="Save">
</form>
Validating and saving the data
When the form is submitted we can again use our helper to assist.
Here's what we do:
Validate the form
If there are errors then
Show the form again with error messages
Else
Save the data
Go to the next page after the data is successfully saved.
End if
And the code ...
<!--- Pass in the form scope for the helper to extract the fields it needs --->
<cfset helper.collectData(form)>
<!--- Any errors? --->
<cfset errors = helper.validate()>
<cfif arrayLen(errors) gt 0>
<!--- If there are errors then include them in the data and show the form again --->
<cfset data.errors = errors>
<--- Do something to show the form again --->
<cfelse>
<!--- If all OK then just save what we have right now --->
<cfset helper.save()>
<!--- Data was saved, so now continue to the next page of the application --->
</cfif>
Now we need a little extra code above our form to handle the error display:
put in the 'data' structure as an array. --->
<cfset formErrors = "">
<cfif structKeyExists(data,"errors")>
<cfset formErrors = data.errors>
</cfif>
<cfif isArray(formErrors)>
<cfloop index="i" from="1" to="#arrayLen(formErrors)#">
<cfoutput>#formErrors[i]#<br></cfoutput>
</cfloop>
</cfif>
Building our PersonHelper.cfc
Now let's look at how we might build our helper.
Our PersonHelper needs to access the Person and Address tables, so we need data access objects (DAOs) for each of these. Let's assume that these are provided when the helper is created.
Firstly we need to declare all of the fields required in a data structure.
<!--- Create a struct to hold all of our form fields. --->
<cfset variables.data = structNew()>
<!--- Initialise our form field values. --->
<cfset variables.data.personId = 0>
<cfset variables.data.firstName = "">
<cfset variables.data.lastName = "">
<cfset variables.data.homeAddress1 = "">
<cfset variables.data.homeAddress2 = "">
<cfset variables.data.homeSuburb = "">
<cfset variables.data.homeState = "">
<cfset variables.data.homePostcode = "">
<cfset variables.data.postAddress1 = "">
<cfset variables.data.postAddress2 = "">
<cfset variables.data.postSuburb = "">
<cfset variables.data.postState = "">
<cfset variables.data.postPostcode = "">
<!--- Other functions here ... --->
</cfcomponent>
PersonHelper.Init
The init() function is passed in any objects is needs to do its work. In our case it needs a couple of data access objects.
<cfargument name="personDAO">
<cfargument name="addressDAO">
<cfset variables.personDAO = arguments.personDAO>
<cfset variables.addressDAO = arguments.personDAO>
<cfreturn this>
</cffunction>
PersonHelper.ReadData
The readData() function is passed parameters that allows if to read data from the database and initialse all of the form variables. In this example, it simple needs the personId. It uses the DAOs provided by init() to do it's work.
<cfargument name="personId">
<cfset var person = variables.personDAO.read(arguments.personId)>
<cfset var homeAddress = variables.addressDAO.read(person.getHomeAddessId())>
<cfset var postAddress = variables.addressDAO.read(person.getPostalAddessId())>
<cfset variables.data.firstName = person.getFirstName()>
<cfset variables.data.lastName = person.getLastName()>
<cfset variables.data.homeAddress1 = homeAddress.getAddressLine1()>
<cfset variables.data.homeAddress2 = homeAddress.getAddressLine2()>
<!--- And so on for the rest of the fields --->
</cffunction>
Just as an aside, it might be nice to have the addresses available directly from within the person object:
<cfset var homeAddress = person.getHomeAddess()>
<cfset var postAddress = person.getPostalAddess()>
PersonHelper.CollectData
The collectData() function is very similar to the readData() function in that it sets all of the form fields, but this time from the passed in structure (which is typically the form scope).
<cffunction name="get" output="false">
<cfargument name="data">
<cfargument name="key">
<cfif structKeyExists(arguments.data,arguments.key)>
<cfreturn structFind(arguments.data,arguments.key)>
</cfif>
<cfreturn "">
</cffunction>
<cffunction name="collectData" output="false">
<cfargument name="form">
<cfset variables.data.firstName = get(arguments.form,"firstName")>
<cfset variables.data.lastName = get(arguments.form,"lastName")>
<cfset variables.data.homeAddress1 = get(arguments.form,"homeAddressLine1")>
<cfset variables.data.homeAddress2 = get(arguments.form," homeAddressLine2")>
<!--- And so on for the rest of the fields --->
</cffunction>
PersonHelper.Validate
The validate() function does whatever is required to ensure the data is ready to be saved. This might be to ensure date fields contain real dates, and that mandatory fields are not blank, etc.
If any errors are found it needs to build up a collection of errors. In this example this will be an array of simple error messages, but it might be nicer to have a collection of error objects that provide error codes, field codes and messages.
Also, if you have validation code already implemented in your business objects, then you could create and use them here.
<cfset var errors = arrayNew()>
<cfif len(trim(variables.data.firstName)) eq 0>
<cfset arrayAppend(errors,"The first name field may not be blank.")>
</cfif>
<cfif len(trim(variables.data.lastName)) eq 0>
<cfset arrayAppend(errors,"The last name field may not be blank.")>
</cfif>
<!--- More validations here --->
<cfreturn errors>
</cffunction>
PersonHelper.Save
The save() function knows how to save all of the form data, using whatever DAOs have been provided to the helper.
In this example, it creates the business objects required then saves them.
<cfset var person = "">
<cfset var homeAddress = "">
<cfset var postAddress = "">
<!--- If the personId is available, then get the corresponding address ids --->
<cfif variables.data.personId gt 0>
<cfset person = variables.personDAO.read(variables.data.personId)>
<cfset homeAddress = variables.addressDAO.read(person.getHomeAddessId())>
<cfset postAddress = variables.addressDAO.read(person.getPostalAddessId())>
<cfelse>
<!--- Create empty business objects --->
<cfset person = createObject("component","Person").init()>
<cfset homeAddress = createObject("component","Address").init()>
<cfset postAddress = createObject("component","Address").init()>
</cfif>
<!--- Populate/overwrite the fields in all of the objects --->
<cfset person.setFirstName(variables.data.firstName)>
<cfset person.setLastName(variables.data.lastName)>
<cfset homeAddress.setAddressLine1(variables.data.homeAddress1)>
<cfset homeAddress.setAddressLine2(variables.data.homeAddress2)>
<!--- An so on for the other fields --->
<!--- Then save the records using our data access objects. --->
<cftransaction>
<cfset variables.personDAO.save(person)>
<cfset variables.addressDAO.save(homeAddress)>
<cfset variables.addressDAO.save(postAddress)>
</cftransaction>
</cffunction>
PersonHelper.GetData
The getData() simply returns a struct of the data currently in the helper.
This is used when we want to get the data out of the helper to fill in the actual form.
<cfreturn variables.data>
</cffunction>
And we're done
I found this strategy particularly useful when dealing with forms containing many fields. It keeps the large number of fields away from the controlling code that manages the form display as well as the validation and save. It is also useful to have one object that understands the form as a whole and hides away the actual tables affected by the form.
All feedback appreciated!
Let me know what you think. Good, average, doesn't make sense. All feedback appreciated - discussions are the best way to learn.
