技术开发 频道

PHP 开发经典教程 (Part 12):程序缺陷排除

【IT168 专稿】第一部分

保护你的代码

即使是最好的开发人员有时也会犯错误。那就是为什么大部分编程语言(包括PHP)提供了内建的捕获错误然后采取补救措施的能力。这个措施可以和显示一条错误消息一样简单,或者和向站点管理员发送一封具有完整的堆栈路径的电子邮件一样复杂。

为了更容易地执行这些,PHP提供了完整特征错误处理API,该API可用于捕获和解决错误。除了决定用户可见哪种类型的错误之外,你也可以使用你自己自定义(通常来讲,更有创造性的)函数来代替内建的错误处理机制。如果你正在使用PHP 5,那么你就得到一个意外收获:一种全新的异常模型,该模型允许你将你的代码以类似于Java的try-catch()块结构封装起来以获得更加有效的错误处理。

在PHP 101的本次版本中,我将会讨论所有的这些事情,给你一个如何在你的PHP应用中增加错误处理的快速课程。继续阅读吧,因为这是一个非常酷的玩意!

在我们进入如何编写错误处理器的本质之前,你需要知道一些理论知识。

通常情况下,当一个PHP脚本遇到一个错误时,它显示一条指示错误原因的消息然后可能也(取决于错误的严重程度)终止脚本的执行。现在,虽然这种行为在开发阶段是可以接受的,但一旦PHP应用程序被分发到真实的用户时它就不能再继续发生了。在“实况”情况下,显示模糊的错误消息(通常情况下对非技术用户是不能理解的)是不够专业的;拦截这些错误然后或者解决错误(如果解决方法可行的话)或者以一种易于理解的错误消息通知用户(如果解决方法不可行的话)而更加专业。

PHP中有三种基本的运行时间错误类型:

1. 注意:当执行一个脚本时,这些是PHP遇到的微不足道的、非关键性的错误(比如,访问一个仍没有被定义的变量)。默认情况下,这类错误根本不会被展示给用户(但是,正如你将看到的,你可以改变这种默认行为)。

2.警告:这是更严重的错误(比如,试图包含一个不存在的文件)。默认情况下,这些错误被显示给用户,但它们不会导致脚本终止执行。

3. 致命错误:这类错误是严重的错误(比如,例示一个不存在的类的对象或者调用一个不存在的函数)。这些错误导致脚本立即终止执行,然后PHP的默认行为就是当它们发生时将其显示给用户。

应了解,PHP脚本中的语法错误(比如,丢失括号或者分号)被作为一个致命错误且导致脚本的终止运行。那就是为什么如果你在你的PHP语句结尾忘记分号,那么PHP将会拒绝执行你的脚本直到你修正了这个错误的原因。

