Form Validation Class using PHP 5.3

18

Posted on : 18-06-2010 | By : Brett | In : PHP, Programming

Currently, I am working on a minimal MVC framework for a somewhat ambitious project. I chose to host this website using the latest and greatest: PHP-FPM on nginx running PHP 5.3. After realizing that I needed a form validation library, I came up with one that is fairly easy to use and somewhat extensible. This is the first time I have been able to use Closures in PHP code so maybe I am just a little bit geeky-giddy.

The Code

My only real criteria when I began writing is that I wanted to use method chaining so I could “stack up” rules on a given post variable. Here is what I ended up with:

<?php
 
class FormValidator extends Library_Base {
 
    private $messages = array();
    private $errors = array();
    private $rules = array();
    private $fields = array();
    private $haspostdata = FALSE;
 
    /*** ADD NEW RULE FUNCTIONS BELOW THIS LINE ***/
 
    /**
     * email
     * @param string $message
     * @return FormValidator
     */
 
    public function email($message='')
    {
        $message = ( empty ($message) ) ? '%s is an invalid email address.' : $message;
        $this->set_rule(__FUNCTION__, function($email) {
            return ( filter_var($email, FILTER_VALIDATE_EMAIL) === FALSE ) ? FALSE : TRUE;
        }, $message);
        return $this;
    }
    /**
     * required
     * @param string $message
     * @return FormValidator
     */
 
    public function required($message='')
    {
        $message = ( empty ($message) ) ? '%s is required.' : $message;
        $this->set_rule(__FUNCTION__, function($string) {
            return ( empty ($string) ) ? FALSE : TRUE;
        }, $message);
        return $this;
    }
 
    /**
     * numbersonly
     * @param string $message
     * @return FormValidator
     */
    public function numeric($message='')
    {
        $message = ( empty ($message) ) ? '%s must consist of numbers only.' : $message;
        $this->set_rule(__FUNCTION__, function($string) {
            return ( preg_match('/^[-]?[0-9.]+$/', $string) ) ? TRUE : FALSE;
        }, $message);
        return $this;
    }
 
    /**
     *
     * @param int $len
     * @param string $message
     * @return FormValidator
     */
    public function minlength($minlen, $message='')
    {
        $message = ( empty ($message) ) ? '%s must be at least ' . $minlen . ' characters or longer.' : $message;
        $this->set_rule(__FUNCTION__, function($string) use ($minlen) {
            return ( strlen(trim($string)) < $minlen ) ? FALSE : TRUE;
        }, $message);
        return $this;
    }
 
    /**
     *
     * @param int $len
     * @param string $message
     * @return FormValidator
     */
    public function maxlength($maxlen, $message='')
    {
        $message = ( empty ($message) ) ? '%s must be no longer than ' . $maxlen . ' characters.' : $message;
        $this->set_rule(__FUNCTION__, function($string) use ($maxlen) {
            return ( strlen(trim($string)) > $maxlen ) ? FALSE : TRUE;
        }, $message);
        return $this;
    }
 
    /**
     *
     * @param int $len
     * @param string $message
     * @return FormValidator
     */
    public function length($len, $message='')
    {
        $message = ( empty ($message) ) ? '%s must be exactly ' . $len . ' characters in length.' : $message;
        $this->set_rule(__FUNCTION__, function($string) use ($len) {
            return ( strlen(trim($string)) == $len ) ? TRUE : FALSE;
        }, $message);
        return $this;
    }
 
    /**
     *
     * @param string $field
     * @param string $label
     * @param string $message
     * @return FormValidator
     */
    public function matches($field, $label, $message='')
    {
        $message = ( empty ($message) ) ? '%s must match ' . $label . '.' : $message;
 
        $matchvalue = $this->getval($field);
 
        $this->set_rule(__FUNCTION__, function($string) use ($matchvalue) {
            return ( (string)$matchvalue == (string)$string ) ? TRUE : FALSE;
        }, $message);
        return $this;
    }
 
    /**
     *
     * @param string $field
     * @param string $label
     * @param string $message
     * @return FormValidator
     */
    public function not_matches($field, $label, $message='')
    {
        $message = ( empty ($message) ) ? '%s must not match ' . $label . '.' : $message;
 
        $matchvalue = $this->getval($field);
 
        $this->set_rule(__FUNCTION__, function($string) use ($matchvalue) {
            return ( (string)$matchvalue == (string)$string ) ? FALSE : TRUE;
        }, $message);
        return $this;
    }
 
    /*** ADD NEW RULE FUNCTIONS ABOVE THIS LINE ***/
 
