深圳升蓝软件
数据库开发 .Net技术  |  ASP技术 PHP技术 JSP技术 应用技术类   
Hiblue Software

细看PEAR的错误处理


March 23,2004

PEAR提供了强大的错误处理机制。这篇文章向你展示如何从这个系统中获益。


许多程序已经使用了PEAR的包。许多PHP程序员或多或少的熟悉了PEAR中的错误处理。但是这个机制并不局限于PEAR的包——所有人都能在他们的类和程序中使用这些方法。

这篇文章被分为两个部分:首先我们将看看类中用于错误处理的函数,然后我们将看看如何基于PEAR错误处理机制来处理错误。

我们的例子类称为cvs2db,它把数据从CSV文件插入到数据库的表中。因为数据可能是手写的,他们的数据应该在插入之前先得到验证——落实postcode。函数import()完成读入,检查和插入的工作;它返回损坏的记录数目。如果返回的值大于0,出错的记录集能够使用exportUnvalid()写入到新的CSV文件中。典型的用法是这样的:

<?php
$cd = new csv2db();
$dsn = 'mysql://root@localhost/csv2db';
if( 0 < $cd->import("./dat.csv", $dsn, 'address')) {
$cd->exportUnvalid("./dat2.csv");
}
?>

可能的错误包括:

要导入的CSV文件不存在,
连接到数据库失败,
记录集损坏,以及CSV导出文件无法创建。

在提供错误信息的经典解决方案中你可能写这样的代码:

<?php
$cd = new csv2db();
$dsn = 'mysql://root@localhost/csv2db';
$result = $cd->import("./dat.csv", $dsn, 'address')
switch($result) {
case FILE_NOT_OPENED:
...
break;
case DATABASE_ERROR:
...
break;
default:
if(0 < $result) {
$cd->exportUnvalid("./dat2.csv");
} else {
echo 'every thing ok!'
}
}
?>

这对于短的脚本来说是可接受的也是常用的办法——但是对于错误处理经常受到关注的大程序来说不是这样。传统的可能性强迫类的作者做最终的决定!在大部分情况下,这个决定根据的是那时对类的调用而不是基于长期的使用和可重用代码的思想。一个灵活的错误处理机制是可重用代码的重要部分,PEAR Error API 就是这样的一种受到良好测试的机制。


用户眼中的类

除了那两个函数之外,类提供了一套错误处理函数和一个自己的错误对象称为DB2CVS_Error,它有一个特殊的本地化的错误信息的特性功能。

现在我将向你展示如何在错误发生时控制类的行为。

局部和全局错误处理

你用setErrorHandling()管理错误处理;这个函数需要两个参数:第一个是错误模式,而第二个(可选的)参数是错误模式特定的选项。例如 setErrorHandling(PEAR_ERROR_PRINT, 'This error occurred %s') 还有 setErrorHandling(PEAR_ERROR_TRIGGER, E_USER_WARNING)。


这个函数的调用方式是一般行为中最重要的:静态还是实体。在类cvs2db中,我们能两者都用来设置错误处理,所有这些调用有相同的结构——为类设置错误模式:

// per instance
$cd = new csv2db();
$cd->setErrorHandling(PEAR_ERROR_DIE):
// static
CVS2DB::setErrorHandling(PEAR_ERROR_DIE);
PEAR::setErrorHandling(PEAR_ERROR_DIE);

如果两者给出同样的结果,区别在哪?实体调用仅仅为那个类设置而静态调用对于所有使用PEAR_Error或者从那个类派生的所有类起作用。这个也作用于第一个静态命令CVS2DB::setErrorHandling(PEAR_ERROR_DIE)——虽然它看上去仅仅影响了cvs2db类。


总结:作为一个实体函数使用命令意味着仅仅为这个实体(局部)设置错误模式,而作为静态函数来调用就是为整个脚本设置错误模式(全局)。


setErrorHandling() 和 raiseError()


两个函数都能够被静态调用和作为实体的函数调用。记住怎样的一个组合使得他们如何互相影响的很重要。