PHP错误可以由Zend引擎、PHP内建函数或者用户自定义函数来产生。它们可能发生在开始、解析、编译或者运行时。在PHP内部,这些变化可以由12种不同的错误类型来表示(PHP 5中的),而且你可以在http://www.php.net/manual/en/ref.errorfunc.php中阅读关于它们的信息。指定的常量(比如E_NOTICE和E_USER_ERROR)提供了一种方便的方法以涉及不同的错误类型。
这里是一个快捷的提示:大部分时间里,你将会担心运行时间错误(E_NOTICE、E_WARNING和E_ERROR)和用户触发错误((E_USER_NOTICE、E_USER_WARNING和E_USER_ERROR)。在调试阶段期间,你可以使用捷径E_ALL类型来看你的脚本所产生的所有的致命和非致命错误,而且在PHP 5中,你也可能打算使用新的E_STRICT错误类型来查看影响你的代码前向兼容性的错误。

早期告警

将理论放到一旁,让我们现在将其应用到一些例子中。考虑下面的代码段:
<?php // initialize the $string variable $string = 'a string'; // explode() a string // this will generate a warning or E_WARNING because the number of arguments to explode() is incorrect explode($string); ?>
如果你运行这个脚本,那么你将会得到一个非致命的错误(E_WARNING),该错误意味着如果在对explode()调用的后面还有语句,那么这些语句会得到执行。你亲自试试然后看看!

为了产生一个致命的错误,你需要投入一些更多的工作。请看下面:
<?php // call a non-existent function // this will generate a fatal error (E_ERROR) callMeJoe(); ?>
这里,对不存在的函数的调用绊到了PHP的警报线然后产生了一个致命错误,该错误立即停止了脚本的执行。

现在,更有趣一点了吧。你可以通过使用内建的PHP函数error_reporting()来控制向用户显示哪条错误消息。该函数接受一个指定的常量然后告诉脚本只报告与该类型匹配的错误。为了看看这方面的实际操作,考虑下面的为“隐藏”非致命错误而对之前脚本之一的改写:
<?php // report only fatal errors error_reporting(E_ERROR); // initialize the $string variable $string = 'string'; // attempt to explode() a string // this will not generate a warning because only fatal errors are reported explode($string); ?>
在该例子中,当脚本执行时,即使对函数explode()的调用所包含的参数比它应该需要的参数少了一个,但也没有告警产生。
你也可以使用类似的技术来关闭致命错误的显示:
<?php // report no fatal errors error_reporting(~E_ERROR); // call a non-existent function callMeJoe(); ?>
然而,请记住,错误不被报告并不意味着它没有发生。即使上述脚本不会显示一个可见的错误消息,但脚本执行将会在错误点处停止而且该点随后的语句将不被执行。error_reporting()函数给了你显示哪个错误的控制权;但它不能阻止错误本身的发生。

请注意,在php.ini内具有进一步的设置以用于生产一线。你可以(且应该)关闭display_errors、指定error_log文件和打开log_errors。

也请注意,上面所使用的隐藏错误消息的方法,虽然极其简单,但不被推荐做一般性的使用。不管错误类型,捕获所有的错误而且忽略它们是一种拙劣的编程习惯;提前预测可能的错误然后编写监视这些错误的防御性代码然后适当的处理这些错误要比上述方法好的多(而且更专业)。这会防止你的用户在错误发生时发现他们自己看到的是一个莫名其妙的空白页面。

构造你自己的错误处理器

请记住这个,让我们来讨论一些关于改变错误被处理的方式。考虑一个典型的PHP错误消息:它列出了错误类型、一条描述性消息和产生错误的脚本的名字。大部分情况下,这是非常足够的,但如果你的老板是一个要求苛刻的客户而且要求必须有一种“更好的方法”时该怎么办呢?

嗯,这里有一种方法。它就是一个被称为set_error_handler()的小函数而且它允许你将所有的PHP错误转移给一个你已经定义的自定义函数而不是将其发送到默认的处理器。该自定义函数必须能够最少接受两个强制性参数(错误类型及其对应的描述性消息)一直到三个另外的参数(错误发生地的文件名称和行号以及错误发生时的变量空间转储)。

下列例子或许可以使这点更清楚些:
<?php // define a custom error handler set_error_handler('oops'); // initialize the $string variable $string = 'a string'; // explode() a string // this will generate a warning because the number of arguments to explode() is incorrect // the error will be caught by the custom error handler explode($string); // custom error handler function oops($type, $msg, $file, $line, $context) { echo "<h1>Error!</h1>"; echo "An error occurred while executing this script. Please contact the <a href=mailto:webmaster@somedomain.com>webmaster</a> to report this error."; echo "<p />"; echo "Here is the information provided by the script:"; echo "<hr><pre>"; echo "Error code: $type<br />"; echo "Error message: $msg<br />"; echo "Script name and line number of error: $file:$line<br />"; $variable_state = array_pop($context); echo "Variable state when error occurred: "; print_r($variable_state); echo "</pre><hr>"; } ?>
set_error_handler()函数告诉脚本所有的错误都将被路由到我的用户自定义的oops()函数。该函数被建立以接受五个参数:错误类型、消息、文件名称、行号以及包含关于错误发生的上下文(包括服务器和平台以及脚本信息)的许多信息的数组。上下文数组的最终元素包含有错变量的当前值。然后这些参数被用于创建一个比PHP标准的单行错误消息更友好且信息更丰富的错误提示页面。

你可以使用此自定义的错误处理器在错误类型的基础上来改变用户所看到的错误消息。请看下一个例子,该例子演示了该技术的使用:
<?php // define a custom error handler set_error_handler('oops'); // initialize $string variable $string = 'a string'; // this will generate a warning explode($string); // custom error handler function oops($type, $msg, $file, $line, $context) { switch ($type) { // notices case E_NOTICE: // do nothing break; // warnings case E_WARNING: // report error print "Non-fatal error on line $line of $file: $msg <br />"; break; // other default: print "Error of type $type on line $line of $file: $msg <br />"; break; } } ?>
请注意,某些错误类型不能使用该方法处理。举例而言,一条致命的E_ERROR将阻止PHP脚本的继续执行,因此,它永远无法达到用户自定义的错误处理机制。访问http://www.php.net/set-error-handler以获得更多的关于这点的信息。PHP (第十二章): 程序缺陷排除- 第二部分-

扣动扳机

到目前为止,我们一直在讨论处理由PHP自身所产生的错误,但为什么要停留在那里呢?PHP也允许你使用其内建的错误处理系统来生成你自己自定义的错误。

这是通过一个名为trigger_error()的函数来完成的,该函数允许你生成为用户所保留的三种错误类型中的任意一个:E_USER_NOTICE、E_USER_WARNING和E_USER_ERROR。当这些错误被触发时,PHP内建的处理器将会自动唤醒以处理这些错误。
<?php // function to test a number // generates E_USER_WARNING if number is a float // generates E_USER_ERROR is number is negative function testNumber($num) { // float // trigger a warning if (is_float($num)) { trigger_error("Number $num is not an integer", E_USER_WARNING); } // negative // trigger a fatal error if ($num < 0) { trigger_error("Number $num is negative", E_USER_ERROR); } } // test the function with different values testNumber(100); testNumber(5.6); testNumber(-8); ?>
如果你打算用一个自定义的错误处理器来处理你自定义的错误……,嗯,你只是很难满足的,不是吗?请看下这个例子,该例子重新编写了之前的脚本以使用用户自定义的错误处理器:
<?php // function to test a number // generates E_USER_WARNING if number is a float // generates E_USER_ERROR is number is negative function testNumber($num) { // float // trigger a warning if (is_float($num)) { trigger_error("Number $num is not an integer", E_USER_WARNING); } // negative // trigger a fatal error if ($num < 0) { trigger_error("Number $num is negative", E_USER_ERROR); } } // custom error handler function myErrorHandler($type, $msg, $file, $line, $context) { switch ($type) { // warnings case E_USER_WARNING: // report error print "Non-fatal error on line $line of $file: $msg <br />"; break; // fatals case E_USER_ERROR: // report error and die() die("Fatal error on line $line of $file: $msg <br />"); break; // notices default: // do nothing break; } } // set the name of the custom handler set_error_handler('myErrorHandler'); // test the function with different values testNumber(100); testNumber(5.6); testNumber(-8); ?>
 请注意,如果用户产生致命错误发生的话,那么调用die()函数是自定义的处理器的责任(PHP不会自动做这个的)。
你也可以使用同样的方法来处理异常。请向下滚动鼠标,让我向你展示如何去做。

捕获

如果你正在使用PHP 5,那么除了目前为止所讨论的关于新的异常模型技术(异常是另外一种错误)之外你也有其他的选择。异常是PHP中新出现的技术(虽然它们已经在诸如Java和Python语言中多年了)而且它们正在引起人们大量的激动。

在基于异常的方法中,程序代码被try()块结构包围起来然后该代码所产生的异常被catch()块结构“捕获”和解决。多个catch()块是可能的,其中每一个处理不同的错误类型;这就允许开发人员捕获不同类型的错误而且执行适当的异常处理。

下面是一个典型的try-catch()块看上去的样子:
try { execute this block } catch (exception type 1) { execute this block to resolve exception type 1 } catch (exception type 2) { execute this block to resolve exception type 2 }
诸如此类……

当PHP遇到被包含在try-catch()块内部的代码时,它首先试图执行try()块内部的代码。如果该代码被处理时无异常产生,那么控制就转移到try-catch()块之后的代码行。然而,如果在运行try()块内部的代码时产生了异常,那么PHP在发生异常的地方停止该try()块的执行然后检查每个catch()块以查看是否有一个该异常的处理器。如果找到一个处理器,那么在适当的catch()块内部的代码得到执行;如果没有找到,那么就产生一个致命的错误。使用异常以一种良好的方式来处理该致命错误甚至也是可能的;请访问http://www.php.net/set-exception-handler以获得更多的关于这方面的内容:
异常本身通过PHP的throw语句来产生。Throw语句需要被传递一个描述性消息和一个可选的错误代码。当异常被产生时,该描述和代码将会对异常处理器变得可用。

想要看看这是如何工作的吗?下面是一个例子:
<?php // PHP 5 error_reporting(0); // try this code try { $file = 'somefile.txt'; // open file if (!$fh = fopen($file, 'r')) { throw new Exception('Could not open file!'); } // read file contents if (!$data = fread($fh, filesize($file))) { throw new Exception('Could not read file!'); } // close file fclose($fh); // print file contents echo $data; } // catch errors if any catch (Exception $e) { print 'Something bad just happened...'; } ?>
如果文件不存在或不可读,那么throw语句将会产生一个异常(基本上,PHP内建异常对象的一个实例)然后为其传递一个描述错误的消息。当这样的异常被产生时,程序控制转到第一个catch()块。如果该catch()块可以处理该异常类型,那么该catch()块内部的代码得到执行。如果第一个catch()块不能处理所产生的异常,那么程序控制就转移到下一个catch()块中。

不要在该点上对“异常类型”过多担心(所有的都会被简单介绍)。目前,所有你需要知道的就是上面的catch()块将会捕获所有的异常而无论其类型。

现在,之前的代码清单中有一个问题。虽然catch()块将会捕获异常然后打印一条消息,但是它不能显示throw语句所发送的异常的描述性消息。为了访问该消息和一些其他有趣的信息片段,有必要使用一些Exception对象内建的方法。请看之前脚本的修订版,其对此做了解释说明:
<?php // PHP 5 error_reporting(0); // try this code try { $file = 'somefile.txt'; // open file if (!$fh = fopen($file, 'r')) { throw new Exception('Could not open file!', 12); } // read file contents if (!$data = fread($fh, filesize($file))) { throw new Exception('Could not read file!', 9); } // close file fclose($fh); // print file contents echo $data; } // catch errors if any catch (Exception $e) { print '<h2>Exception</h2>'; print 'Error message: ' . $e->getMessage() . '<br />'; print 'Error code: ' . $e->getCode() . '<br />'; print 'File and line: ' . $e->getFile() . '(' . $e->getLine() . ')<br />'; print 'Trace: ' . $e->getTraceAsString() . '<br />'; } ?>
当你运行该脚本时,你会看到异常处理器所产生的消息包含如下内容:
? throw语句发送的描述性数据,
? 错误代码(也是被throw语句发送的)
? 异常发生地的文件名称和行号,以及
? 以类层次结构指示异常过程的堆栈跟踪信息,如果有的话。
该数据是通过在catch()块内分别调用Exception对象的getMessage()、getCode()、getFile()、getLine()和getTraceAsString()方法而产生的。

添加一些类

你可以通过子类化的Exception对象然后使用多个catch()块以不同的方法来处理不同的异常。下面的例子是对这点的一个简单解释:
<?php // PHP 5 // sub-class the Exception class class NegativeNumException extends Exception {} class OutOfRangeException extends Exception {} class FloatException extends Exception {} // function to test a number function testNumber($num) { // float // trigger an exception if (is_float($num)) { throw new FloatException($num); } // negative // trigger an exception if ($num < 0) { throw new NegativeNumException($num); } // out of range // trigger an exception if ($num > 1000 || $num < 100) { throw new OutOfRangeException($num); } } // try this code try { testNumber(-19); } // catch errors, if any catch (NegativeNumException $e) { print 'A negative number was provided ('.$e->getMessage().'). Please provide a positive integer between 100 and 1000.<br />'; } catch (OutOfRangeException $e) { print 'The number provided is out of range ('.$e->getMessage().'). Please provide a positive integer between 100 and 1000.<br />'; } catch (FloatException $e) { print 'The number provided is not an integer ('.$e->getMessage().'). Please provide a positive integer between 100 and 1000.<br />'; } catch (Exception $e) { print 'Error message: ' . $e->getMessage() . '<br />'; print 'Error code: ' . $e->getCode() . '<br />'; print 'File and line: ' . $e->getFile() . '(' . $e->getLine() . ')<br />'; print 'Trace: ' . $e->getTraceAsString() . '<br />'; } ?>
在该例子中,我已经从基本的对象创建了三种新的Exception子类,每个可能的错误一个子类。接着,我为每个异常类型建立了catch()块,然后编写了特定于每种类型的异常处理代码。根据发生哪类异常(你可以向testNumber()函数发送不同的值来产生不同的异常),调用适当的catch()块然后打印不同的错误消息。

请注意,因为PHP会一直使用与异常类型匹配的第一个catch()块,而且因为Exception类匹配所有的异常,所以catch()块必须按照最精确的catch()块优先的顺序进行排列。这个在上述例子中已经实现,上述例子中,catch()块出现在列表的最后。

下面是另外一个例子,该例子解释说明了一个更加有用的应用程序(在用户验证的类中使用异常模型以提供易于理解的错误处理)。请看:
<?php // PHP 5 // class definition class userAuth { // define properties private $username; private $passwd; private $passwdFile; // constructor // must be passed username and non-encrypted password public function __construct($username, $password) { $this->username = $username; $this->passwd = $password; } // set .htaccess-style file to check for passwords public function setPasswdFile($file) { $this->passwdFile = $file; } // perform password verification public function authenticateUser() { // check that the file exists if (!file_exists($this->passwdFile)) { throw new FileException("Password file cannot be found: " . $this->passwdFile); } // check that the file is readable if (!is_readable($this->passwdFile)) { throw new FileException("Unable to read password file: ". $this->passwdFile); } // read file $data = file($this->passwdFile); // iterate through file foreach ($data as $line) { $arr = explode(":", $line); // if username matches, test password if ($arr[0] == $this->username) { // get salt and crypt(), assuming encryption $salt = substr($arr[1], 0, 2); // if match, user/pass combination is correct if ($arr[1] == crypt($this->passwd, $salt)) { echo "User was authenticated"; // do some other stuff } // otherwise return exception else { throw new AuthException("Incorrect password"); break; } } else { // could not find a username match // return exception throw new AuthException("No such user"); } } } // end class definition } // subclass exceptions class FileException extends Exception {}; class AuthException extends Exception {}; // try the code try { // create instance $ua = new userAuth("joe", "secret"); // set password file $ua->setPasswdFile("password.txt"); // perform authentication $ua->authenticateUser(); } // catch authentication failures, if any catch (FileException $e) { // print file errors print "A file error occurred. ".$e->getMessage(); } catch (AuthException $e) { // an authentication error occurred print "An authentication error occurred. ".$e->getMessage(); // more normally, redirect to new page on auth errors, e.g. // header ('Location: login_fail.php'); } catch (Exception $e) { print "An unknown error occurred"; } ?>
这里,根据错误类型,FileException()或者AuthException()将会被产生(被相应的catch()块处理)。我们注意到,异常处理框架理解和扩展是多么的容易。准确地讲,正是由于它的易于使用和扩展帮助新的PHP 5模型的评价超过了早期的更基本的处理应用错误的技术。
恩,那就是目前的内容。为了更多的PHP 101,请尽快返回!
0
相关文章