Object-Oriented Form Handler

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Object-Oriented Form Handler

Post by Camedo »

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.

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; }
}
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:

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);
}
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.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

You might want to think about using external, atomic classes for your rules rather than internal ones. It reduces the side of the validator while making is consistently extensible. Examples:

Code: Select all

$Form->addRule('email', new Rule_Length(6, 128), 'Email must be 6-128 characters');
$Form->addRule('location', new Rule_Required(), 'Location is a required field');
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

I'd lose the calls to die() too. Having your code exit without explicitly asking it to do so is going to bite you in ass eventually ;)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:I'd lose the calls to die() too. Having your code exit without explicitly asking it to do so is going to bite you in ass eventually ;)
Yeah. Maybe adding an isError() method so you can check.
(#10850)
User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Post by Camedo »

I like the atomic class idea, I'm going to redesign the engine today then to use those instead. And when I do, those die() calls will disappear anyway. I'll probably go ahead and finish hashing out the whitelist / blacklist rules when that happens.

Thanks for the suggestions, the next version should be a lot cleaner. I'll post the code for review when it's done.
User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Post by Camedo »

How about this?

Code: Select all

if ( !defined( 'FORM_FAIL' ) ) define( 'FORM_FAIL', -1 );       // Event - Fires when the form validation fails
if ( !defined( 'FORM_POST' ) ) define( 'FORM_POST', 0 );        // Event - Fires when the handler has a new post
if ( !defined( 'FORM_PASS' ) ) define( 'FORM_PASS', 1 );        // Event - Fires when the form validation passes

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( $field, $rule, $failAlert='' ) {
        array_push( $this->rules, array($field, $rule, $failAlert) );
    }

    //
    // Register a 'field call' callback to fire during processing. (Optional)
    // ----------------------------------------------------------------------------
    function registerCall($field, $function) {
        if(!is_callable($function)) { return FALSE; }
        array_push( $this->cRegister, array($field, $function) );
        return TRUE;
    }
    //
    // Register an event callback to fire during processing. (Optional)
    // ----------------------------------------------------------------------------
    function registerEvent($event, $function) {
        if ( $event != FORM_PASS && $event != FORM_FAIL && $event != FORM_POST ) return FALSE;
        if(!is_callable($function)) return FALSE;
        array_push( $this->eRegister, array($event, $function) );
        return TRUE;
    }
    
    //
    // 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 this form post has occured
        if ( !isset($_POST['handle']) || $_POST['handle'] != $this->id ) return FALSE;
        
        // Fire the 'POST' events
        $this->_fireEvent(FORM_POST);

        // 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;
            if ( get_magic_quotes_gpc() ) $Value = stripslashes($Value);
            $this->data[$key] = $value;
        }

        // Process Rule List, Look for Errors
        $this->conflicts = FALSE;
        foreach($this->rules as $rule) {
            list( $field, $validator, $failAlert ) = $Rule;
            $data = isset( $this->data[$field] ) ? $this->data[$field] : NULL;

            if ( is_object( $validator ) ) {
                if ( !$validator->inspect( $this, $data ) ) $this->_recordError( $field, $failAlert );
            }
        }

        // 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 ? FORM_FAIL : FORM_PASS );
        
        return TRUE;
    }

    function _recordError( $field, $msg ) {
        array_push( $this->errors, array($field, $msg) );
        $this->conflicts = TRUE;
    }
    
    function hasErrors( ) { return $this->conflicts; }
    function getData( ) { return $this->data; }
}

//
// Validation Rules
// ================================================================================
class _Rule { function inspect( &$form, $data ) { return FALSE; } }

class Rule_Length extends _Rule {
    var $lMin = 0;
    var $lMax = 0;
    function Rule_Length( $lMin, $lMax ) { $this->lMin = $lMin; $this->lMax = $lMax; }
    function inspect( &$form, $data ) {
        $tmp = strlen($data);
        return ( ( $tmp <= $this->lMax && $tmp >= $this->lMin ) ? TRUE : FALSE );
    }
}

class Rule_Required extends _Rule {
    function inspect( &$form, $data ) { return ( $data != NULL && strlen($data) > 0 ) ? TRUE : FALSE; }
}

class Rule_Whitelist extends _Rule {
    var $filter = "";
    function Rule_Whitelist( $filter ) { $this->filter = $filter; }
    function inspect( &$form, $data ) { /* ... */ return TRUE; }
}