    /**
     * callback
     * @param string $name
     * @param mixed $function
     * @param string $message
     * @return FormValidator
     */
 
    public function callback($name, $function, $message='')
    {
        if ( is_callable($function) ) {
            // set rule and function
            $this->set_rule($name, $function, $message);
        } elseif ( is_string($function) && preg_match($function, 'callback') !== FALSE ) {
            // we can parse this as a regexp. set rule function accordingly.
            $this->set_rule($name, function($value) use ($function) {
                return ( preg_match($function, $value) ) ? TRUE : FALSE;
            }, $message);
        } else {
            // just set a rule function to check equality.
            $this->set_rule($name, function($value) use ( $function) {
                return ( (string)$value === (string)$function ) ? TRUE : FALSE;
            }, $message);
        }
        return $this;
    }
 
    /**
     * validate
     * @param string $key
     * @param string $label
     * @return bool
     */
 
    public function validate($key, $label='') {
        // do not attempt to validate when no post data is present
        if ($this->haspostdata) {
            // set up field name for error message
            if (!empty($label)) {
                $this->fields[$key] = $label;
            }
            // try each rule function
            foreach ($this->rules as $rule => $function) {
                if (is_callable($function)) {
                    if ($function($this->getval($key)) === FALSE) {
                        $this->register_error($rule, $key);
                        // reset rules
                        $this->rules = array();
                        return FALSE;
                    }
                } else {
                    $this->register_error($rule, $key, 'Invalid function for rule');
                    $this->rules = array();
                    return FALSE;
                }
            }
            // reset rules
            $this->rules = array();
            return TRUE;
        }
    }
 
    /**
     * has_errors
     * @return bool
     */
 
    public function has_errors()
    {
        return ( count($this->errors) > 0 ) ? TRUE : FALSE;
    }
 
    /**
     * set_error_message
     * @param string $rule
     * @param string $message
     */
 
    public function set_error_message($rule, $message) {
        $this->messages[$rule] = $message;
    }
 
    /**
     * get_error
     * @param string $field
     * @return string
     */
 
    public function get_error($field)
    {
        return $this->errors[$field];
    }
 
    /**
     * get_all_errors
     * @return array
     */
 
    public function get_all_errors()
    {
        return $this->errors;
    }
 
    /*public function __set($key, $value) {
        $this->messages[$key] = $value;
    }
 
    public function __get($key) {
        return $this->messages[$key];
    }*/
 
    /**
     * getval
     * @param string $key
     * @return mixed
     */
 
    private function getval($key)
    {
        return ( isset ($_POST[$key]) ) ? $_POST[$key] : FALSE;
    }
 
    /**
     * register_error
     * @param string $rule
     * @param string $key
     * @param string $msg_override
     */
 
    private function register_error($rule, $key, $msg_override='')
    {
        $message = ( empty($msg_override) ) ? $this->messages[$rule] : $msg_override;
        $field = $this->fields[$key];
 
        if ( empty ($message) )
            $message = '%s has an error.';
 
        if ( empty ($field) )
            $field = "Field with the name of '$key'";
 
        $this->errors[$key] = sprintf($message, $field);
    }
 
    /**
     * set_rule
     * @param string $rule
     * @param closure $function
     * @param string $message
     */
 
    private function set_rule($rule, $function, $message='') {
        // do not attempt to validate when no post data is present
        if ($this->haspostdata) {
            if (is_callable($function)) {
                $this->rules[$rule] = $function;
                if (!empty($message)) {
                    $this->messages[$rule] = $message;
                }
            }
        }
    }
 
}
?>

Please note: This only works for forms submitted with POST. However, it can be easily modified to work with GET also.

Usage

Now, let’s say that I have a form and I want to validate an email field and set up a password. The password must be at least 10 characters and the password confirmation field must match the password field. On top of that, I have a simple captcha where I just ask the user to add two integers that equal 10. With my class, I only need to write the following code:

Usage   
<?php
$formval = new FormValidator();
$formval->required()->email()->validate('email', 'Email Address');
$formval->required()->minlength(10)->validate('password', 'Password');
$formval->required()->matches('password', 'Password')->validate('password2', 'Password Confirmation');
$formval->required()->callback('captcha', function($value) {
	return ( preg_match('/^\d\s*\+\s*\d$/', trim($value)) && eval ($value) == 10 ) ? TRUE: FALSE;
}, '%s does not add up to 10!')->validate('captcha', 'Captcha');
if ( $formval->has_errors() ) {
   print_r($formval->get_all_errors());
} else {
	// submit the form!
}
?>

Pretty simple!

