8

I have looked for better ways to handle validation for as long as I've been developing web applications. Catching multiple validation errors is frequently necessary, so I wanted to know if there was a better way to do it than the following.

Right now I have an assert method in a framework I've developed myself. An example of the method is this:

assert(($foo == 1), 'Foo is not equal to 1');

If the condition in the first argument is false, the error message in the second argument is added to an $errors array (which is wrapped in a class (referenced by $eh below) that provides convenience functions such as hasErrors()).

This method works but is messy in practice. Consider this code:

public function submit($foo, $bar, $baz)
{
    assert(($foo == 1), 'Foo is not equal to 1');
    assert(($bar == 2), 'Bar is not equal to 2');

    if (!$eh->hasErrors())
    {
        assert(($baz == 3), 'Baz is not equal to 3');

        if (!$eh->hasErrors())
        {
            finallyDoSomething();
            return;
        }
    }

    outputErrors();
}

This is something fairly common. I want to check two conditions before moving on, and then if those pass, check a third condition before finally doing what I want to do. As you can see, most of the lines in this code are related to validation. In a real application, there will be more validation and possibly more nested if statements.

Does anyone have a better structure for handling validation than this? If there are frameworks that handle this more elegantly, what are they and how do they accomplish it? Multiple nested if statements seem like such a 'brute-force' solution to the problem.

Just a note, I understand it would probably be a good idea to wrap some common validation functions in a class so that I can check length, string format, etc., by calling those functions. What I am asking is a cleaner approach to the code structure, not how I am actually checking the errors.

Thank you!

4

6 に答える 6

6

Please check Respect\Validation. It's a library built for that purpouse. It can handle multiple rules very easily and uses exceptions for errors. Here is a quick sample:

<?php

use Respect\Validation\Validator as v;

$usernameValidator = v::alnum()->noWhitespace()->length(1,15);

$valid = $usernameValidator->validate("alganet"); //$valid now == true
$valid = $usernameValidator->validate("ácido acético"); //$valid now == false

Now using exceptions:

try {
    $usernameValidator->assert("foo # bar");
} catch (Exception $e) {
    $errors = $e->findMessages('alnum', 'noWhitespace', 'length');
}

In the sample above, the $errors variable would be something like this:

array(
    "alnum" => '"foo # bar" must contain only letters and digits',
    "noWhitespace" => '"foo # bar" must not contain whitespace',
    "length" => null
)

I broke two previously declared rules using "foo # bar": It has whitespace and it has a non-alnum char. For every rule that doesn't pass, a message will be returned. Since "length" is OK, the error message is null.

The documentation includes several more samples including nested, hierarchical rules and better exception handling. Also has a extensive list of samples for all of the 30+ built in validators.

Hope that helps!

于 2012-05-28T17:06:33.650 に答える
4

How about throwing exceptions? you can catch exceptions explicity with try /catch blocks, and/or catch them using set_exception_handler()

there are a number of useful exception types defined in PHP, which you can use to your advantage if you need granularity in exeption handling. plus you can define custom exceptions.

http://php.net/manual/en/function.set-exception-handler.php http://www.php.net/manual/en/spl.exceptions.php

EDIT

To answer your question about how some other frameworks approach this problem - judicious use of exceptions seems pretty common. The useful thing about using them is, say you have a particular method that does a number of different validations that might possibly be erroneous - you can throw an appopriate exception in each case, but you don't have to handle the different possible exceptions in that method. instead, depending on how you structure your code, you can allow the exception to bubble up to a more centralised place in your code where you can catch it and handle it appropriately.

EDIT 2

To elaborate on my last comment about filter_input_array()

Based on a really simple example with POSTed user data. First create a definition:

$userFormDefinition = array(
    'email' => FILTER_VALIDATE_EMAIL,
    'age'   => FILTER_VALIDATE_INT,
    'name'  => array(
        'filter'  => FILTER_VALIDATE_REGEXP, 
        'options' => array('regexp' => '/^\w+$/')
    ),
);

Then using a generic validation class (class definition below):

$formValidator = new FormValidator();
$formValidator->validatePost($userFormDefinition);

if ($formValidator->isValid()) {

    // if valid, retrieve the array
    // and use the values how you wish

    $values = $formValidator->getValues();

    // for example, extract and populate
    // a User object, or whatever :)

    extract($values);

    $user = new User();
    $user->setName($name);
    $user->setEmail($email);
    $user->setAge($age);

    // etc.
}

A very basic (and untested) implementation of a FormValidator.

The basic use case is to call the appropriate method for the request method to filter against. This in turn checks the returned values and decides if the input is valid.

