【问题标题】:What is the cleanest way to handle validation errors with PHP?用 PHP 处理验证错误的最简洁方法是什么?
【发布时间】:2012-05-26 13:52:48
【问题描述】:

只要我一直在开发 Web 应用程序,我就一直在寻找更好的方法来处理验证。经常需要捕获多个验证错误,所以我想知道是否有比以下更好的方法。

现在我在自己开发的框架中有一个assert 方法。该方法的一个示例是这样的:

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

如果第一个参数中的条件为假,则第二个参数中的错误消息被添加到一个$errors 数组中(该数组被包装在一个类中(由下面的$eh 引用),该类提供了诸如@ 之类的便利函数987654325@)。

这种方法有效,但在实践中很混乱。考虑这段代码:

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();
}

这是相当普遍的事情。我想在继续之前检查两个条件,然后如果通过,在最终做我想做的事情之前检查第三个条件。如您所见,此代码中的大多数行都与验证有关。在实际应用中,会有更多的验证,可能还有更多的嵌套 if 语句。

还有比这更好的处理验证的结构吗?如果有框架可以更优雅地处理这个问题,它们是什么以及它们是如何实现的?多个嵌套的 if 语句似乎是解决问题的“蛮力”解决方案。

请注意,我知道将一些常见的验证函数包装在一个类中可能是个好主意,这样我就可以通过调用这些函数来检查长度、字符串格式等。我要问的是一种更简洁的代码结构方法,而不是我实际检查错误的方式。

谢谢!