基本上是:setErrorHandling()的静态调用仅仅影响raiseError()的静态调用——setErrorHandling()作为实体函数仅仅影响raiseError()作为静态函数调用。在类csv2db中,使用csv2db::setErrorHandling()来设置错误模式是不可行的,因为我们使用$this->raiseError(...)。解决这个闻天有一点小技巧——改写raiseError():

function raiseError(...,$mode=null, $options=null,...) {
if($mode==null && $this->_default_error_mode!=null) {
$mode = $this->_default_error_mode;
$options = $this->_default_error_options;
}
return PEAR::raiseError(...,$mode, $options,...);
}

这样,我们映射实体调用到静态上,如果你用错误模式调用raiseError(),然后这个模式将会覆盖这些设置——这里是指的是全局的设置。

你应当当心错误是如何被类抛出的,如果你不小心,这可能导致不可预期的副作用。


错误的模式

对错误模式的了解对于使用PEAR的错误处理来说是重要的。PEAR错误处理让用户能够决定怎么去做——注意:下文中术语用户指的的是实际使用PEAR_Error程序的开发者而不是浏览脚本结果或者网页的用户。我将详细展示可能的错误模式。

PEAR_ERROR_DIE——将这个模式开启,程序将终结并且将打印错误信息。可选的,你能定义一个printf()式的字符串,它能够用于产生信息;首先'%s'在字符串中将替代储存在错误对象中的错误信息。

PEAR_ERROR_PRINT——仅仅打印错误信息,包括用于PEAR_ERROR_DIE的同样的可选用的字符串。

PEAR_ERROR_RETURN——当错误发生时的一般行为;你能用类提供isError()函数或者PEAR::isError()检查错误。