This could use a lot of love - especially the filterInput method, because you might have to do some testing to make sure you handle 'truthy' or 'falsy' values appropriately. I'm thinking checkbox type values. A straight up in_array check for false might not cut it as implemented here. But there are loads of flags that you can pass in with the definition.

I guess you could also check for missing inputs by comapring a count of the resulting $values array and the definition, to make sure they match. Additional inputs not in the definition are filtered out (you might want to check that but I'm reasonably sure about this off the top of my head).

<?php

class FormValidator
{   
    private $isValid = false;

    private $isBound = false;

    private $values  = array();

    public function validatePost(array $definition)
    {
        // additional REQUEST_METHOD checking here?
        $this->filter(INPUT_POST, $definition);
    }

    public function validateGet(array $definition)
    {
        // additional REQUEST_METHOD checking here?
        $this->filterInput(INPUT_GET, $definition);
    }

    protected function filterInput($type, $definition)
    {
        $this->isBound = true;

        $this->values = filter_input_array($type, $definition);

        // might have to do some reading on nulls vs false, 
        // and validating checkbox type values here... you can
        // set all sorts of flags so a better implementation
        // would probably be required here... :s

        if (is_array($this->values) && !in_array(false, $this->values))) {
            $this->isValid = true;
        }   
    }

    public function isValid()
    {
        if (!$this->isBound) {
            throw new Exception("you didn't validate yet!");
        }

        return $this->isValid;
    }

    public function getValues()
    {
        if (!$this->isBound) {
            throw new Exception("You didn't validate yet!");
        }

        return $this->values;
    }
}

Anyway, I would say refactor and test the bejayzis out of that class, (or even totally change it) but hopefully it outlines the basic idea: for each type of input, create a definition and then use a generic validation class to filter and ensure validity.

Hope this helps. filter_input and filter_input_array rock :)

于 2012-05-19T02:57:36.540 に答える
2

When you say "validation" - I am assuming that you are validating user input before you do an action. I often use this when submitting data with AJAX by jQuery or when I am responding from a web-service.

If so, you might want to look at my very simple validation class.

<?php

$validator = new Validator();

// Each validation rule is a property of the validator object.
$validator->username = function($value, $key, $self)
{
    if(preg_match('~\W~', $value))
    {
        return 'Your username must only contain letters and numbers';
    }
};

$validator->password = function($value, $key, $self)
{
    if(strlen($value) < 8)
    {
        return 'Your password must be at least 8 characters long';
    }
};

if( ! $validator($_POST))
{
    die(json_encode($validator->errors()));
}

// ... register user now

You can use it to validate any data - as long as it's in array form. Not just $_POST/$_GET arrays.

于 2012-05-22T21:11:30.027 に答える
1

We have created and used number of different frameworks. Form handling is usually an essential part of the creation of web applications. So, to answer your question about error handling, I would suggest looking at the question more widely.

Clearly, for anything to be validated, you need input data some sort of and definition of the input data. Next, are you having one form or do you plan to have centralized validation for more than one form. If, so, creating common validator object makes sense.

class validator {}

Okay, so, for validator to work nicely, it must know what to validate and how. So, here we step back to question of how you create forms - are those dynamic, based on Models, or are they plain html. If form is based on Model, it usually has all fields defined and usually most validation rules are present already on the model level. In that case, it makes sense to teach your validator to learn fields from model e.g.

function setModel($model){}
function getFields(){ -- iterates through $model fields}

alternatively, if you don't use models and form is plain html, the simple array of fields and validators makes most sense:

$fields = array(
    "name" => array("notNull"),
    "age" => array("notNull", array("size", 13, 99))
);

above approach lets you defined validators (one or more), and each validator may contain extra params. in this case, your validator would look like:

function validate($data, $fields){
    $this->valid = true;
    $this->data = $data;
    foreach ($fields as $field_name => $validators){
        foreach ($validators as $v){
            $params = array($field_name, isset($data[$field_name])?$data[$field_name]:null);
            if (is_array($v)){
                $m = "_" . $v[0];
                unset($v[0]);
                $params = array_merge($params, $v);
            } else {
                $m = "_" . $v;
            }
            if (method_exists($this, $m)){
                call_user_func_array(array($this, $m), $params);
            }
        }
    }
    if (!empty($this->errors)){
        $this->valid = false;
    }
    return $this->valid;
}

cool thing is that you can add your next validators as new methods to the validator class in following way:

function _notNull($field_name, $value){
    if (!$value){
        $this->errors[$field_name][] = "Must be filled";
    }   
}

function _size($field_name, $value, $min = null, $max = null){
    if ($value < $min && $min){
        $this->errors[$field_name][] = "Must be at least $min";
    } else if ($value > $max && $max){
        $this->errors[$field_name][] = "Must be at most $max";
    }   
}