I have not started using this class heavily ( I just finished it) so let’s call it alpha code for now.

Forks!

I had submitted my code as a project on github, to which, Tasos has generously forked (and pointed out a bug in my original code–thanks!) Tasos has modified rules and created many more. Since this post was more of a demonstration of how using closures and method chaining goes a long way to making a better validation class, I would seriously consider using Tasos version since it has been “polished” for production use. You can find his repo at: https://github.com/bekos/php-validation

If you want to fork my original version (with any future big fixes and/or new features) the project is also at github:

https://github.com/bretticus/PHP-5.3-Form-Validator

Comments

$this->set_rule(__FUNCTION__, function($email) {
return ( filter_var($email, FILTER_VALIDATE_EMAIL) === FALSE ) ? FALSE : TRUE;
}, $message);

Getting error on this line

Parse error: syntax error, unexpected T_FUNCTION in C:\xampp\htdocs\work\CASC\admin\form-validator.php on line 21

What version of PHP are you using?

PHP Version 5.2.6

I know it should be 5.3 but what are the alternatives for 5.2

You could convert the functions to lamdas via the create_function() function, however, everytime you see something like:

….function($value) use ($some_global_var)…

You can try to use global in the lambda function but it’s kind of a pain. I’d recommend taking all the functions and turn them into private methods. You’d have to call them with something like call_user_func_array(). This requires some significant re-factoring so perhaps you shouldn’t use this class.

Best recommendation, use PHP 5.3 if you can. :)

Thanks,I will try

pity that this class does not work on PHP 5.2: (

Why show me:
Parse error: syntax error, unexpected T_FUNCTION in /var/www/virtual/jokerman.y0.pl/CLASS2/class.form_validator.php on line 27

Hi moto0000,

Thanks for leaving a comment.

PHP Closures were just a way for me to stack up anonymous functions that run the current validation (POST field.) It’s a simple programming convenience that could be replaced with lambda functions. Or you could just skip lamdas and use the magic method __call() to determine what tests to run and using the call_user_func() function to make actual function calls to class-defined functions or defined callbacks.

The method chaining is as easy as returning the current object for each validation function called (not PHP 5.3 specific either.)

Cheers!

How to make class pay all the mistakes and not just the first?

Personally, I prefer not to nag the user about more than one validation failure per field. 1) Because most users are non-technical and it’s better to keep instructions simple and 2) users rarely make multiple validation mistakes on the same field. If you require all messages being reported, it shouldn’t take too much effort to re-factor the validate() method. Feel free to fork the library at github.

Good script but I think there is a bug, about scope.

To reproduce it you can validate 2 different fields for minlength 5 (the 1st) and 2 (the 2nd).
Scope problem validates both fields for minimum length of 5.

Can you confirm this bug?
Thx

…continue…

I think bug is on line 273 where the new function for the 2nd field is not set because already exists, but with the older aruments.

You can probably remove this check, or better create a private arguments array to be always set and called with validate.

Thanks for the information Tasos.

Can you post an example of how you are validating the fields? Once the validation method is called, the rules array is reset. Not sure why you are having that issue, but then again, I haven’t used this validation class in production much yet.

Thanks Tasos. That was a function caching algorithm I wrote in later. Really not necessary and didn’t really save me on resources since I compiled and sent the function anyway. It was working for closures where the use keyword was not in effect because the parameter for the closure was always the current POST value, however, there seems to be no obvious way to determine if the “use” variables were different. I thought about adding a boolean parameter toggle function caching for set_rules() but it just didn’t seem feasible (unless I check for the cached function in the public functions themselves, but again little gain for a unnecessary feature.)

Thanks again. I will be updating the source code shortly.

I might implement the args private array as you suggested later on. For now, feel free to fork the project at github.

Hello again Brett.

I have made some changes to your work and published it on github.

I have added rules (basically date validation), moved default error messages into separate function to make it easier for i18n and tried to minimize script warnings.

I implemented the arguments private array in order to leave your caching method untouched.

See: https://github.com/bekos/php-validation and if it’s OK with you you can publish it.

Tasos,

Thanks for the additional rules that you have added to the FormValidator class on github, per my request.

Tasos has added the following rule validations…

startsWith
notstartsWith
endsWith
notendsWith

Thanks again.

The validator also protects against sql injections?

The code I posted here is purely for validating form input data. No, it does not protect from SQL injection attacks. The PHP manual has some suggestions for avoiding SQL Injections. Personally, I like to use PDO prepared statements with named place holders. You can use this class to add a callback function or a new inline function to test that your data is valid before actually inserting in your database (which is one of the several purposes of form validation anyway.)

Post a comment