php_study日记:异常处理
2011年12月21日
##异常处理
尽管我们写出了整洁、设计良好的到吗,但在程序运行中仍可能有各种各样的意外出现,当一个程序在运行过程中出现意外时,如除0溢出、数组越界、文件找不到等的异常事件,将阻止程序的正常运行,因此在设计程序时,要充分考虑容错性,估计到可能会发生的各种异常情况并做出相应的处理,以便程序可以稳健地运行。
>以往的异常处理方法
1.PHP.ini将能够显示的错误类型。
2.PHP.ini中error_reporting控制输出到用户端的消息种类。以下几种是php.ini中推荐的配置。
error_reporting = E_ALL; 表示输出所有的信息
error_reporting = E_ALL&~E_NOTICE; 表示输出所有的错误,除了提示。
error_reportin = E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE _ERROR : 表示输出所有的ERROR信息。
3.在php.ini中,display_errors可以设置是否将以上设置的错误信息输出到用户端
display_errors=On 输出到用户端(调试代码的时候,打开这项非常的方便)
display_errors=OFF 消息将不会输出到用户端(最终发布给用户时记得改成off)
注意:
在PHP中,对于错误处理的要求是非常宽松的。PHP系统会尽量让程序运行下去,除非遇到致命错误。
除了在php.ini文件中可以调整错误消息的显示级别外,在PHP代码中也可以自定义消息的显示级别。PHP提供了函数error_reporting()来实现本程序中自定义消息显示级别的功能。
函数语法:int error_reporting([int level])
函数说明:使用这个函数可以定义当前PHP页面中错误消息的显示级别。参数level使用了二进制掩码组合的防方式,具体组合方式查表。
>使用die()来终止程序运行
在以往的程序开发过程中,最常见的异常处理办法是程序出现异常就停止执行,使用die()来终止,使当前程序退出。
大新程序项目一般将基础类单独存储在一个文件夹中,每个类单独存储为一个PHP文件,以类名为PHP文件命名。
假设在网站的mail_class目录下为基类库存储文件,该目录下有一stmp.php文件是提供SMTP功能的基类或者接口程序,并有其他一些以类名为文件名的子类文件:normal.php(普通邮件通过mail()函数发送)、auth.php(以需要验证发信人身份的第三方SMTP服务器发送邮件)、MIME.php(发送复杂的图文邮件)等。
Dir . "/" . $smtp . ".php";
if (!file_exists($path))
{
die("Cannot find " . $path . ".\n");
}
require_once $path;
if(!class_exists($smtp))
{
die("class". $smtp . "does not exist .\n");
}
$ret = new $smtp();
if (!($ret instanceof SMTP))
{
die($smtp . " is not a SMTP class .\n");
}
return $ret;
}
}
$smtp = new SMTPManager();
$smtp->GetObject("normal");
?>
在例子中,如果我们尝试寻找类的操作失败,脚本执行将会终止,这体现了代码的安全性。但这段代码不灵活,没有足够的弹性,事实上有时候我们并不希望在找不到想要的类文件时就马上停止执行程序,也许我们有一个默认的命令让程序继续执行。
>使用trigger_error()显示自定义警告信息。
在高于4.01的PHP各版本中提供了trigger_error函数,用以提供自定义警告,在处理错误上会更具优秀,对于客户程序员来说更易于处理错误。
函数语法:bool trigger_error(string $error_msg[,int $error_type])
函数说明:该函数用来提供用户自定义级别的警告、提示、错误信息,trigger_error()接受一个错误信息$error_msg和一个常量$error_type作为参数,可以使用的$error_type常量有3个:E_USER_ERROR(致命错误)、E_USER_WARNING(警告级别非致命错误)、E_USER_NOTICE(提示信息)。
Dir . "/" . $smtp . ".php";
if (!file_exists($path))
{
trigger_error("Cannot find " . $path . ".\n",E_USER_ERROR);
}
require_once $path;
if(!class_exists($smtp))
{
trigger_error("class". $smtp . "does not exist .\n",E_USER_ERROR);
}
$ret = new $smtp();
if (!($ret instanceof SMTP))
{
trigger_error($smtp . " is not a SMTP class .\n",E_USER_ERROR);
}
return $ret;
}
}
function ErrorHandler($errnum,$errmsg,$file,$line)
{
if($errnum == E_USER_ERROR)
{
echo "ERROR:$errmsg\n
";
echo "FILE:$file\n
";
echo "LINE:$line\n
";
exit();
}
}
$handler = set_error_handler('ErrorHandler');
$smtp = new SMTPManager();
$smtp->GetObject("auth");
?>
在本例中,为了处理异常,使用了set_error_handler()自定义处理函数来接收trigger_error()函数抛出来的错误。set_error_handler()接受一个函数名作为参数。如果出发了一个错误,用作参数的这个函数就会被调用来处理错误。此函数需要传入4个参数:错误标志、错误信息、出错文件、出错处的行数。我们还可以将一组数组传递给set_error_handler(),数组中的第一个元素必须是错误处理器将调用的对象,第二个元素时错误处理函数的名称。我们这个错误处理器设计的很简单,还可以改进,如增加记录出错信息,输出debug数据等。但仍然是一个很粗糙的错误处理途径,只能限于已经考虑到的出错情况,如扑捉一个E_USER_ERROR错误,或许我们希望不使用exit()和die()来中止脚本执行,但如果这样做的话,可能会引起一些很微妙的bug,本来应该中止的程序却继续执行了。
>随时判断错误标识
##通过返回值进行处理的方法确实有效,意味着我们可以根据环境来处理多个错误,不一定在第一个错误发生时就马上停止程序的执行,但也有许多不足之处:
#程序复杂
#可靠性差
#返回信息有限
#返回代码标准化困难
使用"false"这样的错误标志的好处是直观,但是明显给出的信息量不够,我们无法得知到底是在哪一个环节上出现错误而导致返回false,于是我们增设一个error_str属性,这样产生错误后能输出更详细的出错信息。
error_str = get_class($this). "::{$method}(): $msg";
}
public function getError()
{
return $this->error_str;
}
public function getObject($smtp)
{
$path = $this->Dir . "/" . $smtp . ".php";
if(!file_exists($path))
{
$this->setError(__FUNCTION__, "Cannot find" . $path . ".\n");
return false;
}
require_once $path;
if(!class_exists($smtp))
{
$this->setError(__FUNCTION__, "Class" . $smtp . "does not exist.\n");
return false;
}
$ret = new $smtp();
if(!($ret instanceof SMTP))
{
$this->setError(__FUNCTION__, $smtp . "is not a SMTP class .\n|");
return false;
}
return false;
}
}
$smtp = new SMTPManager();
if($smtp->getObject("MIME"))
{
// 此处书写正常流程代码
}
else
{
echo $smtp->getError() . "
";
die("getObject Error!");
}
?>
这个简单的机制可以让setError()记录下错误信息,其他代码再通过getError()来获得脚本错误的相关信息。我们可以将这个功能抽取出来放在一个最基本的类中,其他所有类都从这个类继承而来,这样就可以统一处理错误,否则可能出现混乱。然而,实际开发中要让程序中的所有类从同一个类中继承而来是很困难的,不过可以通过接口(interface),否则无法实现一些子类自身特有的功能。
使用PEAR扩展类库处理异常
最后一种办法,以PHP4中扩展出来的PEAR基类库来处理异常时,最重要的方法是raiseError(),它可以在出错的时候返回一个PEAR_Error对象,它的用法是:
PEAR::raiseError(string $message[,int $code[,int $mode[,int |array $options[,mixed $userinfo[,string $error_class[,boolean $skipmsg]]]]])
参数说明
string $message : 错误信息,如果左面是空的,默认错误信息是"unknown error"
integer $code : 错误代码
integer $mode : 错误模式。(有些常量需要自己查表设定)
mixed$options:错误代码。(记清调用方式)
mixed $userinfo : 存放附加的用户信息。
string $error_class : 错误对象的错误类名。
boolean $skipmsg:如果自定义类的构造器不能接受一个错误信息,就使用这个参数。如果没有$error_class参数,就不要使用$skipmsg参数,它将不工作。
在执行一段可能产生错误的代码后,我们需要使用PEAR的isError()方法来检测是否存在错误,并且可以使用PEAR_Error的getMessage()来取得最近一次的错误信息。需要注意的是一定要再关键的地方用PEAR::isError().
IsError()方法检测一个变量是否是PEAR_Error对象,可选包含一个明确的错误信息或者代码。它的用法如下:
boolean PEAR::isError(mixed $data[,mixed $msgcode])
参数说明:
mixed $data :检查的值
mixed $msgcode : 附加的错误信息或者准备检测的代码。
error_str = get_class($this) . "::{$method}():$msg";
}
public function GetError()
{
return $this->error_str;
}
public function GetObject($smtp)
{
$path = $this->Dir . "/" . $smtp . ".php";
if(!file_exists($path))
{
return PEAR::RaiseError("Cannot find" . $path .".\n");
}
require_once $path;
if(!class_exists($smtp))
{
return PEAR::RaiseError("class" . $stmp ."does not exist.\n");
}
$ret = new $smtp();
if(!($ret instanceof SMTP))
{
return PEAR::RaiseError($smtp . "is not a SMTP class .\n"); // 一种可能的返回
}
return $ret; // 另一种可能的返回。
}
}
$smtp = new SMTPManager();
if(PEAR::isError($smtp))
{
echo $cmd->getMessage() . "\n
";
exit;
}
else
{
// 此处书写正常流程代码。
}
?>
Pear_Error既是出错标志又包含有错误的相关具体信息,这对于客户代码来说是很好用的。尽管返回一个对象值可以让我们灵活地对程序中的问题作出反映,但它的getObject()方法有两个可能的出口,而且都是对象,有时可能产生混淆,就有了"污染接口"的副作用。
>PHP5中的异常处理
反观以往我们需要的异常处理机制
#允许一个方法给出一个出错标记给客户代码
#提供程序错误的详细信息
#让你同时判断多个出错条件,将错误报告和程序处理流程分开。
#返回值必须是独立的类型,不会与正常返回的类型相混淆,造成污染接口。
PHP5提供了面向对象的异常处理机制来达到这些目的。
>异常类Exception
异常类Exception中属性和方法的定义:
message = $message;
}
$this->code = $code; // 默认错误代码为0
$this->file = __FILE__; // 文件名
$this->line = __LINE__; // 行号
$this->trace = debug_backtrace();
$this->string = StringFormat($this);
}
// 取得出错信息
final public function getMessage()
{
return $this->message;
}
// 出错的代码
final public function getCode()
{
return $this->code;
}
// 异常发生的文件
final public function getFile()
{
return $this->file;
}
// 异常发生的行数
final public function getLine()
{
return $this->line;
}
// 跟踪异常每一步传递的路线,存入数组,返回该数组
final public function getTrace()
{
return $this->trace;
}
// 和getTrace()功能一样,但可以将数组中的元素转成字符串并按一定格式输出
final public function getTraceAsString()
{
}
//
public function __toString()
{
return $this->string;
}
}
?>
当我们的脚本遇到一个异常时,我们可以实例化该对象:
$ex = new Exception("Could not open $this->file");
在PHP5中,不需要书写上面的这段代码来定义Exception类,该类已经内置在PHP5中了,在触发异常时推荐使用关键字throw来抛出异常,而不是直接生成Exception的对象实例。
>异常抛出关键字throw
我们把生成异常对象并把它提交给运行时系统的这个过程称为抛出(throw)一个异常。运行时系统在方法的调用栈中查找,从生成异常的方法开始进行回朔,直到找到包含相应异常处理的方法为止,这一个过程称为捕获(catch)一个异常。
throw的用法是:
throw new Exception("Exception Message",2);
>异常捕获try-Catch语句
任何调用可能会抛出异常的程序代码都应该使用try语句。一旦throw抛出异常,程序会在throw语句后立即终止,它后面的语句不再执行,而是在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。
catch语句则用来处理可能抛出的异常。catch语句的参数类似于方法的声明,包括一个异常类型和一个异常对象。异常类型必须为Exception类的子类,它指明了catch语句所处理的异常类型,异常对象则由运行时系统在try所指定的代码块中生成并被捕获,大括号中包含对象的处理,其中可以调用对象的方法。
try-catch语句的语法是:
try
{
// 可能发生异常的语句
}
catch(异常类型 异常实例)
{
// 异常处理语句
}
截取:
try
{
$smtp = new SMTPManager();
$mail = $smtp->getObject('MIME');
}
catch (Exception $e)
{
print $e->getMessage();
exit();
}
如果try块未产生任何异常,try块将正常运行完毕,catch块内容不会被执行。
try块如果抛出了异常,会立刻在catch中寻找可以捕获此异常的catch块,并运行相应的catch块代码,然后跳出try-catch块继续运行。而try块中抛出异常后面的代码将被跳过。
如果try块中的异常不能被catch块捕获,将抛向系统,引发系统致命错误,代码终止运行。
在catch中,异常类型后面跟的是一个变量,这个变量将指向内存中被捕获的异常实例。
如果try块中的语句可能抛出两种以上异常,在使用throw new Exception($message, $code)时,可通过第二个参数$code来指定异常代码,然后在catch块中判断不同的异常代码,分别进行异常处理。
catch语句可以有多个,分别处理不同类型的异常,有多个扩展的异常处理类时就常用多个catch语句来对应匹配,运行系统从上到下分别对每个catch语句处理的异常类型进行检测,直到找到类型相匹配的catch语句为止。每个catch语句的参数就像是一个匹配测试条件,第一个发生匹配的catch语句将会执行,而不执行其他的catch语句。这里,类型匹配指catch所处理的异常类型与生成的异常对象的类型完全一致或者是它的父类,因此,catch语句的排列顺序应该是从特殊到一般,将针对特定异常的catch语句卸载前面,而将针对一般性的异常的catch语句写在后面。
也可以用一个catch语句处理多个异常类型,这时它的异常类型参数应该是这多个异常类型的父类,如Exception,程序设计中要根据具体的情况来选择catch语句的异常处理类型。
>异常处理函数设置Set_exception_handler
一般来说,我们都是通过try-catch来捕获并处理throw抛出的异常,为了保持向后兼容,PHP绝大部分内置函数是不会抛出异常的。然而,为了使用这些功能,新的扩展正在被定制,与前文介绍过的set_error_handler()类似,PHP另外提供了一种不太常用的办法,在不使用try-catch的情况下,指定一个自定义函数来自动捕获并处理异常,提供了此功能的是set_exception_handler()
在使用set_exception_handler()时,要指定的用作捕获异常的函数定义代码必须放置在set_exception_handler()之前,将该函数名称作为set_exception_handler()的参数。
getMessage() . "
Exception Code : " . $e->getCode()
. "
FileName: " . $e->getFile() . "
Line: " . $e->getLine() . "
Exception Trace: " . $e->getTraceAsString();
}
// 处理未被捕获(未进行try..catch操作的)异常
// 自定义异常处理函数test_error_except一定要在set_exception_handler()之前
set_exception_handler('test_error_excepts');
function file_open()
{
if(!file_exists($f))
{
throw new Exception("File not found!",1);
}
if(!fopen($f,"r"))
{
throw new Exception("File open error!",2);
}
}
file_open("test.htm");
?>
>完整的异常信息
Exception类及其子类都有getTraceAsString()方法,它可以返回各方法中导致发生异常的每一层细节;getTrace()方法则返回一个多维数组,提供更详细的异常信息,这个数组的第一个元素包含有异常发生的位置,第二个元素包含外部方法调用的细节,直到最高一层的调用。这个数组的每个元素本身也是一个数组,包含有如下几个键名元素。
KEY:
file : 产生异常的文件
line : 产生异常的类方法所在行数
function : 产生异常的函数/方法
class : 调用的方法所在类
type : 调用类型:"::"表示调用静态类成员 "->"表示实例化调用(先实例化生成对象再调用)
args : 类方法接收到参数
>扩展的异常处理类
PHP5也内置提供了Exception类的一些子类,有两个直接子类LogicException和RuntimeException,分别表示逻辑异常和执行异常。
LogicException子类又衍生出许多逻辑异常子类。
RuntimeException子类衍生出许多运行异常子类。
我们也可以增加其他自定义异常类,这些自定义异常类必须继承自Exception类或者它的子类。
正式项目中,在抛出异常时,使用多个不同的catch块来检测匹配。另外增加程序是否运行在调试模式下不同的处理机制和同一个catch块根据不同的异常代码来分别处理异常的机制。
Dir . "/" . $smtp . ".php";
if(!file_exists($path))
{
throw new SMTPException("Cannot find" . $path . ".\n");
}
require_once $path;
if(!class_exists($smtp))
{
throw new IllegalException("class" . $smtp . "does not exist .\n",101);
}
$ret = new $smtp();
if(!($ret instanceof SMTP))
{
throw new IllegalException($smtp . "is not a SMTP class .\n",102);
}
return $ret;
}
}
try
{
$smtp = new SMTPManager();
$mail = $smtp->getObject('normal');
}
catch(SMTPException $e) // 第一个catch块
{
if($debug)
{ // 调试模式
echo $e->getMessage();
exit();
}
else
{ // 网站运行模式,不输出调试信息
exit();
}
}
catch (IllegalException $e) // 第二个catch块
{
if($debug)
{ // 调试模式
switch($e->getcode())
{
case 101:
//文件存在但类不存在时处理代码
echo $e->getMessage(); // 只输出信息,并不需要exit()退出
break;
case 102:
//不能生成对象实例时的处理代码
break;
echo $e->getMessage(); // 无对象实例可用,需要exit()退出
exit();
default:
//其他默认情况
break;
}
}
}
catch (Exception $e) // 第三个catch块
{
if($debug)
{ //调试模式
echo $e->getMessage();
exit();
}
else
{
exit();
}
}
?>
当不存在smtp_class文件夹下的类同名文件时,将会抛出SMTPException异常,当文件存在二类不存在或者类不能生成对象实例时将会抛出IllegalException异常,虽然这段程序只可能抛出2个异常自定义类,但最后还是提供了第3个catch块,用于匹配Exception异常,而Exception类是所有异常类的基类,可以匹配所有的异常情况,所以,如果这个catch块放在了第一个和第二个catch块之前,则只会执行这个catch块的内容,其余两个catch块的异常处理代码永远没有机会去执行了。
>异常的传递与重掷
如果我们已经触发了一些在发生时调用程序无法马上处理的异常怎么办?类似地,将处理这个异常的责任交还给调用当前方法的上一级代码,也就是在catch语句中再次抛出了异常,又叫重掷异常,这就使得异常可以沿着方法的调用层链逐级向上传递了。
>正确使用异常处理
1.不提倡用一个catch语句捕获所有的异常,最常见的情形就是catch(Exception $e)语句。
要理解原因,我们必须回顾下catch语句的用途。catch语句表示我们预期会出现某种异常,而且希望能够处理该异常。异常类的作用就是告诉PHP我们想要处理的是哪一种异常。由于绝大多数异常都直接或间接从Exception继承,catch(Exception ex)就相当于说我们想要处理所有的异常,可是在同一个catch块中处理几种截然不同的异常是不合适的,catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类。必要时使用多个catch,不要试图处理所有可能出现的异常。
2.尽量避免把大量的代码放入单个try块,这不是好习惯。这样做为分析程序抛出异常的原因带来了困难,因为一大段代码中有太多的地方可能抛出Exception
##本章小结
异常是PHP5中的一个新的重要特性,异常机制提供了几个非常关键的好处。
1.通过将错误处理集中于catch语句中,我们可以将错误处理从应用流程中独立出来。这也使代码的可读性提高,看起来令人愉快。通过采取非常严格的策略来捕捉所有异常并中止脚本执行,这样可以获得所需的附加的弹性,同时实现安全易用的异常管理。
2.重掷异常,可以在catch中传播错误信息,将异常数据流从低层传递至高层,就是说异常被传回最适合决定如何处理异常的地方。这看起来会显得有点奇怪,但实际情况中经常在异常发生的时候无法立刻决定如何处理它。
3.异常机制提供的throw/catch避免了直接返回错误标识,方法的返回值是可以由类来决定。其他程序员使用我们的代码时,可以指定返回i一个他希望的形式,而不需要令人疲倦的不停测试,方便定位错误并维护。