This post is more of a note-to-self than anything. Part one of a two-parter, concentrating on web form processing – first from a server-side point of view, and then later, from a user-centric client-side point of view. The aim is to document a pattern for forms processing that is consistent and repeatable for any website. Think PRG (Post-Redirect-Get), with some server-side detail added in for good measure. PRG+.
In this post, I just want to focus on server-side data validation, and how to deal with the different types of invalid data entry. The first point to note is that you should never make any assumption about the origin of the data. You must ignore any notion of client-side validation – the data that your controller receives as the request could have come from anywhere, and may contain anything. Data is sent over the wire as text key-value pairs, and should therefore go through a number of validation steps:
- Checking the data type – everything arrives as a string over the wire, so needs checking
- Simple data type logic – is an email in a valid format, is a date of birth set in the past
- Domain logic – is an email / username a duplicate, does a product exist, is the price ‘right’?
Only at this point can you attempt to process the form with any confidence.
When an rule fails, there are a couple of different ways in which the user can be notified:
- Return to the form, and allow them to amend the invalid data
- Proceed to a new page, with appropriate messaging (e.g. PRG)
I will go into more detail around this side of the process in the next post (when to redirect, how to notify users etc.), however one thing I would highlight here is the difference between recoverable and unrecoverable errors: if the data fails validation before any changes are made server-side, then I believe the user should be alerted via a warning message and not an error message. Errors should indicate that something went wrong, and if a form fails validation then the user should always have the option to amend their data and resubmit it. In project management terminology, warnings indicate a risk (something that could go wrong if mitigating action is not taken), whilst errors indicate an issue – something that has gone wrong already.
I also believe that there is a difference between the simple data validation and the contextual business domain validation, and that only the first type should ever be replicated client-side (ignoring AJAX for now – more of that next time). In an MVC world, I think the first type can be done by the controller, and that this validation should match client-side validation, whilst the second type should be done deeper into the model and / or application domain. (Assuming that the controller is just that – a controller – and that it delegates the ‘doing’ to other components.)
Below is some pseudo-code demonstrating what I believe** to be the ideal processing and validation for a sample form request handler.
ProcessForm(request)
{
/* first do some 'dumb' data type validation of input values – remember
that all request values are passed over the wire as text, so they need to
be validated according to the basic destination data type. At this point
client-side validation should be ignored – we don’t know that the information
was submitted using the form – we simply know which controller was called. */
if (! isEmail(request.email))
view-data.errors.add(new InvalidPropertyException("email"))
if (! isZipcode(request.zip))
view-data.errors.add(new InvalidPropertyException("zipcode"))
if (! isDate(request.dob))
view-data.errors.add(new InvalidPropertyException("dob"))
etc.
/* if we have any errors so far then don't bother continuing, return to the
form and prompt the user for new values. This is WARNING, and not an ERROR,
as nothing has been changed server-side, and the user can always amend the
values and resubmit. This is the server-side equivalent of client-side JS
validation. */
if (view-data.hasErrors)
returnToFormAndHighlightIssues()
/* if we get here then we know that the values are 'correct' but that doesn't
mean they will work. This next validation step may require more context than
the simple validation above – it is not something that can (or should) be
replicated in client-side JS. e.g. is the price in the acceptable range, is the
username available. */
try
{
model = new model(request.email, request.zip, request.dob, request.price)
doSomethingWithModel(model)
}
/* exception thrown by any property setter that doesn't like the value it's
given; this is functionally equivalent to the case above - nothing has really
happened so log this as a WARNING, and not an ERROR */
catch (InvalidPropertyException)
{
view-data.errors.add(theException)
returnToFormAndHighlightIssues()
}
/* exception thrown by the doSomethingWithModel method that occurs BEFORE
anything has been committed, and whilst there is the opportunity to resubmit
the data. An example of this might be an attempt to register a duplicate
username; this can be marked as a WARNING or an ERROR, depending on context.*/
catch (RecoverableException)
{
view-data.errors.add(theException)
returnToFormAndHighlightErrors()
}
/* exception thrown by the doSomethingWithModel method after data has been
irreversibly committed. In this case resubmitting the data is not desirable,
and the user should be alerted! An example of this might be a database
exception after the record has been partially committed. */
catch (UnrecoverableException, AnyOtherUnexpectedException)
{
view-data.errors.add(theException)
renderDifferentPageWithMoreInformation()
}
// phew - if we got here it all went well, so we render the anticipated page
renderExpectedPage()
}
** It would be fair to say that my views on this aren’t universally accepted – comments welcome.
No comments:
Post a Comment