分类:
2008-03-31 16:19:52
本文面向的读者
本文面向希望了解PHP5异常处理机制的程序员。读者需要具有一定面向对象编程和PHP基础。
简介
本文集中讨论各种错误处理,在这里你将可以看到PHP4,PHP5中的多种错误处理方式。PHP5引入了“异常机制”――一个在对象体系中进行错误处理的新机制。就像你即将看到的,“异常”提供了不少比传统的错误处理机制先进的特性。
PHP5之前的错误处理
在PHP5之前的程序错误处理多使用以下三种办法:
1. 使用trigger_error()或die()函数来生成一个脚本层次的警告(warning)或致命错误(fatal error);
2. 在类方法或函数中返回一个错误标记(如false),也可能设置一个之后可以检查的属性或全局变量(如$error),然后在适合的地方检验其值再决定是否继续执行程序(如if($error==1){});
3. 使用PEAR处理错误;
(一)使用die()或trigger_error()
你可以使用die()函数来结束程序运行。以下是一个简单的类,它尝试从一个目录中加载一个类文件。
代码列表 index.php ?>
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
var $cmdDir = "cmd_php4";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
die("Cannot find $path ");
}
require_once $path;
if (!class_exists($cmd)) {
die("class $cmd does not exist");
}
$ret = new $cmd();
if (!is_a($ret, 'Command')) {
die("$cmd is not a Command");
}
return $ret;
}
}
?>
这是一个用PHP实现“Command Pattern设计模式”的简单例子(请参看《Java与模式》)。使用这个类的程序员(客户程序员client coder)可以将一个类放到目录中(例中为cmd_php4目录)。一旦文件和其中包含的类同名,并且这个类是Command类的子类,我们的类方法将生成一个可用的Command对象。Command类中定义了一个 execute()方法用来执行找到的命令,即 getCommandObject()方法返回的对象将执行execute().
为了进一步处理异常,我们需要使用try-catch语句—包括Try语句和至少一个的catch语句。任何调用 可能抛出异常的方法的代码都应该使用try语句。Catch语句用来处理可能抛出的异常。以下显示了我们处理getCommandObject()抛出的异常的方法:
代码列表 index_php5.php 后半段?>
// PHP 5
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
} catch (Exception $e) {
print $e->getMessage();
exit();
}
?>
可以看到,通过结合使用throw关键字和try-catch语句,我们可以避免错误标记“污染”类方法返回的值。因为“异常”本身就是一种与其它任何对象不同的PHP内建的类型,不会产生混淆。
如果抛出了一个异常,try语句中的脚本将会停止执行,然后马上转向执行catch语句中的脚本。
如果异常抛出了却没有被捕捉到,就会产生一个fatal error。
可以看出来,Exception 类的结构和Pear_Error 很相似。当你的脚本中遇到一个错误,你可以建立你的异常对象:
$ex = new Exception( "Could not open $this->file" );
Exception类的构造函数将接受一个出错信息和一个错误代码。
使用throw关键字
建立一个Exception对象后你可以将对象返回,但不应该这样使用,更好的方法是用throw关键字来代替。throw用来抛出异常:
throw new Exception( "my message", 44 );
throw 将脚本的执行中止,并使相关的Exception对象对客户代码可用。
以下是改进过的getCommandObject() 方法:
(见下页代码列表)。代码中我们使用了PHP5的反射(Reflection)API来判断所给的类是否是属于Command 类型。在错误的路径下执行本脚本将会报出这样的错误:
Fatal error: Uncaught exception 'Exception' with message 'Cannot find command/xrealcommand.php' in /home/xyz/BasicException.php:10
Stack trace:
#0 /home/xyz/BasicException.php(26):
CommandManager->getCommandObject('xrealcommand')
#1 {main}
thrown in /home/xyz/BasicException.php on line 10
默认地,抛出异常导致一个fatal error。这意味着使用异常的类内建有安全机制。而仅仅使用一个错误标记,不能拥有这样的功能。处理错误标记失败只会你的脚本使用错误的值来继续执行。
Try-catch 语句
代码列表 index_php5.php?>
// PHP 5
require_once('cmd_php5/Command.php');
class CommandManager {
private $cmdDir = "cmd_php5";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
throw new Exception("Cannot find $path");
}
require_once $path;
if (!class_exists($cmd)) {
throw new Exception(
"class $cmd does not exist");
}
$class = new ReflectionClass($cmd);
if (!$class->isSubclassOf(new ReflectionClass('Command'))) {
throw new Exception("$cmd is not a Command");
}
return new $cmd();
}
}
?>
这个简单的机制可以让setError()记录下错误信息。其它代码可以通过error()来获得脚本错误的相关信息。你应该将这个功能抽取出来并放在一个最基本的类中,其它所用类都从这个类继承而来。这样可以统一处理错误,否则可能出现混乱。我就曾经见过有些程序在不同的类中使用getErrorStr()、 getError()和error()等功能相同的函数。
然而,实际开发中要让程序中的所有类都从同一个类中继承而来是很困难的,除非同时使用接口(interface)否则无法实现一些子类自身特有的功能,但那已经是PHP5的内容。就像我们将提到的,PHP5中提供了更好的解决方案。
(三)使用PEAR处理错误
你也可以使用PEAR来处理错误。当发生错误,将返回一个Pear_Error对象。后面的代码通过一个静态方法PEAR::isError()来检验这个对象。如果错误确实发生了,那么返回的Pear_Error对象将提供你需要的所有相关信息:
这里我们修改了getCommandObject()方法,使之返回一个Pear_Error对象。
代码列表 index_pear.php?>
// PHP 4
require_once("PEAR.php");
require_once('cmd_php4/Command.php');
class CommandManager {
var $cmdDir = "cmd_php4";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
return PEAR::RaiseError("Cannot find $path");
}
require_once $path;
if (!class_exists($cmd)) {
return
PEAR::RaiseError("class $cmd does not exist");
}
$ret = new $cmd();
if (!is_a($ret, 'Command')) {
return
PEAR::RaiseError("$cmd is not a Command");
}
return $ret;
}
}
?>
这意味着你可以根据环境来处理多个错误,而不会在第一个错误发生时马上停止程序的执行。
代码列表 ?>
// PHP 4
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
if (is_bool($cmd)) {
die("error getting command ");
} else {
$cmd->execute();
}
?>
或者只是记录错误:
代码列表 ?>
// PHP 4
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
if(is_bool($cmd)) {
error_log("error getting command ", 0);
}
else {
$cmd->execute();
}
?>
使用像“false”这样的错误标志的好处是直观,但是明显给出的信息量不够,我们无法得知到底是在哪一个环节上错而导致返回false。你可以再设置一个error属性,这样在产生错误后输出出错信息。
代码列表 index4.php?>
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
var $cmdDir = "cmd_php4";
var $error_str = "";
function setError($method, $msg) {
$this->error_str =
get_class($this)."::{$method}(): $msg";
}
function error() {
return $this->error_str;
}
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
$this->setError(__FUNCTION__, "Cannot find $path ");
return false;
}
require_once $path;
if (!class_exists($cmd)) {
$this->setError(__FUNCTION__, "class $cmd does not exist");
return false;
}
$ret = new $cmd();
if (!is_a($ret, 'Command')) {
$this->setError(__FUNCTION__, "$cmd is not a Command");
return false;
}
return $ret;
}
}
?>
我们再看看父类Command类,我们将它存在cmd_php4/Command.php文件中。
代码列表cmd_php4/Command.php ?>
// PHP 4
class Command {
function execute() {
die("Command::execute() is an abstract method");
}
}
?>
你可以看到,Command是PHP4中抽象类的实现,我们无法直接将其实例化,而必须先从中派生中子类然后再实例化。当我们使用PHP5后,我们可以使用更好的方式—使用abstract关键字将类和方法声明为“抽象”:
代码列表 ?>
// PHP 5
abstract class Command {
abstract function execute();
}
?>
下面是对上面的抽象类的实现,覆写了execute()方法,在其中加入真正可以执行的内容。这个类命名为realcommand,可以在cmd_php4/realcommand.php文件中找到。
代码列表 cmd_php4/realcommand.php ?>
// PHP 4
require_once 'Command.php';
class realcommand extends Command {
function execute() {
print "realcommand::execute() executing as ordered sah! ";
}
}
?>
使用这样的结构可以使代码变得很灵活。你可以在任何时候增加新的Command类,而不需要改变外围的框架。但是你不得不注意一些潜在的中止脚本执行的因素。我们需要确保类文件存在,并且在文件中该类存在,并且该类是Command的子类(就像realcommand一样)。
在例子中,如果我们尝试寻找类的操作失败,脚本执行将会中止,这体现了代码的安全性。但这段代码不灵活,没有足够的弹性。极端的反映是类方法只能进行积极正面的操作,它只负责找出和实例化一个Command对象。它无法处理更大范围内脚本执行的错误(当然它也不应该负责处理错误,如果我们给某个类方法加上太多与周边代码的关联,那么这个类的重用将会变得困难,不易扩展)。
尽管使用die()避免了在getCommandObject()方法中嵌入脚本逻辑的危险,它对于对于错误的反应显得过于激烈—-马上中止程序。事实上有时候我们并不希望在找不到想要的类文件时就马上停止执行程序,也许我们有一个默认的命令让程序继续执行。
set_error_handler()接受一个函数名作为参数。如果触发了一个错误,参数中的这个函数会被调用来处理错误。函数需要传入四个参数:错误标志,错误信息,出错文件,出错处的行数。你也可以将一组数组传递给set_error_handler()。数组中的第一个元素必须是错误处理器将调用的对象,第二个元素是错误处理函数的名称。
可以看出,我们的错误处理器相当简单简陋,还可以改进。然而尽管你可以在错误处理器添加某些功能,如记录出错信息,输出debug数据等,这仍然是一个过于粗糙的错误处理途径。你的选择仅限于已经考虑到的出错情况。例如捕捉一个E_USER_ERROR错误,如果你愿意的话可以不中止脚本的执行(不使用exit()和die()),但如果这样做的话,可能会引起一些很微妙的bug,本来应该中止的程序却继续执行了。
(二) 返回错误标记
脚本层次的错误处理比较粗糙但很有用。尽管如此,我们有时需要更大的灵活性。我们可以使用返回错误标识的办法来告诉客户代码“错误发生了!”。这将程序是否继续,如何继续的责任交给客户代码来决定。
这里我们改进了前面的例子来返回一个脚本执行出错的标志(false是一个常用的不错的选择)。
代码列表 index3.php ?>
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
var $cmdDir = "cmd_php4";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
return false;
}
require_once $path;
if (!class_exists($cmd)) {
return false;
}
$ret = new $cmd();
if (!is_a($ret, 'Command')) {
return false;
}
return $ret;
}
}
?>
我们或许可以通过trigger_error()生成一个用户警告来代替,使程序更具有灵活性。
代码列表 Index2.php ?>
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
var $cmdDir = "cmd_php4";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
trigger_error("Cannot find $path", E_USER_ERROR);
}
require_once $path;
if (!class_exists($cmd)) {
trigger_error("class $cmd does not exist", E_USER_ERROR);
}
$ret = new $cmd();
if (!is_a($ret, 'Command')) {
trigger_error("$cmd is not a Command", E_USER_ERROR);
}
return $ret;
}
}
?>
如果你使用trigger_error()函数来替代die(),你的代码在处理错误上会更具优势,对于客户程序员来说更易于处理错误。trigger_error()接受一个错误信息和一个常量作为参数。常量为:
常量
含义
E_USER_ERROR A fatal error
E_USER_WARNING
A non-fatal error
E_USER_NOTICE
A report that may not represent an error
你可以设计一个错误处理器,然后再定义一个处理器选择函数set_error_handler()来使用这个错误处理器。
代码列表 Index2.php 后半段?>
// PHP 4
function cmdErrorHandler($errnum, $errmsg, $file, $lineno) {
if($errnum == E_USER_ERROR) {
print "error: $errmsg ";
print "file: $file ";
print "line: $lineno ";
exit();
}
}
$handler = set_error_handler('cmdErrorHandler');
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
?>