Object-Oriented Form Handler
Posted: Thu May 17, 2007 10:29 pm
So i've been reading the DevNet forums for about a week now, and i've really enjoyed seeing how people are solving problems in the real world. So I can't pass up on the chance to have one of my objects critiqued. I've always been trying to find a better way to handle form validation, without going through a huge headache. So I wrote up this Form class, I was hoping you could all tell me if it's a good design or if I made some crucial mistake in the construction.
It supports a ton of features, maybe too many for a form handler honestly. The code below is just to demonstrate how you might use the class:
It should be noted that the WHITELIST and BLACKLIST rules are not complete. I have the space open for them, but I haven't written the code for them yet. To be honest, I haven't even had a use for them yet, but I could see them being useful in simple email validation and such.
Code: Select all
// Methods:
// addField( [array]Fields );
// addRule( $Nickname, $Field, $Rule, $Args='' );
// registerCall($Field, $Function);
// registerEvent($Event, $Function);
// run( );
// hasErrors( );
// getData( );
// METHOD NOTES:
// registerCall - During rule validation, passes a field to the function and keeps the
// return value. Occurs AFTER rule validation, during run()
// registerEvent - Calls a function when a specified event occurs. Valid events are
// 'HASPOST', 'VALID' and 'FAILED'. Occurs during run()
class FormHandler {
var $ID = ""; // Form ID
var $cRegister = array(); // 'Field Call' Callback Register
var $eRegister = array(); // Event Callback Register
var $Fields = array(); // Expected Fields (if empty, grab everything from $_POST)
var $Rules = array( ); // Form Field Rules
var $Data = array( ); // Cleaned POST Data
var $Errors = array( ); // Form Errors
var $Conflicts = FALSE; // Did the form have any errors?
function FormHandler( $ID ) {
$this->ID = $ID;
}
//
// Add a field to the 'expected fields' list. (Optional)
// ----------------------------------------------------------------------------
function addField( $Field ) {
if ( !is_array( $Field ) ) $Field = array( $Field );
foreach($Field as $F) { array_push($this->Fields, $F); }
}
//
// Add a rule to the processing list. (Optional)
// ----------------------------------------------------------------------------
function addRule( $Nickname, $Field, $Rule, $Args='' ) {
array_push( $this->Rules, array($Nickname, $Field, $Rule, $Args) );
}
//
// Register a 'field call' callback to fire during processing. (Optional)
// ----------------------------------------------------------------------------
function registerCall($Field, $Function) {
if(!is_callable($Function)) { die("FormHandler: $Function is not a valid function name for registerCall."); }
array_push( $this->cRegister, array($Field, $Function) );
}
//
// Register an event callback to fire during processing. (Optional)
// ----------------------------------------------------------------------------
function registerEvent($Event, $Function) {
$Events = array( "HASPOST", "VALID", "FAILED" );
if(!in_array($Event, $Events)) { die("FormHandler: $Event is not a valid event for registerEvent."); }
if(!is_callable($Function)) { die("FormHandler: $Function is not a valid function name for registerEvent."); }
array_push( $this->eRegister, array($Event, $Function) );
}
//
// Fire an event to run potential callback functions. (Private Function)
// ----------------------------------------------------------------------------
function _fireEvent($Event) {
foreach($this->eRegister as $Register) {
list($Name, $Function) = $Register;
if ( $Event == $Name ) call_user_func_array( $Function, $this );
}
}
//
// Check for an incoming form, and begin processing. (Required)
// ----------------------------------------------------------------------------
function run( ) {
// Flush any existing errors and data
$this->Data = array();
$this->Errors = array();
// Check if a form post has occured
if ( !isset($_POST['handle']) || $_POST['handle'] != $this->ID ) return FALSE;
// Fire the 'HASPOST' events
$this->_fireEvent("HASPOST");
// Ingest and clean all POST Fields into $this->Data
foreach($_POST as $Key=>$Value) {
// If we have any expected fields, make sure this is one of them.
if ( count($this->Fields) > 0 && !in_array($Key, $this->Fields) ) continue;
// Clean the $Value
if ( get_magic_quotes_gpc() ) $Value = stripslashes($Value);
// Store the $Value
$this->Data[$Key] = $Value;
}
// Process Rule List, Look for Errors
$this->Conflicts = FALSE;
foreach($this->Rules as $Rule) {
list( $Nickname, $Field, $Type, $Arg ) = $Rule;
$Type = strtoupper($Type);
$Data = $this->Data[$Field];
$Args = explode(";", $Arg);
if ( $Type == 'LENGTH' ) {
// Validate Length
if ( strlen($Data) < (integer)($Args[0]) ) { $this->_RecordError($Nickname, "String too short"); }
else if ( strlen($Data) > (integer)($Args[1]) ) { $this->_RecordError($Nickname, "String too long"); }
} else if ( $Type == 'REQUIRED' ) {
// Validate that we have contents
if( strlen($Data) <= 0 ) { $this->_RecordError($Nickname, "Required Field"); }
} else if ( $Type == 'WHITELIST' ) {
// Inspect Character by Character against the whitelist
// ...
die( "Whitelist incomplete" );
} else if ( $Type == 'BLACKLIST' ) {
// Inspect Character by Character against the blacklist
// ...
die( "Blacklist incomplete" );
} else if ( $Type == 'EQUALS' ) {
// Make sure one field equals another
if ( $Data != $this->Data[$Args[0]] ) { $this->_RecordError($Nickname, "Fields do not match"); }
} else if ( $Type == 'CHECKWITH' ) {
// Call a function with field name and data - if it returns FALSE, log an error
if ( !is_callable($Args[0]) ) { $this->_RecordError($Nickname, "Function could not be found."); }
$Rtn = call_user_func_array( $Args[0], array( $Field, $Data ) );
if ( $Rtn !== TRUE ) { $this->_RecordError($Nickname, $Rtn); }
}
}
// Fire 'field call' callbacks
foreach($this->cRegister as $Call) {
list($Field, $Func) = $Call;
$this->Data[$Field] = call_user_func_array($Func, $this->Data[$Field]);
}
// Fire necessary events
$this->_fireEvent( $this->Conflicts ? "FAILED" : "VALID" );
return TRUE;
}
function _RecordError( $Nickname, $Msg ) {
array_push( $this->Errors, array($Nickname, $Msg) );
$this->Conflicts = TRUE;
}
function hasErrors( ) { return $this->Conflicts; }
function getData( ) { return $this->Data; }
}Code: Select all
// Invoke the Form Handler
$Form = new FormHandler('signup');
// Populate the Form Handler with the available fields. By setting these, the Form Handler
// will automatically filter out all of the unnecessary $_POST fields.
$Form->addField( array( 'ref', 'email', 'display', 'pass1', 'pass2', 'location', 'zip', 'human' ) );
// Set Validation Rules for specific fields
// --------------------------------------------------------------------------------------------------
// Length Validation Rules. Fails if field is outside the specified minimum or maximum.
$Form->addRule('email_length', 'email', 'LENGTH', '6;128');
$Form->addRule('display_length', 'display', 'LENGTH', '4;36');
$Form->addRule('pass1_length', 'pass1', 'LENGTH', '6;36');
// This is an 'EQUAL' rule, if pass2 doesn't match pass1 it fails.
$Form->addRule('pass2_match', 'pass2', 'EQUALS', 'pass1');
// Whitelist rule. Fails if the email contains any character but the allowed ones below.
$Form->addRule('email_characters', 'email', 'WHITELIST', 'abcdefghijklmnopqrstuvwxyz0123456789@_.');
// These rules are 'REQUIRED' rules, the fields must contain data to pass.
$Form->addRule('location_required', 'location', 'REQUIRED');
$Form->addRule('zip_required', 'zip', 'REQUIRED');
$Form->addRule('human_required', 'human', 'REQUIRED');
// These rules are 'CHECKWITH' rules, they pass the data to a specified function for comfirmation.
// If they return TRUE, data is okay. Any other result is an error.
$Form->addRule('email_unique', 'email', 'CHECKWITH', 'isEmailUnique');
$Form->addRule('display_unique', 'display', 'CHECKWITH', 'isDisplayUnique');
// This function checks if the email field already exists, failing if true.
function isEmailUnique( $Field, $Data ) {
$DB = connectDB();
$Res =& $DB->query("SELECT * FROM `users` WHERE `email` = '$Data' LIMIT 1;");
if ( $Res->numRows() > 0 ) return FALSE;
return TRUE;
}
// This function checks if the display name already exists, failing if true.
function isDisplayUnique( $Field, $Data ) {
$DB = connectDB();
$Res =& $DB->query("SELECT * FROM `users` WHERE `name` = '$Data' LIMIT 1;");
if ( $Res->numRows() > 0 ) return FALSE;
return TRUE;
}
// Configure an array of error messages. Relates directly to the rule nicknames.
// --------------------------------------------------------------------------------------------------
$Alerts = array( "code_required"=>"A valid beta registration code is required for sign up.",
"email_length"=>"Your email address must be between 6 and 128 characters.",
"display_length"=>"Your display name must be between 4 and 36 characters.",
"pass1_length"=>"Your password must be between 6 and 36 characters.",
"pass2_match"=>"Your passwords did not match.",
"location_required"=>"You must enter a valid location from the selection box.",
"zip_required"=>"You must enter a valid zipcode if you live in the US.",
"human_required"=>"You must enter the code contained in the human verification box.",
"email_unique"=>"The specified email address is already in use.",
"display_unique"=>"The specified display name is already in use." );
// Process the form. Collect the data. If there's a critical error, bounce 'em out.
// --------------------------------------------------------------------------------------------------
if ( $Form->run() ) { $Data = $Form->getData(); } else { header("location: /index.php"); exit(); }
// Display any rule validation errors that occured, otherwise process the data.
if ( $Form->hasErrors() ) {
$Errors = "";
foreach( $Form->Errors as $Conflict ) { $Errors .= $Alerts[$Conflict[0]] . "<BR/>"; }
echo "<DIV ALIGN='CENTER'><B>This registration form contained the following errors:</B><BR/><BR/>$Errors<BR/></DIV>";
} else {
// Handle registration data.
print_r($Data);
}