class Rule_Blacklist extends _Rule {
    var $filter = "";
    function Rule_Blacklist( $filter ) { $this->filter = $filter; }
    function inspect( &$form, $data ) { /* ... */ return TRUE; }
}

class Rule_Match extends _Rule {
    var $field = "";
    function Rule_Match( $field ) { $this->field = $field; }
    function inspect( &$form, $data ) {
        // Lookup field $field and compare data
        if ( !isset($form->data[$field] ) ) return FALSE;
        return ( $form->data[$field] == $data ) ? TRUE : FALSE;
    }
}

class Rule_Call extends _Rule {
    var $func = "";
    function Rule_Call( $func ) { $this->func = $func; }
    function inspect( &$form, $data ) {
        if ( !is_callable($this->func) ) return FALSE;
        return call_user_func_array( $this->func, array( $form, $data ) );
    }
}
Example Code: (Same as before, just updated for the new style)

Code: Select all

require "class.form.php";

// Invoke the Form Handler
$Form = new FormHandler('signup');

// Populate the Form Handler with the available 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', new Rule_Length(6, 128), 'Your email length must be between 6 and 128 characters long.');
$Form->addRule('display', new Rule_Length(4, 36), 'Your display name must be between 4 and 36 characters long.');
$Form->addRule('pass1', new Rule_Length(6, 36), 'Your password must be between 6 and 36 characters.');

// This is an 'EQUAL' rule, if pass2 doesn't match pass1 it fails.
$Form->addRule('pass2', new Rule_Match('pass1'), 'Passwords do not match.');

// Whitelist rule. Fails if the email contains any character but the allowed ones below.
$Form->addRule('email', new Rule_Whitelist('abcdefghijklmnopqrstuvwxyz0123456789@_.'), 'Your email address contains invalid characters.');

// These rules are 'REQUIRED' rules, the fields must contain data to pass.
$_Require = new Rule_Required( );
$Form->addRule('location', $_Require, 'Your location is required.');
$Form->addRule('zip', $_Require, 'Your zipcode is required.');
$Form->addRule('human', $_Require, 'You must enter the code you see below.');

// These rules are 'CALL' 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', new Rule_Call('isEmailUnique'), 'This email address is already in use.');
$Form->addRule('display', new Rule_Call('isDisplayUnique'), 'This display name is already in use.');