【问题讨论】:

    标签: php validation error-handling


    【解决方案1】:

    下面我编写了一个示例,向您展示了如何使用一般异常(非特定于您的情况)并进一步了解更具体的内容(仍然使用异常)。前两个示例将一次处理 1 个错误。我提供的第三个示例给出了如何处理多个错误和异常的示例。

    大部分解释都在代码的cmets中,所以一定要仔细看一遍:)

    一般异常处理

    <?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/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)

    然后您可以在以后显示/记录/任何所有错误。

    注意:以上所有代码都可以直接复制粘贴,给你一些实验的东西

    【讨论】:

    • 异常不会停止代码的执行吗?如果我不想进行多次验证并返回所有错误,那么在第一个错误上抛出异常不会捕获其余的潜在错误。这是正确的吗?
    • * 咳嗽 * 是的,哈哈。我忽略了您帖子中的“多个”位。无论如何,我现在已经在我的答案中添加了一些。
    【解决方案2】:

    请查看Respect\Validation。这是一个为此目的而建的库。它可以非常轻松地处理多个规则并使用异常来处理错误。这是一个简单的示例:

    <?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
    

    现在使用异常:

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

    在上面的示例中,$errors 变量将是这样的:

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

    我使用“foo # bar”打破了两个先前声明的规则:它有空格,它有一个非 alnum 字符。对于每条未通过的规则,都会返回一条消息。由于“长度”是可以的,所以错误信息为空。

    documentation 包含更多示例,包括嵌套、分层规则和更好的异常处理。还为所有 30 多个内置验证器提供了广泛的示例列表。

    希望有帮助!

    【讨论】:

    • 我非常喜欢这个库,我想我会更详细地研究它的工作原理,并尝试在我自己的框架中复制它的功能。谢谢!
    • Validation 很棒,是我见过的最好的。向你致敬!
    • +1。我终于在昨天的一个快速项目中尝试了Respect\Validation。非常有用的库!
    【解决方案3】:

    为了帮助您放松,无论您做什么,最终都会得到与您描述的相同的基本程序循环。您可以稍微移除嵌套(见下文),但不能太多。

    对于验证,您需要一个程序流程,这就是您所拥有的。可能会有细微的变化(例如,即使某些其他字段错误,您也可以执行组合验证器),但这是程序流程。

    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. }
    

    从编码的角度来看,为了提高效率,您几乎可以遵循那里的任何答案——添加“字段”类,然后循环遍历这些类,或者一组验证条件并循环遍历这些类。你可以添加“验证器类”(但你需要两种类型——一种附加到字段,一种附加到表单),你可以使用异常让你回到上面的循环——但是那个基本的程序循环你'关心永远不会改变。


    但更恰当地回答我的工作方式(在较大的项目中)是:

    1. “字段验证器”类,用于验证字段类型、长度、强制等。
    2. “表单验证器”类,用于验证特定的数据组合等。
    3. “表单”类控制表单的操作。这也很有用,因为不同类型的表单类可以链接到数据库(类似于 VB/C# .Net)并从字段类型中提取字段验证,并具有标准的“编辑”、“添加”和“删除”功能。
    4. “字段”类控制字段的操作。字段也可以链接到数据库,链接到其他字段等。

    表单将使用完全相同的程序结构进行验证,除了它循环遍历字段对象(不是原始字段),并将异常返回到存储表单错误的循环中(如 Darragh 所建议的)。有了类,它就更加结构化,更容易添加/编辑等。

    对于没有框架悬垂的快速项目(一页表单),我只会使用带有特定验证的代码。 (这是个人选择 - 其他人会说即使是小型项目也应该始终使用该框架;这两个选项都是有效的,这里不讨论。有时我只使用中级选项。无论哪个适合项目。)

    但无论如何——基本的程序循环是相同的。您无需为此做任何事情,因为这是必需的。

    【讨论】:

      【解决方案4】:

      我们创建并使用了许多不同的框架。表单处理通常是创建 Web 应用程序的重要组成部分。因此,要回答您有关错误处理的问题,我建议您更广泛地研究这个问题。

      显然,对于要验证的任何内容,您都需要某种输入数据和输入数据的定义。接下来,您是否拥有一个表单,或者您是否计划对多个表单进行集中验证。如果是这样,那么创建通用验证器对象是有意义的。

      class validator {}
      

      好的,因此,要让验证器正常工作,它必须知道要验证什么以及如何验证。所以,在这里我们回到你如何创建表单的问题——那些是动态的,基于模型,还是它们是纯 html。如果表单基于模型,它通常定义了所有字段,并且通常大多数验证规则已经存在于模型级别。在这种情况下,教你的验证者从模型中学习字段是有意义的,例如

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

      或者,如果您不使用模型并且表单是纯 html,那么简单的字段和验证器数组最有意义:

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

      上述方法允许您定义验证器(一个或多个),并且每个验证器可能包含额外的参数。在这种情况下,您的验证器将如下所示:

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

      很酷的是,您可以通过以下方式将下一个验证器作为新方法添加到验证器类:

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

      因此,使用这种方法,您将拥有可以轻松扩展的验证器类,您可以为验证器提供多个参数,并且验证器可以使用正则表达式/过滤器或任何其他验证字段的方法。最后,$this-&gt;errors array 将包含带有字段和错误的关联数组。而且,每个字段只有一个错误,不要混淆用户。显然你可以根据验证发生的环境使用数组或模型。

      【讨论】:

        【解决方案5】:

        抛出异常怎么样?您可以使用 try /catch 块显式捕获异常,和/或使用 set_exception_handler() 捕获它们

        在 PHP 中定义了许多有用的异常类型,如果您需要异常处理的粒度,可以使用它们来发挥您的优势。此外,您还可以定义自定义异常。

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

        编辑

        回答您关于其他一些框架如何解决此问题的问题 - 明智地使用异常似乎很常见。使用它们的有用之处在于,假设您有一个特定的方法,该方法执行许多可能是错误的不同验证 - 您可以在每种情况下抛出一个适当的异常,但您不必处理不同的可能异常那个方法。相反,根据您构建代码的方式,您可以允许异常冒泡到代码中更集中的位置,在那里您可以捕获并适当地处理它。

        编辑 2

        详细说明我对filter_input_array()的最后评论

        基于一个非常简单的 POSTed 用户数据示例。首先创建一个定义:

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

        然后使用一个通用的验证类(下面的类定义):

        $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.
        }
        

        FormValidator 的一个非常基本的(未经测试的)实现。

        基本用例是为请求方法调用适当的方法来过滤。这反过来检查返回的值并确定输入是否有效。

        这可能需要很多的爱 - 尤其是 filterInput 方法,因为您可能需要进行一些测试以确保您正确处理“真实”或“虚假”值。我在考虑复选框类型的值。 in_array 直接检查 false 可能不会像这里实现的那样削减它。但是您可以在定义中传递大量标志。

        我想您还可以通过对生成的 $values 数组和定义的计数来检查丢失的输入,以确保它们匹配。定义中没有的其他输入被过滤掉(你可能想检查一下,但我有理由确定这一点)。

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

        无论如何,我会说从该类中重构和测试 bejayzis,(甚至完全改变它)但希望它概述了基本思想:对于每种类型的输入,创建一个定义,然后使用一个通用的验证类来过滤并确保有效性。

        希望这会有所帮助。 filter_inputfilter_input_array 摇滚 :)

        【讨论】:

        • 我已经对此进行了调查,但当我询问时并没有真正得到很大的回应。我得到的两个回应都不赞成例外。在这里查看我的问题:stackoverflow.com/questions/10196227/…
        • 有趣。我想那里的意见各不相同——它们肯定有用途。有可能过度使用它们(就像任何东西一样),但我认为适当使用异常绝对有用。恕我直言。在我看来,这像是一种文化——其他语言似乎更喜欢它们的用法。
        • 是的。多读一些。对这个话题的看法似乎大相径庭。一方面,一些人吹捧例外是针对 exception 情况 - 当情况超出您的控制范围时。一个很好的例子是创建一个 PDO 对象——你可以将它包装在一个 try/block 中,以在连接到数据库时处理无数可能的错误。也就是说,如果你看一下 Symfony2 的 Validator 组件,它的几个类会根据参数的评估抛出 UnexpectedValueExceptionException。我会举手说这是一个品味问题。并且可能机智:)
        • 看起来 Symfony2 的文档提供了与我类似的示例(测试 $errors 数组的存在)。在我接受答案之前正在寻找更多输入。
        • 我正在考虑这个问题。最重要的想法.. 但也许你可以通过构建某种基于filter_input_array() 的通用验证器组件来完成单独检查大量表单值的一些工作。为您需要验证的每个提交操作指定一个定义数组,并将其传递给filter_input_array()。如果任何输入的验证定义失败,则该键的值将返回 false。所以你可以用in_array() 搜索false - 如果这是肯定的,你至少有一个无效的输入,你可以相应地处理吗?
        【解决方案6】:

        当您说“验证”时 - 我假设您在执行操作之前验证用户输入。当我通过 jQuery 使用 AJAX 提交数据或从 Web 服务响应时,我经常使用它。

        如果是这样,你可能想看看我的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
        

        您可以使用它来验证任何数据 - 只要它是数组形式。不仅仅是 $_POST/$_GET 数组。

        【讨论】:

          猜你喜欢
          • 2020-08-19
          • 2013-08-18
          • 1970-01-01
          • 1970-01-01
          • 2014-05-07
          • 1970-01-01
          • 2012-02-06
          • 2011-04-10
          • 2016-02-27
          相关资源
          最近更新 更多