so, then, using this approach you would have validator class which can be easily extended, you can have multiple parameters to the validators, and validators may use regex/filter or any other method of validating the fields. finally, $this->errors array will contain associative array with fields and the errors. Moreover, only one error per field, not to confuse the user. and obviously you can use just array or model based on the environment in which the validation will take place.

于 2012-05-26T08:43:54.953 に答える
0

以下に、一般的な例外の使用方法(状況に固有ではない)と、さらに詳細な例外の使用方法(まだ例外を使用している)を示す例を示します。これらの最初の2つの例では、一度に1つのエラーを処理します。私が提供した3番目の例は、複数のエラーと例外を処理する方法の例を示しています。

説明のほとんどはコードのコメントにあるので、それを徹底的に調べてください:)

一般的な例外処理

<?php
//Define some variables to work with
$var  = false;
$var2 = false;

try { //Outer try
    echo 'Do something here!<br />';

    try { //Inner try
        if($var !== true) { //Fail
            throw new Exception('$var is not true',123); //Exception is thrown (caught 2 lines down)
        }
    } catch (Exception $e) { //Exception caught here
        echo 'InnerError# '.$e->getCode().': '.$e->getMessage().'<br />'; //Exception handled (in this case printed to screen)
    }

    //Code is continuing here even after the exception was thrown
    echo 'Do something else here!<br />';

    if($var2 !== true) { //Fail
        throw new Exception('$var2 is not true', 456); //Exception is thrown (caught 6 lines down)
    }

    //Code fails to run as the exception above has been thrown and jumps straight into the below catch
    echo 'Do the third thing here!<br />';

} catch (Exception $e) { //Exception caught here
    echo 'Error # '.$e->getCode().': '.$e->getMessage().' on line '.$e->getLine().' in '.$e->getFile().'<br />'; //Exception handled (in this case printed to screen)
}

//Code is continuting here even after both of the exceptions
echo 'Do even more stuff here!<br />';
?>

標準の例外クラスコンストラクター:

public __construct ([ string $message = "" [, int $code = 0 [, Exception $previous = NULL ]]] )

カスタム例外

さて、これをあなたの例に関連付けると、これらの線に沿って何かをすることができます:

<?php
class customException extends Exception { //Create a custom exception handler that allows you to pass more arguments in the constructor             
    public function __construct($errorString, $errorNumber, $errorFile, $errorLine) {
        $this->message = $errorString; //Using the Exception class to store our information
        $this->code = $errorNumber;
        $this->file = $errorFile;
        $this->line = $errorLine;
    }
}

function err2Exception($errNo, $errStr, $errFile, $errLine) {  //This function converts the error into an exception
    throw new customException($errStr, $errNo, $errFile, $errLine); //Throw the customException
}

set_error_handler('err2Exception'); //Set the error handler to the above function

try {
    assert(1==2); //This fails, calls the function err2Exception with the correct arguments, throws the error and is caught below
} catch (Exception $e) { //Error caught as an Exception here
    //Echo out the details (or log them, or whatever you want to do with them)
    echo 'Error String: '.$e->getMessage().'<br />';
    echo 'Error Number: '.$e->getCode().'<br />';
    echo 'File containing error: '.$e->getFile().'<br />';
    echo 'Line with error: '.$e->getLine().'<br />';

}
?>

http://php.net/manual/en/function.set-error-handler.php

上記のコードの出力:

エラー文字列:assert():アサーションに失敗しました

エラー番号:2

エラーを含むファイル:18

エラーのある行:/var/www/test2.php

最初のコード例のネストtry/ステートメントの概念を、カスタムエラー処理のこの2番目の例に適用できます。catch

複数のエラー/例外の処理

<?php
class errorLogger  { //create an errorLogger class
    private $errors; //Stores all errors

    public function addError($errCode, $errMsg, $errFile = null, $errLine = null) { //Manually add an error
        $this->errors[] = array( //Add to the error list
            'code' => $errCode,
            'message' => $errMsg,
            'file' => $errFile,
            'line' => $errLine
        );
    }

    public function addException($exception) { //Add an exception to the error list
        $this->errors[] = array( //Add to the error list
            'code' => $exception->getCode(),
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine()
        );
    }

    public function getErrors() { //Return all of the errors
        return $this->errors;
    }

    public function numErrors() { //Return the number of errors
        return count($this->errors);
    }
}

$el = new errorLogger(); //New errorLogger
set_error_handler(array($el, 'addError')); //Set the default error handler as our errorLoggers addError method
set_exception_handler(array($el, 'addException')); //Set the default exception handler as our errorLoggers addException method

