Page 1 of 1

OO Validator package

Posted: Mon Jul 12, 2004 2:49 am
by Weirdan
This package is inspired by ideas scattered around on devnetwork and sitepoint forums:

Code: Select all

<?php
 /**
 * @package Validator
 * @author  Bruce Weirdan
 * @version $Id: Validator.php,v 1.14 2004/06/10 01:33:13 weirdan Exp $
 */

/**
 * abstract class to inherit Validators from
 * @abstract
 */
class Rule {
    /**
     * real constructor
     * @abstract Redefine it in subclasses if appropriate
     * @return void
     */
     function __construct() {}

    /**
     * destructor. Does nothing.
     * @return void
     */
     function __destruct() {}

    /**
     * fake constructor for upcoming PHP5 compatibility
     */
     function Rule() {
        $args = func_get_args();
        register_shutdown_function( array(&$this, '__destruct') );
        call_user_func_array( array(&$this, '__construct'), $args);
     }

    /**
     * heart of validator and rules
     * @return bool true if there were no errors, false otherwise
     * @abstract Redefine it in subclasses
     */
     function execute() {}

    /**
     * @return string description of error(s) (if any)
     * @abstract Redefine it in subclasses
     */
     function getDescription() {}
}
 /**
  * Main validator class. Implements slightly simplified Composite pattern
  * example:
  *<code>
  *$val = new Validator($_REQUEST);
  *$val->addRule( 'asd', new Required(new AlphaNum) );
  *$val->addRule( 'sdf', new Optional(new AlphaNum) );
  *var_dump($val->execute());
  *var_dump($val->errors);
  *</code>
  *
  */
class Validator extends Rule {
    /**
     * @var array $rules array(string name => Rule)
     * @final
     */
    var $rules = array();

    /**
     * @var array $incoming array to validate (eg $_GET)
     * @final
     */
    var $incoming = array();

    /**
     * @var array $errors array to store errors in
     * @final
     */
    var $errors = array();

    /**
     * real constructor.
     * Note: one must supress PHP notes while calling this method with no params
     * by prepending it with @ sign or there might be notes issued.
     * It's limitation of PHP4. Currently there is no <i>elegant</i> workaround
     * for this issue.
     * @param array &$operate_on array to operate on
     * @return void
     */
    function Validator(&$operate_on){
	    $this->incoming =& $operate_on;
    }

    /**
     * add rule for name. Call this function once for each name
     * @param string $for_name name to add rule for
     * @param Rule $rule validator for given name
     * @return bool true on successful addition, false otherwise
     * @final
     */
    function addRule($for_name, $rule) {
    	if( isset($this->rules[$for_name]) ) {
    	    trigger_error('Rule has been already set. Use Validator instead of multirules');
    	    return false;
    	}
    	$this->rules[$for_name] = &$rule;
    	return true;
    }
    /**
     * remove rule for name. Found no usage for this as for now...
     * @param string $for_name name to remove rule for
     * @return bool true if rule has been removed, false on error
     * @final
     */
    function removeRule($for_name) {
    	if( !isset($this->rules[$for_name]) ) {
    	    trigger_error('No rules has been set for ' . $for_name);
    	    return false;
    	}
    	unset($this->rules[$for_name]);
    	return true;
    }
    /**
     * heart of validator. scans $rules array and executes them on corresp. data
     * Note: one must supress PHP notes while calling this method with no params
     * by prepending it with @ sign or there might be notes issued.
     * It's limitation of PHP4. Currently there is no <i>elegant</i> workaround
     * for this issue.
     * @param array &$operate_on array to operate on.
     * @return bool true if there were no errors, false otherwise
     */
    function execute(&$operate_on) {
        if( isset($operate_on) ) {
            $this->incoming =& $operate_on;
        }
    	if( !count($this->rules) ) {
    	    trigger_error('No rules has been set at all');
    	    return false;
    	}
    	/*
    	if( !count($this->incoming) ) {
    	    return false; //incoming is empty
    	}*/
    	foreach($this->rules as $name => $rule) {
    	    //echo "About to execute rule for $name\n";
            $was_set = isset($this->incoming[$name]);
    	    if(!@$rule->execute(&$this->incoming[$name])) {
        		$this->errors[$name] = $rule->getDescription();
    	    }
		    if(!$was_set && !is_a($rule, 'Checkbox'))
                unset($this->incoming[$name]); // special handling for originally unset fields
                                               // it's necessary since call to $rule->execute()
                                               // would create $this->incoming[$name] element
                                               // set to NULL. ( Does it make any sense to
                                               // validate unset values? Nevertheless, original
                                               // state of $this->incoming array is preserved )
                                               // It's done for all rules *except* those which
                                               // are expected to change the value. As for now,
                                               // only Checkbox does so, but later, when we'll
                                               // introduce more modifying rules, it may become
                                               // a problem. Modifying rules are evil.

    	}
    	return !count($this->errors);
    }

