Object Oriented Form Handling

This entry discusses a strategy for handling web forms using object oriented techniques, in particular; how to handle the creation of new data, editing existing data, validating the fields and saving it to a database.

An example form

Let’s 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
Date of Birth

Street Address Line 1
Street Address Line 2
Street Suburb
Street State
Street Postcode

Postal Address Line 1
Postal Address Line 2
Postal Suburb
Postal State
Postal Postcode

PersonId (this is a hidden field)

The First Name, Last Name and Date of Birth are stored in a Person table

The Street Address and Postal Address are stored as two separate records in an Address table.

So this is all stored in the database as three separate data records

Using a Form Helper

For each form we can create a Form Helper object to do a lot of the work for us. The great thing about the helper is that it knows about every field in the form, including hidden fields, so is able to handle all form wide operations.

In particular it knows:

  1. About all form fields, including hidden fields.
  2. How to read data from the database and assign this data to the form fields – used during an edit.
  3. How to validate the entire form – used before saving.
  4. Save all of the form data.

However, the Helper does not know anything about how the form is presented to the user. It does not do any work related to displaying the form – it is only aware of the raw data that the form uses.

In this example, our helper will be called PersonFormHelper.

Before we look at how our helper is implemented, let’s look at how it might be used.

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.

We only need to init() the object to get it to its initial state:

init() Defines all of the form fields and sets their values to an initial state.
<!--- Create the helper and set the form data to its initial state. --->
<cfset helper = createObject("component","PersonFormHelper").init()>

Initialising an edit form

If an edit is required, this means that a personId has been provided allowing us to read all of the necessary form data from the database.

Functions used here:

load(key=value) Reads data from the database and sets the corresponding form fields.

When you call load() you must pass in the parameters in the form key=value. You can pass in as many parameters as required.

<!--- Create the helper and set the form data to its initial state. --->
<cfset helper = createObject("component","PersonFormHelper").init()>
 