if(!is_numeric('a')) //Will fail
    $el->addError('Invalid number', 1); //Adds a new error

if(($name = 'Dave') !== 'Fred') //Will fail
    $el->addError('Invalid name ('.$name.')', 2, 'test.php', 40); //Adds another error

assert(1==2); //Something random that fails (non fatal) also adds to the errorLogger

try {
    if('Cats' !== 'Dogs') //Will fail
        throw new Exception('Cats are not Dogs', 14); //Throws an exception
} catch (Exception $ex) { //Exception caught
    $el->addException($ex); //Adds exception to the errorLogger
}

trigger_error('Big bad wolf blew the house down!'); //Manually trigger an error

//throw new Exception('Random exception', 123); //Throw an exception that isn't caught by any try/catch statement
                                                //(this is also added to the errorLogger, but any code under this is not run if it is uncommented as it isn't in a try/catch block)

//Prints out some 
echo '<pre>'.PHP_EOL;
echo 'There are '.$el->numErrors().' errors:'.PHP_EOL; //Get the number of errors

print_r($el->getErrors());

echo '</pre>'.PHP_EOL;
?>

もちろんerrorLogger、ニーズに合わせてクラスを変更および適合させることができます。

上記のコードの出力:

5つのエラーがあります:

配列 (

[0] => Array
    (
        [code] => Invalid number
        [message] => 1
        [file] => 
        [line] => 
    )

[1] => Array
    (
        [code] => Invalid name (Dave)
        [message] => 2
        [file] => test.php
        [line] => 10
    )

[2] => Array
    (
        [code] => 2
        [message] => assert(): Assertion failed
        [file] => /var/www/test.php
        [line] => 42
    )

[3] => Array
    (
        [code] => 14
        [message] => Cats are not Dogs
        [file] => /var/www/test.php
        [line] => 46
    )

[4] => Array
    (
        [code] => 1024
        [message] => Big bad wolf blew the house down!
        [file] => /var/www/test.php
        [line] => 51
    )

)。

上記のコードを使用すると、次のことができます。

  • 例外をスローし、それらをに追加しますerrorLogger
  • 通常はエラーが表示されるランダム関数からの未処理の状況を処理します
  • 独自のエラーを手動で追加する
  • トリガーエラー(http://uk3.php.net/trigger_error

その後、すべてのエラーを表示/ログ/記録することができます。

注意:上記のコードはすべて直接コピーして貼り付けることができ、実験することができます。

于 2012-05-22T19:57:53.313 に答える
0

To help you relax, no matter what you do, you’re going to end up with the same basic procedural loop as you describe. You can slightly remove the nesting (see below) but not by much.

For validation, you need a procedural flow, which is what you have. There may be subtle variations (such as you may do the combinatory validators even if some other fields are wrong) but this is the procedural flow.

1.  Loop through all fields, and validate them; store errors if any
2.  If (no errors) {
3.      loop though all multiple combinations and validate them, store errors if any
4.  }
5.  If (no errors) {
6.     do action, store errors if any
7.  }
8.  If (no errors) {
9.     Report back success
10. } else {
11.    Report back problems
12. }

To make it more efficient from a coding perspective you can follow almost any of the answers there - add “field” classes, and loop through those, or an array of validation conditions and loop though those. You may add “validators classes” (but you’ll need two types – one type attached to the field, one type attached to the form) and you can use exceptions to throw you back to the above loop – but that base procedural loop you're concerned about will never change.


But to answer more appropriately the way I work (on larger projects) is to have a:

  1. “Field Validator” class that validates the type of field, length, compulsory etc.
  2. “Form Validator” class that validates specific combinations of data etc.
  3. “Form” class that controls the actions of the form. This is also useful that different types of form class can link to a database (similar to VB/C# .Net) and pull in field validation from the field types and has standard “edit”, “add” and “delete” functionality.
  4. “Field” class that controls the actions of the field. Field can also be linked to a DB, linked to other fields etc.

The Form will validate with exactly the same procedural structure, except it loops through the field objects (not raw fields), and spits exceptions back to the loop that stores the errors against the form (as suggested by Darragh). With classes it’s just more structured and easier to add/edit etc.

For quick projects (one page forms) without the overhang of the framework, I’d just use your code with specific validations. (That’s personal choice – others will say you should always use the framework even for small projects; both options are valid and not a discussion for here. Sometimes I'd just use a mid-level option. Whichever suits the project.)

But no matter what – the base procedural loop is the same. Nothing you an do about it as that's what's required.

于 2012-05-29T00:44:01.847 に答える