    /**
     * @return string description of error(s)
     */
    function getDescription() {
        return implode("<br />\n", $this->errors);
    }
}

/**
 * uses decorator pattern to check for field existence before passing it to next validator
 */
class Required extends Rule {
    /**
     * @var Rule $next rule to wrap around
     */
    var $next;

    /**
     * @var string $description description of the error. If value is not set - uses default desc
     */
    var $description = 'Required field not set';

    /**
     * check for field presence and forward request to next validator
     * @param mixed $value value to check
     * @return bool return value from $next validator if field has been set, false otherwise
     */
    function execute($value) {
    	if( !isset($value) ) return false;
        if( is_null($this->next) ) return true; // if no next validator complete processing
    	$this->description = $this->next->getDescription();
    	return $this->next->execute($value);
    }

    /**
     * @return string description of error
     */
    function getDescription() {
    	return $this->description;
    }

    /**
     * constructor
     * @param Rule $next reference to next validator. Pass null if there is no next validator
     * @return void
     */
    function __construct(&$next) {
    	$this->next = &$next;
    }
}
/**
 * decorator to prevent errors for optional fields
 */
class Optional extends Rule {
    /**
     * @var Rule $next rule to wrap around
     */
    var $next;

    /**
     * @var string $description description of error (if any)
     */
     var $description;

    /**
     * constructor
     * @param Rule $next reference to next validator. Pass null if there is no next validator
     * @return void
     */
    function __construct(&$next) {
        $this->next =& $next;
    }

    /**
     * check for field absence and forward request to next validator if appropriate
     * @param mixed $value value to check
     * @return bool return value from $next validator if field has been set, true otherwise
     */
    function execute($value) {
        if( empty($value) || is_null($value) || is_null($this->next) ) return true;
        $ret = $this->next->execute($value);
	    if(!$ret)
		    $this->description = $this->next->getDescription();
	    return $ret;
    }
    /**
     * @return string description of error (if any)
     */
    function getDescription() {
        return $this->description;
    }
}
/**
 * check if field is alpanumeric
 */
class AlphaNum extends Rule {
    /**
     * check value against string
     * @param string $value value to operate on
     * @return bool true if value is alphanumeric, false otherwise
     */
    function execute($value) {
    	return !eregi('[^0-9a-zA-Z]', $value);
    }

    /**
     * @return string description of error
     */
    function getDescription() {
    	return 'Field should contain alphanumeric chars only';
    }
}
/**
 * process checkboxes and convert them to bool values
 */
class Checkbox extends Rule {
    /**
     * @param string $value it will be converted as $value = isset($value)
     * @return bool always true
     */
    function execute(&$value) {
        $value = isset($value);
        return true;
    }
    /**
     * @return string always empty string
     */
    function getDescription() {
        return '';
    }
}

/**
 * rule to check if field contains integer value
 */
class Integer extends Rule {
    /**
     * @param string $value value to check
     * @return bool true if $value is integer, false otherwise
     */
    function execute($value) {
        return is_numeric($value) && (strval(intval($value)) == $value);
    }
    /**
     * @return string description of error
     */
    function getDescription() {
        return 'Field should be integer';
    }
}
/**
 * decorator to check CSV fields
 */