$db->setErrorhandling(PEAR_ERROR_RETURN)
if(!csv2db::isError(0 < $d = $cd->import("./dat.csv", $dsn, 'address'))) {
if(!csv2db::isError($cd->exportUnvalid("./dat2.csv")) {
} else {
// handle error
}
} else {
// handle error
}

PEAR_ERROR_TRIGGER——这儿函数向PHP运行时错误行为一样。你必须定义哪种错误应该发生:E_USER_NOTICE,E_USER_WARNING或者E_USER_ERROR。他们直接和PHP本身产生的信息相对应。请注意,在错误信息中错误发生的那行(xxx on line yy)指的是在PEAR.php中调用trigger_error的那行——而不是错误直接发生的那行。

PEAR_ERROR_CALLBACK——这是只在一个地方处理错误并且让你得代码不用考虑错误处理的最佳方式。它需要一个函数或者类函数来捕获错误,你能写一个listing 2中展示的那样的脚本,其中可以看到类相关错误对象的好处:import()函数抛出一个CSV2DB_Error给基于CSV的错误和一个DB_Error对象给相关于数据库访问的错误。

Listing 2

$cd = new csv2db();
$cd->setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
$dsn = 'mysql://root@localhost/csv2db';
if( 0 < $d = $cd->import("./dat.csv", $dsn, 'address')) {
$cd->exportUnvalid("./dat2.csv");
}

function handleError($error) {
if(DB::isError($error) {
// handle database error
}
if(csv2db::isError($error) {
switch($error->getCode()) {
case FILE_NOT_OPENED :
...
break;
case CORRUPTED_RECORD :
...
break;
}
}
}

单个的错误处理

我们有两种可能的错误:我们能够忽略的错误(损坏的记录),以及使得程序无法运行的错误(找不到文件或者打不开数据库)。如果你在shell脚本中使用类,你可以让脚本终止于第二类错误。

自然的,你可以写 $cd->setErrorHandling(PEAR_ERROR_DIE)——但是这可能在如果损坏的记录错误发生时导致问题。在这样的情况下你需要对某个错误停用或者替换错误处理办法的可能。解决办法时expectError(),如果你传递一个错误代码给这个函数,指定错误的错误模式将被单独于缺省错误模式地设置为PEAR_ERROR_RETURN。

expectError()函数把传递来的错误代码储存在栈中,使用popExpected()移出最后传递的错误代码。自从PHP 4.3之后你还能使用delExpect()了;这个函数从栈中删除了指定错误代码的匹配,你不需要关心位置了。

在实际使用中,是这样的:

$cd->setErrorHandling(PEAR_ERROR_DIE);
...
$cd->expectError(CORRUPTED_RECORD);
$cd->import(...);
$cd->popExpect();

pushErrorHandling() 和 popErrorHandling() 用起来差不多;他们能够暂时的控制错误处理。例如:如果在 exportUnvalid() 中的文件不能打开,你想要忽略错误:

PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
$cd->exportUnvalid("./dat2.csv");
PEAR::popErrorHandling();

注意调用方法的区别!expectError()/popExpect()必须作为实体函数来调用——pushErrorHandling和popErrorHandling可以静态调用。如果作为实体函数,那么他们仅仅影响那个实体。

用户有很多可能性,这是否又意味着程序员要做很多的工作呢?是,是因为你要比return false做更多的事情;否,是因为PEAR Error API给你完成了很多工作。


一些关于错误处理的思考

作为好的程序员,你不应该从你的类的用户眼中遮掩起确切的错误原因。这阻止了简单的return false的用法;还要注意也可能被PHP自动型别转换为0——这对于import()函数来说意味着所有的记录都已经被正确插入了!简单地终止脚本?,可能对于简单地PHP shell脚本来说这是可以接受地,但是对于一个web程序来说是一个坏的选择!而且,在记录损坏的情况,错误是能够被忽略的。什么不trigger_error()?这是一个可能的选择,但是有两个缺点:类的行为取决于php.ini的设置,而且对于类来说这种行为不常见。可能性能够需要用额外的函数找出错误状态。即使是所有类都提供了非标准的函数名,这还是有问题的,而且类的用户看上去会忘记那样的函数调用——正如邮件列表和新闻组中显示的那样。怎么办?让用户决定PEAR错误处理API。PEAR错误系统被广泛知道并且许多类已经使用了PEAR类;因而我们无论如何必须用PEAR错误处理机制——为什么不建立在其上呢?这避免了前面提到的问题并且给用户提供了很大的可能性。看看Listing 1,它展示了csv2db类和它的错误对象的实现。它可能是有些让人吓到,但是我们将一行行地浏览源代码。

Listing 1

<?php
require_once 'PEAR.php';
require_once 'DB.php';

define("FILE_NOT_OPENED", 10);
define("CORRUPTED_RECORD", 20);

class csv2db extends PEAR{

var $records=array();
var $unvalid=array();

function csv2db() {
$this->PEAR("CSV2DB_Error");
}

function import($file, $dsn, $table) {
$this->PEAR("CSV2DB_Error");
if($fp=@fopen($file, 'r')) {
while($data=fgetcsv($fp, 1024,';')) {
$this->records[]=$data;
}
fclose($fp);
} else {
return $this->raiseError(null, FILE_NOT_OPENED);
}

$unvalidCount=0;

$storeMode = $GLOBALS['_PEAR_default_error_mode'];
$storeOpts = $GLOBALS['_PEAR_default_error_options'];
$GLOBALS['_PEAR_default_error_mode'] = $this->_default_error_mode;
$GLOBALS['_PEAR_default_error_options'] = $this->_default_error_options;

$db = DB::connect($dsn);

$GLOBALS['_PEAR_default_error_mode']= $storeMode;
$GLOBALS['_PEAR_default_error_options'] = $storeOpts;

if(!DB::isError($db)) {
$db->setErrorHandling($this->_default_error_mode,
$this->_default_error_options);
$qp = $db->prepare("INSERT INTO $table VALUES (?, ?, ?, ?)");
foreach( $this->records as $record) {
if(preg_match('/d{5}/',$record[2])) {
$db->execute($qp, $record);
} else {
$unvalidCount++;
$this->unvalid[]=$record;
$this->raiseError(corrupted record, CORRUPTED_RECORD);
}
}
$db->disconnect();
} else {
return $db;
}
return $unvalidCount;
}

function exportUnvalid($file) {
if($fp=@fopen($file, "w")) {
foreach($this->unvalid as $data) {
fwrite($fp, implode(';', $data)."n", 1024);
}
fclose($fp);
} else {
return $this->raiseError(null,FILE_NOT_OPENED);
}
}

function isError($data) {
return (bool)(is_object($data) &&
(get_class($data) == 'CSV2DB_Error' ||
is_subclass_of($data, 'CSV2DB_Error')));
}
}

class CSV2DB_Error extends PEAR_Error {
var $msgs = array(
FILE_NOT_OPENED =>
array( 'de' =>"Datei konnte nicht ge?ffnet werden",
'en' => "File couldn't be opened"),
CORRUPTED_RECORD =>
array( 'de' =>"fehlerhafter Datensatz",
'en' => "corrupted record")
);

function CSV2DB_Error($message=null, $code = null, $mode = null,
$level = null, $debuginfo = null) {
$this->PEAR_Error(null, $code, $mode, $level, $debuginfo);
}

function getMessage($lang = "en") {
return $this->msgs[$this->code][$lang];
}
}
?>

自己的错误对象

有一个自己的错误类总是好的,虽然它可能对于这么一个小的类来说是太大的额外负担——但是这个类仅仅是一个例子并且你从如果没有错误对象需要很多代码来实现的特性那儿获益良多。好处是:首先错误是直接赋给类的;以及本地化变得更加容易。

类必须从PEAR_Error继承而来,为的是保持我们的实现简单,否则PEAR::isError()将不能正常工作。

实现包含了构造函数,其中没有改变地把参数传递给了PEAR_Error地构造函数。

改写getMessage()函数是提供本地化错误信息地关键。错误定西被定义为类的变量并且将取决于语言动态的赋值。这也将帮助消息聚集于一处——而不是把他们分散于整个主要类的源代码中。

实现PEAR错误处理

你在文章的第一部分看到了我们的类提供了一堆函数——但是他们中的仅仅有四个是直接实现的。所有的相关函数的错误处理是由PEAR基类提供的。为了从所有那些错误处理特性中获益,我们必须让cvs2db类从PEAR基类继承,也就是:class csv2db extends PEAR。

在前面的错误对象段落中,我从对isError()的解释开始。覆盖这个方法不是必要的,虽然它确实使得我们能够直接检查我们的错误类,并且使得错误跟踪更加精确并且可能节约了几毫秒。

类的构造函数仅仅只是用错误类名称最为参数调用了父类的构造函数。这个调用注册了我们的错误对象并且确保了我们的错误类在每次触发错误的时候被使用。


raiseError

在import()和exportUnvalid()的函数体中对raiseError()的使用是值得注意的。这是创建错误的关键函数;PEAR提供两个函数用于这个目的:raiseError() 和 throwError()。后一个自从PHP 4.3开始存活在并且是raiseError()的一个简化变体,两者行为是一致的;它们的参数在段落 'raiseError 和 throwError' 中描述。


raiseError 和 throwError

原型:

&raiseError( $message, $code, $mode, $options, $userinfo, $errorclass, $skipmessage)
&throwError( $message, $code, $userinfo)

Parameter Description
$message (string) The error message
$code (int) The error number
$mode (constant) Error mode
$options (mixed) Error mode specific parameters
$userinfo (mixed) additional data (ie. Debug information)
$errorclass (string) A class name  

可选的你能够把已经存在的错误对象传递给这些函数:

&raiseError($error_object)
&throwError($error_object)

如果你从源代码比较这两个函数的参数表你将看到类并没有设置message参数——这是不必须的因为我们在错误类中用 getMessage() 函数赋给错误信息。而且,调用PEAR构造函数来引入你的错误类也是不必要的,你可以在对 raiseError() 调用中指定错误类。在脑子中记住这个选项!例如,如果你的类提供了静态函数或者多于一个错误对象,你不能给你的类像我们在csv2db中做得那样全局地设置它们。

raiseError() 和 throwError() 能够被静态地调用以及像 setErrorHandling() 那样作为实体函数来调用。当你作不作静态调用地时候做正确地决定是重要的——它直接影响了用户如何用setErrorHandling()来错作我们的类。留意 setErrorHandling() 和 raiseError(),这将避免你和你的用户的头疼。

从类的这个部分能够看到全局和局部的错误设置和触发的负面影响。

$storeMode = $GLOBALS['_PEAR_default_error_mode'];
$storeOpts = $GLOBALS['_PEAR_default_error_options'];
$GLOBALS['_PEAR_default_error_mode'] = $this->_default_error_mode;
$GLOBALS['_PEAR_default_error_options'] = $this->_default_error_options;

$db = DB::connect($dsn);

$GLOBALS['_PEAR_default_error_mode'] = $storeMode;
$GLOBALS['_PEAR_default_error_options'] = $storeOpts;

首先,全局的错误模式被保存了,然后全局的错误模式设置给了局部的错误模式并且最后几行,原来的错误模式被还原了。为什么?Connect()是一个静态函数!它必须使用PEAR::raiseError()。因而假如我们不保存并且还原设置,我们会遇到问题:看看listing 3——如果类在import()函数不能连接到数据库的时候会发生什么?因为对raiseError()的静态调用受到全局错误模式的影响,而不是局部的$cd->setErrorHandling(...)的影响,脚本终止执行 。实际上push和popErrorHandling()就是设计来用于这样的任务的——但是PHP中一个现下的bug看上去不幸的组织了它很好的工作。

强制$db对象使用我们的错误模式是更舒服的方式,它支持完整的PEAR Error API,这使得代码能这样写:$db->setErrorHandling($this->_default_error_mode, $this->_default_error_options)。两个实体变量都是由PEAR_Error类提供的。

那行$this->raiseError(corrupted record, CORRUPTED_RECORD)看上去值得注意——而且缺失的返回看上去不顺眼。原因是:我们不想在发现损坏的记录时中止函数执行。你能把这个和触发一个警告进行比照。唯一的限制时模式PEAR_ERROR_RETURN没有工作。

Listing 3

<?php
...
PEAR::setErrorHandling(PEAR_ERROR_DIE)

$cd = new csv2db();
$cd->setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
$dsn = 'mysql://root@localhost/csv2db';
if( 0 < $d = $cd->import("./dat.csv", $dsn, 'address')) {
$cd->exportUnvalid("./dat2.csv");
}

$db = DB::connect($dsn);
$db->query(...);
...

function handleError($error) {
if(DB::isError($error) {
// handle database error
}
if(csv2db::isError($error) {
switch($error->getCode()) {
case FILE_NOT_OPENED :
...
break;
case CORRUPTED_RECORD :
...
break;
}
}
}
?>


PEAR错误处理和PHP 5

因为我们使用函数来创建错误,我们没有考虑在PHP 5中的try/catch/throw机制;raiseMethod和throwError将为你完成这些!对于PHP 5,函数能够为你的类透明地调用抛出PEAR_Error()——错误模式PEAR_ERROR_EXCEPTION能够用于这个目的。一下的代码应该能够在不改变类的情况下用于PHP5中:

<?php
$i = new csv2db();
$dsn = 'mysql://root@localhost/csv2db';
try {
if( 0 < $d = $i->import("./dat.csv", $dsn, 'address')) {
$i->exportUnvalid("./dat2.csv");
}
}
catch CSV2DB_Error {
// fetch the error
}
?>

结论

我希望你大概了解了PEAR错误处理,它提供了排除和处理错误的强大机制。看看PEAR手册[1]的代码部分并且找出这些函数提供的好处。

Alexander Merz (alexmerz at php dot net) 是PEAR手册的编辑并且以自由创作者和作家为职业。

链接

[1] pear.php.net/manual/en/core.pear.html
注:本文章为原创文章,版权归文章作者与超越PHP网站所有,未经本站同意,禁止任何商业转载。非盈利网站及个人网站转载请注明出处,谢谢合作!
Copyright © 2001-2008 Shenzhen Hiblue Software Team All rights reserved