<!--- Load the data from the database. This function converts the database data into appropriate form field data. --->
<cfset helper.load(personId=#personId#)>

Displaying the form

To display the form we need to get the current form field data out of the helper. There are two functions that we can use here:

Functions we may use here:

getFields() Returns a struct of all current form field data.
getField(fieldName) Returns the value of the specified field.
<!--- Get the data out of the helper into a simple struct. --->
<cfset data = helper.getFields()>
 
<!--- 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 person's 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>
Date of Birth <input name="dateOfBirth" value="#data.dateOfBirth#"><br>
Street Address Line 1 <input name="streetAddressLine1" value="#data.streetAddressLine1#"><br>
Street Address Kine 2 <input name="streetAddressLine2" value="#data.streetAddressLine1#"><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

Functions that may be used here:

setFields(struct) Sets all of the fields to the data provided in the struct parameter.
setField(fieldName,value) Sets the specified field to the provided value.
validate() Validates the current form fields and sets any errors.
hasErrors() Indicates if any errors have been set.
getErrors() Returns a struct of any set errors.
clearErrors() Clears any errors that have been set.
save() Saves the form data.

And the code …

<!--- Create our helper. --->
<cfset helper = createObject("component","PersonFormHelper").init()>
 
<!--- Set the form fields from the form scope. --->
<cfset helper.setFields(form)>
 
<!--- Validate the form data. --->
<cfset helper.validate()>
 
<!--- If no errors then save the form data. --->
<cfif not helper.hasErrors()>
    <cfset helper.save()>
    <!--- Now redirect to the new location. --->
    <!--- Get the person Id for redirection if required. --->
    <cfset personId = helper.getField("personId")>
    <!--- ... Redirect here ... --->
</cfif>
 
<!--- Display the form here. --->

Displaying form errors

The errors struct can contain anything you need, but in this example it is a struct containing a field name and field error message. A trivial display would just display these above the form:

<cfif helper.hasErrors()>
    <cfset errors = helper.getErrors()>
    <cfloop item="errorField" collection="#errors#">
        <cfoutput>#errors[errorField]#<br></cfoutput>
    </cfloop>
</cfif>

Building the Form Helper

We may use form helpers for multiple forms within an application, so we should place common functionality into one component.

Let’s start by creating a base FormHelper.cfc with generic functions, then we will extend this with a specific helper for each form – in our case a PersonFormHelper.cfc.

FormHelper.cfc

Our generic Form Helper will have the following functions. You can download an implementation of this below.

Field Functions

setField(fieldName,value) Sets a single form field’s value.
getField(fieldName) Returns a form field’s value.
setFields(struct) Sets a struct of form field names and values.
getFields() Returns a struct of all form fields names and values.
getFieldNameList() Returns a list of current form field names.

Error Functions

Functions for handling any errors, such as form validation errors.

setError(fieldName,message) Sets an error for a particular form field.
getErrors() Returns a struct of errors.
hasErrors() Returns true if any errors have been set.
clearErrors() Removes any current errors.

Helper Functions

These are the main functions that should exist in every form helper.

The default implementation of these does nothing and the actual functionality will be provided by the custom helper code you write when you extend this generic helper component.

initialise(argumentCollection) Defines and sets all of the initial form field values. This function is called from the init() function in FormHelper.cfc.
load(argumentCollection) Loads data from the database and sets all of the form field values accordingly.
validate() Validates all fields within the form and sets errors accordingly.
save() Saves the form data.

PersonFormHelper.cfc

The PersonFormHelper is the customised extension to the FormHelper.

We need to implement the four helper functions in this component:
- initialise()
- load()
- validate()
- save()

Initialise() function

The initialise function is the place to define ALL fields that exist within the form, including hidden fields.

This should call setField(fieldName,initialValue) for each field in the form, of if the field is initialised to an empty string we can simply call setField(fieldName) (the default initial value is an empty string).

For our example person form above we have the following code.

<cffunction name="initialise" output="false">
    <cfset setField("personId")>
    <cfset setField("firstName")> 
    <cfset setField("lastName")> 
    <cfset setField("dateOfBirth")> 
    <cfset setField("streetAddressLine1")> 
    <cfset setField("streetAddressLine2")> 
    <cfset setField("streetSuburb")> 
    <cfset setField("streetState")> 
    <cfset setField("streetPostcode")> 
    <cfset setField("postalAddressLine1")> 
    <cfset setField("postalAddressLine2")> 
    <cfset setField("postalSuburb")> 
    <cfset setField("postalState")> 
    <cfset setField("postalPostcode")> 
</cffunction>

Notice that we include the personId field here. This is a hidden field on the form.

Load() function

The load() function loads data from the database and populates the form fields, formatted as required for the form. For example, date fields need to be converted to the format required for display.

In this example, I am assuming that the PersonFormHelper has been provided with a PersonService and an AddressService that provides access to reading the relevant records from the database.

<cffunction name="load" output="false">
    <cfargument name="personId">
 
    <!---
    Load all of the objects we need.
    --->
 
    <cfset var person = personService.getPerson(arguments.personId)>
 
    <cfset var streetAddress = 
                    personService.getStreetAddress(arguments.personId)>
 
    <cfset var postalAddress =
                    personService.getPostalAddress(arguments.personId)>
 
    <cfset var dob = "">
 
    <!---
    Format the date of birth as required for the form.
    --->
 
    <cfif len(person.getDateOfBirth()) gt 0>
        <cfset dob = dateFormat(person.getDateOfBirth(),"d/m/yyyy")>
    </cfif>
 
    <!---
    Set the form fields.
    --->
 
    <cfset setField("personId",person.getPersonId())>
 
    <cfset setField("firstName",person.getFirstName())> 
    <cfset setField("lastName",person.getLastName())> 
    <cfset setField("dateOfBirth",dob)> 
 
    <cfset setField("streetAddressLine1",streetAddress.getAddressLine1())> 
    <cfset setField("streetAddressLine2",streetAddress.getAddressLine2())> 
    <cfset setField("streetSuburb",streetAddress.getSuburb())> 
    <cfset setField("streetState",streetAddress.getState())> 
    <cfset setField("streetPostcode",streetAddress.getPostcode())> 
 
    <cfset setField("postalAddressLine1",postalAddress.getAddressLine1())> 
    <cfset setField("postalAddressLine2",postalAddress.getAddressLine2())> 
    <cfset setField("postalSuburb",postalAddress.getSuburb())> 
    <cfset setField("postalState",postalAddress.getState())> 
    <cfset setField("postalPostcode",postalAddress.getPostcode())> 
 
</cffunction>

Validate() function

Validate needs to check the form for any errors. This is a good place to put code to check the validity of fields that are dependant on other fields within the same form.

In this function use getField() to get access to the form field data.

Whenever an error is found, call setError() to flag the error.

To keep this example small we will just check that First Name and Last Name is provided and that the Date of Birth is a valid date.

<cffunction name="validate" output="false">
 
    <cfset var firstName = trim(getField("firstName"))>
    <cfset var lastName = trim(getField("firstName"))>
    <cfset var dob = trim(getField("dateOfBirth"))>
 
    <cfif len(firstName) eq 0>
        <cfset setError("First Name","Please enter your first name.")>
    </cfif>
 
    <cfif len(lastName) eq 0>
        <cfset setError("Last Name","Please enter your last name.")>
    </cfif>	
 
    <cfif len(dob) gt 0 and not lsIsDate(dob)>
        <cfset setError(
                "Date of Birth",
                "Please check the Date of Birth is a valid date."
            )>
    </cfif>
 
</cffunction>

Save() function

The save() function saves all of the form fields to the database.

This example uses Service components to do the work, but this can be done is whatever way suits your application.

<!---
Convert the form fields into data ready for saving,
then save to the database.
--->
<cffunction name="save" output="false">
 
    <!---
    Create an empty person object.
    --->
    <cfset var person = variables.personService.createPerson()>
 
    <!--- 
    Create empty address objects. 
    --->
    <cfset var streetAddress = variables.addressService.createAddress()>
    <cfset var postalAddress = variables.addressService.createAddress()>
 
    <!---
    Populate the person object.
    --->
    <cfset person.setPersonId( getField("personId") )>
    <cfset person.setFirstName( getField("firstName") )>
    <cfset person.setLastName( getField("lastName") )>
    <cfset person.setDateOfBirth( getField("dob") )>
 
    <!--- 
    Populate the street address. 
    --->
    <cfset streetAddress.setAddressLine1( getField("streetAddressLine1") )>
    <cfset streetAddress.setAddressLine2( getField("streetAddressLine2") )>
    <cfset streetAddress.setSuburb( getField("streetSuburb") )>
    <cfset streetAddress.setState( getField("streetState") )>
    <cfset streetAddress.setPostcode( getField("streetPostcode") )>
 
    <!---
    Populate the postal address. 
    --->
    <cfset postalAddress.setAddressLine1( getField("postalAddressLine1") )>
    <cfset postalAddress.setAddressLine2( getField("postalAddressLine2") )>
    <cfset postalAddress.setSuburb( getField("postalSuburb") )>
    <cfset postalAddress.setState( getField("postalState") )>
    <cfset postalAddress.setPostcode( getField("postalPostcode") )>
 
    <!--- 
    Save the objects. 
    --->
    <cftransaction>
 
        <cfset variables.personService.savePerson(person)>
 
        <cfset variables.personService.savePersonStreetAddress(
                    person.getPersonId(),
                    streetAddress
                    )>
 
        <cfset variables.personService.savePersonPostalAddress(
                    person.getPersonId(),
                    postalAddress
                    )>
 
    </cftransaction>
 
    <!---
    IMPORTANT: If this was a save of a new person, then the personId will
    now be present in the person object. So get this Id out and store
    it in the helper. The code that uses this helper can then get access
    to this id if required (such as in a redirect).
    --->
    <cfset setField("personId",person.getPersonId())>
 
</cffunction>

Form field and database field mismatch

What happens when the form fields don’t match the database fields? It is up to your Form Helper code to transform the data from form data into database data and visa-versa.

A good example of this is date time fields. You may capture a date and time data in your form in a number of ways, but your form helper needs to convert them to a single date/time field when saving, and must do the reverse when loading from the database.

Form helpers are NOT singletons

It is important to remember that Form Helpers are not singletons. This simply means that you must create a new helper object every time you need to process a form.

This is particularly important if you are using a something like ColdSpring or LightWire to create your objects. You must indicate that they are NOT singletons in their definitions.

Get the code…

FormHelper.cfc

PersonFormHelper.cfc

This entry was posted in OOP and tagged , . Bookmark the permalink. Both comments and trackbacks are currently closed.