class CSV extends Rule {
    /**
     * @var Rule $next rule to check elements against
     */
    var $next;
    /**
     * @var array $errors errors array
     */
    var $errors = array();
    /**
     * constructor
     * @var Rule $next rule to check elements against
     * @return void
     */
    function __construct(&$next) {
        $this->next =& $next;
    }
    /**
     * checking method
     * @var sting $value CSV string
     * @return bool true if there were no errors, false if one of the elts of CSV does not pass $next rule
     */
    function execute($value) {
        if( !is_null($this->next) ) {
            $values = explode(',', $value);
            foreach($values as $offset => $val) {
                if( !$this->next->execute($val) ) {
                    $this->errors[$offset] = $this->next->getDescription();
                }
            }
        }
        return !count($this->errors);
    }
    /**
     * @return string description of error(s)
     */
    function getDescription() {
        $ret = 'Element validation failed because of:<br />';
        foreach($this->errors as $offset => $error) {
            $ret .= $error . ' at offset ' . $offset . '<br />';
        }
        return $ret;
    }
}
/**
 * Rule to check ISO dates
 */
class ISODate extends Rule {
    /**
     * @param string $value date to check
     * @return bool true if $value is correct date, false otherwise
     */
    function execute($value) {
        list($year, $month, $day) = explode('-', $value);
        return checkdate($month, $day, $year);
    }

    /**
     * @return string description of error
     */
    function getDescription() {
        return 'Field should contain valid Gregorian date (ISO format: YYYY-MM-DD)';
    }
}
/**
 * Rule to check input value against list of allowed ones
 */
class Enum extends Rule {
    /**
     * @var array $values array of allowed values
     */
    var $values;

    /**
     * constructor
     * @param array $values array of allowed values
     * @return void
     */
    function __construct($values) {
        $this->values = $values;
    }

    /**
     * checks if $value in $this->values
     * @param string $value value to check
     * @return bool true on success, false otherwise
     */
    function execute($value) {
        return in_array($value, $this->values);
    }

    /**
     * @return string description of error
     */
    function getDescription() {
        return 'Field value is not in allowed set: "' . implode('","', $this->values) . '"';
    }
}
/**
 * rule to check if given string is indeed valid md5 hash
 */
class MD5_Hash extends Rule {
    /**
     * checking method
     * @param string $value value to check
     * @return bool true if value passes check, false otherwise
     */
    function execute($value) {
        if( strlen($value) != 32 ) return false; // hashes are 32chars strings
        return !eregi('[^0-9a-f]', $value);
    }

    /**
     * @return string description of error
     */
    function getDescription() {
        return 'Field is not a valid md5 hash';
    }
}

class Equal extends Rule {
    /**
     * @var mixed $equal_to value to compare with
     */
    var $equal_to;

    /**
     * constructor
     * @param mixed &$equal_to value to compare with
     * @return void
     */
    function __construct(&$equal_to) {
        $this->equal_to = $equal_to;
    }

    /**
     * checking method
     * @param mixed &$value value to check
     * @return bool true if $value equals to $this->equal_to
     */
    function execute($value) {
        return $value == $this->equal_to;
    }

    /**
     * @return string description of error
     */
    function getDescription() {
        return 'Field should have value: ' . $this->equal_to;
    }
}

Posted: Mon Jul 12, 2004 12:58 pm
by McGruff
That's similar to what I'm doing in many ways.

My starting point was the idea of replacing test arrays (eg GPC) with a ProxyObject instance. Proxies will automatically filter out any invalid values (defined by whatever rules have been registered) - hence raw input doesn't have to be accessed directly.

In order to support arrays of any depth I wound up doing a lot of eval'ing (which usually makes me wonder where I've gone wrong..). For example, the key $test_array['foo']['bar'][0] can be represented by array('foo', 'bar', 0) - passed as an arg in an addRule method and decoded later with eval. This covers the general case: for GPC you could hard-code something to work with 2d arrays (although a GPC array could theoretically be deeper it's a reasonable assumption, I think).

ProxyObjects also provide state information for a FrontController/PageController. This gets kind of complicated: basically the different superglobals all have their own quirks and require to be treated in slightly different ways. It's difficult to really discuss & compare methods in detail right now since I'm still finalising one or two ideas. Will have more soon if I can squeeze the odd spare hour or two out of a busy schedule.