// This function checks if the email field already exists, failing if true.
function isEmailUnique( &$form, $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( &$form, $data ) {
    $DB = connectDB();
    $Res =& $DB->query("SELECT * FROM `users` WHERE `name` = '$data' LIMIT 1;");
    if ( $Res->numRows() > 0 ) return FALSE;
    return TRUE;
}

// Process the form. Collect the data. If there's a critical error, bounce 'em out.
// --------------------------------------------------------------------------------------------------
if ( $Form->run() )
{
    $Data = $Form->getData();

    // Display any rule validation errors that occured, otherwise process the data.
    if ( $Form->hasErrors() ) {
        $Errors = "";
        foreach( $Form->Errors as $Conflict ) { $Errors .= $Conflict[1] . "<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);
    }
}
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

You can also code something like this to make it readable, it doesn't have to be exact, but there is a lot you can do

Code: Select all

$form
 ->rules('email')
  ->add(new Rule_Length(6,128), 'Some error')
  ->add(new Rule_Required(), 'Email is required') ;
User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Post by Camedo »

Thanks, Begby. I didn't know I could do that, I thought I had to precede each -> with the object reference. I'll keep that in mind.

Anybody else have any tips, or does the form validation class look pretty solid? Any flaws, especially security vulnerabilities?
User avatar
Benjamin
Site Administrator
Posts: 6935
Joined: Sun May 19, 2002 10:24 pm

Post by Benjamin »

What were your intentions when creating this class? Is it achieving its objectives?
User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Post by Camedo »

Well, to be honest, my only problem seems to be remembering to use it. But otherwise, it does seem to solve quite a few problems. But seeing as it's not a necessary component, the question comes down to 'How bad will this hurt the program efficiency?', and 'Does this open any security holes?'

So far it doesn't seem to create any security problems and the impact on performance is negligible. I was just wondering if anybody else noticed any glaring problems.
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

Just to note Begby's fluid interface suggestion requires that add() returns the current object reference, i.e. return $this; at the end of the function.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Maugrim_The_Reaper wrote:Just to note Begby's fluid interface suggestion requires that add() returns the current object reference, i.e. return $this; at the end of the function.
It will also only work on PHP5 and it appears the original code was written for PHP4.
User avatar
Camedo
Forum Newbie
Posts: 9
Joined: Mon May 14, 2007 12:29 am
Location: Gresham, Oregon

Post by Camedo »

Oh, okay. d11wtq is right, I wrote this code for PHP4.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Ok ... probably the next step would be to move the validation code out of the Form Manager and put it in its own Validator class. Then the Form Manager can create the Validator when it needs it. The Form Manager can then focus on managing the process and being a container to hold the various configuration and processed values.

Typically a Form Manager has two input states and two output states:

Input states:
- Initialize the form the first time in. Data comes from a datasource or default values.
- The form is submitted by the user. Data comes from the request.

Output states:
- Display the form with values and any error messages
- Display a success page or forward/redirect.
(#10850)
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

If you are looking for a case specific form validator you might have accomplished your task. However if you were posting this with the hope that others would view it as the total solution to form validation you won't have achieved it. Form validation is tricky for the single reason that every object orientated solution seems to be consistently limiting in capability and flexibility. The solution is to establish what areas are likely to change and code an interface that will satisfy those needs. Less than a month ago I made such an assessment and came up with this list of possible flex points (things likely to change or that need to be extendible) this is it:
  • The source of the data to be validated
  • The relationship between data and fields. One field can return multiple pieces of data or just one or a field called 'foo' might send data to 'bar'.
  • The destination of errors
  • The error message including the behaviour that generates it
  • The way errors are presented
  • The parameters/data required in order to perform a validation
  • The validation behaviour
  • The criteria by which you assess the success of the validation behaviour
Going from memory (my code is at work) the way I implemented this was to have
  • Atomic validator classes. These had setTestData(), setParams(), getDefaultErrorMessage() and invoke() methods.
  • MessageSet objects that allow you to store many strings under many keys and hold a renderer composition so you can choose how these are displayed, so methods of addMessage($keys, $message), getMessages($keys), renderSingle($keys) and render()
  • A single renderer for the MessageSet object that displayed the errors in an unordered list
  • A data retriever object that implemented ArrayAccess and returned null when a value didn't exist instead of triggering an error
  • A factory/loader object for getting validations class instances returned dynamically that allowed you to specify multiple location where they are stored. So that I could have core validations and project specific ones on top of them.
  • A component object which tied together most of the other objects making full use of a fluent interface and combined setter and getter methods. This also had several helper methods that allowed you to things in several objects at once. For instance with('foo') would call dataSource('foo') and errorDestination('foo'). There were a couple of things like that.
I've left some stuff out but you get the idea.

Ohh idea! I can get some of the code off the remote server. OK The end result looked something like this:

Code: Select all

abstract class Form_Abstract extends Basic_Form_Abstract
{
    protected function _getValidationFactory()
    {
        $val = new Validation_Factory($component = new Validation_Component());
        $component->msgSet($this->_errors)->source($_POST);
        $val->getCaller()->addClassTemplate('Project_Validation_Test_%s');
        return $val;
    }
}

Code: Select all

class Form_JobNew extends Form_Abstract
{
    public function isSubmitted()
    {
        return isset($_POST['fJobNumber']);
    }
    protected function _onValid()
    {
    	// stuff
    }
    protected function _onSubmit()
    {
        $val = $this->_getValidationFactory();

        if ($val->make('exists')->with('fJobNumber')->run()) {
            if ($val->make('integral')->with('fJobNumber', 'I\'m looking for a whole <em>number</em> here')->run()) {
                $val->make('notEqual')->with('fJobNumber')->params(array('to' => 0))->run();
            }
        }
        $len = $val->make('length');
        $len->params(array('min' => 1, 'max' => 255))->with('fTitle')->run();
        $len->params(array('min' => null))->with('fSubtitle')->run();

        if ($_POST['fProtected']) {
            $len->params(array('min' => 5, 'max' => 32))->with('fPassword')->run();
        }
        if ($_POST['fApproval']) {
            $len->params(array('min' => null, 'max' => 63 * 1024))->with('fBodyText')->run();
        }
        return !count($this->_errors);
    }
}
One of the critical features of this style of validation is that because it is not event driven conventional logic can still be used. Also you can still manually instantiate the atomic validations and use them however you like.

All of the objects I have described have abstract superclasses/interfaces above them so all in all it was quite a bit of work that took two evenings of thinking and three days of coding to get right. The end result was more complicated to use than a procedural solution but brilliantly scalable and well worth doing. Also it dramatically improved the testability of my validations.
Post Reply