异常是OOP应用程序中的一个关键部分,PHP5对其进行了介绍。“异常”这一术语表示了由try、catch和throw等语句和Exception类组成的整个处理机制。这一机制是用来帮助定义问题发生的时间,以及在问题发生时代码处理问题的方法。
异常提供了控制应用程序生成和处理错误的方法。还通过提供的异常发生的场景细节,使得我们能够更加轻松地编写程序。另外,通过使用异常,能够创建具有容错特性的更加稳定的应用程序,并且在发生问题时,异常也能够通知到管理员。
实现异常
异常应用被用来处理不应该在正常的代码执行中发生的任何类型的错误。例如,你可能会在连接一个外部数据库的代码中实现一个异常,以处理可能发生的数据库连接失败的情况。
异常是通过增加try、catch和throw这三个关键词和内置的Exception基类来实现的。这些语言构造块允许运行可能发生错误的代码块并且能够从中恢复。
1、try
try关键字用来定义检测异常的代码块。与使用其他代码块语句一样,如if、while或者for,使用try语句块要加上大括号。try语句块具体有以下的形式。
try{ //代码 }
try语句块中的代码可能一直运行到结束,也可能抛出异常。如果抛出了异常,代码的剩余部分就会被跳过,程序会调到catch语句块中重新执行。
2、catch
catch语句块定义了处理发生异常时的处理方法。它允许定义要捕捉的类型,并且可以访问捕捉到的异常的细节。
catch(Exception $e){ echo $e; }
在这个例子中,$e是Exception类的一个实例。Exception类是所有类型的异常祖先,所以捕捉Exception类会捕捉到任何类型的异常。
为了处理不同类型的异常,你也许会定义多个catch语句块。应该先定义最特定的类型,这是因为catch是按照顺序来解析的,在前面的语句块会先执行。
3、throw
throw语句是用来触发异常发生并且在这一点上中断处理过程的。你必须给throw语句传递一个Exception类的实例。可以抛出一个存储在变量中的异常,也可以直接在throw语句中创建这个实例。
以下几种形式都是等价的。
$a = new Exception('Error message'); throw $a;
或者
throw new Exception('Error message');
4、Exception
Exception类是所有异常类的基类。为了定义自定义异常类,可以从Exception类派生。
Exception的构造函数可以接收一条错误信息和一个错误代码作为参数。错误信息很容易理解,但错误代码的含义就需要做一些解释。
通过提供的错代码,在处理异常事件时,就可以灵活处理异常了。通过检查返回的代码,可以以数字的形式映射异常类型,而不是依赖于错误的字符串,因为错误的字符串可能会随时发生改变。
构造好异常的实例后,异常就获得几个关键的信息,其中包括构造异常的代码所处于的位置、在构造时执行的代码、错误信息和错误代码。
方法 | 说明 |
getMessage() | 返回异常信息,此信息是描述错误状态的字符串 |
getCode() | 返回错误代码 |
getFile() | 返回发生错误所在的源文件。此信息对于查找异常抛出的位置是非常有用的 |
getLine() | 返回异常抛出位置在文件中的行号,需要和getFile()方法一起使用 |
getTrace() | 返回包含场景的一个数组,这是当前正在执行的方法以及执行顺序的一个列表 |
getTraceAsString() | 与getTrace()相同,不过这个函数返回的是字符串而不是数组 |
__toString() | 返回用字符串表达的整个异常信息 |
由于数组中的每个键值都包含了文件、行号、函数名称以及重要的信息,所以getTrace()方法非常有用。使用回溯信息,你可以看到导致问题发生的所有数据流。从而简化了调试工作。getTrace()方法与内置函数debug_backtrace()非常相似。
function connectToDatabase($config){ if(!$conn = @mysqli_connect($config['host'],$config['user'],$config['password'],$config['database'],empty($config['port']) ? '' : $config['port'])){ throw new Exception('Could not connect to the database'); } } try{ $config = [ 'host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => 'root', 'database' => 'deruimu11' ]; connectToDatabase($config); }catch(Exception $e){ echo $e->getMessage(); }
如果无法连接数据库,连接函数就会返回false,异常就会被抛出。throw关键字要和一个Exception类的对象一起使用,它会告诉应用程序什么时候发生了错误。一旦抛出了异常,这个函数就不会执行到最后,它会直接跳出到catch语句块。在catch语句块中,应用程序会打印出错误信息。
扩展异常
通用的异常非常好用,但异常也可以有更多的用处。在上面的代码中,如果可以获得数据库连接失败的原因,它将会更加有用。要获取这样的信息,需要为Exception类集成相应的函数。
class DatabaseException extends Exception{ protected $databaseErrorMessage; public function __construct($message = null,$code = 0){ $this->databaseErrorMessage = mysqli_connect_error(); parent::__construct($message,$code); } public function getDatabaseErrorMessage(){ return $this->databaseErrorMessage; } } function connectToDatabase($config){ if(!$conn = @mysqli_connect($config['host'],$config['user'],$config['password'],$config['database'],empty($config['port']) ? '' : $config['port'])){ throw new DatabaseException('Friendly Message'); } } try{ $config = [ 'host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => 'root', 'database' => 'deruimu11' ]; connectToDatabase($config); }catch(DatabaseException $e){ echo $e->getMessage(); echo $e->getDatabaseErrorMessage(); }
在创建这个类时,需要谨慎地调用基类的构造函数,这是因为错误的实现可能导致PHP中发生不可靠并且通常不稳定的行为。
记录异常日志
通常,将异常记录到日志文件中以便日后回顾是很有好处的。使用以下两种方法之一可以做到这一点。
- 为应用程序创建一个自定义的异常基类。
- 定义一个未捕获的异常处理程序。
为了定义一个记录日志的异常基类,需要创建一个Exception类的子类并给它添加一个log()方法,然后在重新的构造函数中调用这个日志方法。
class LoggedException extends Exception{ public function __construct($message = null,$code = 0,$file = './phpException.log'){ $this->log($file); parent::__construct($message,$code); } protected function log($file){ file_put_contents($file,$this->__toString(),FILE_APPEND); } }
然后,在所有调用异常或者自定义异常的地方使用LoggedException类。这记录所有异常的日志,即使是那些被捕捉的异常。但这并不一定符合我们的要求,因为我们可能希望只记录未被捕捉的异常,或者那些被catch语句块重新抛出的异常。在这些情况下,你希望使用PHP函数set_exception_handler()。
set_exception_handler()函数定义了该如何处理当某个异常向上一直回溯到主函数入口都没有被捕捉的情况。
要使用set_exception_handler()函数,必须声明一个接收一个参数的函数。然后,将这个函数的名称作为一个字符串传递给set_exception_handler()函数。然而,必须在调用set_exception_handler()函数之前声明这个函数。
function exceptionLogger($exception){ $file = './exceptionLog.log'; file_put_contents($file,$exception->__toString(),FILE_APPEND); } set_exception_handler('exceptionLogger');
现在,因为已经重写了默认的异常处理程序,所以应用程序中未捕捉的异常会被记录到日志中而不会显示在屏幕上。
异常产生的开销
虽然;异常机制的功能非常强大,但使用它要付出代价,在PHP中,当抛出一个异常时,许多机制必须被初始化,其中包括异常类实例和代码回溯信息。如果异常日志记录到文件中,就会增加更多的花销。异常的强大功能使得我们很容易过分地使用它。
我们不应该使用异常来控制一般的应用程序流,只是因为这样做会大大降低应用程序的性能。例如,在数据库中搜索登录标识并且没有找到对应用户时,就不应该使用它。在这种情况下,应该只返回null或者false来表示失败信息。只是从PHP函数中返回混合结果的标准方法:有效的结果表示返回的数据,而false或者null表示发生错误。换句话说,在PHP函数没有找到希望查找的结果时,通常应该返回null或者false,而不是具体的值。
错误代码
之前提到过,你也希望使用错误代码来控制判断条件。然而,当应用程序的规模越来越大时,你将希望集中存放错误信息中的字符串,以便能够轻松地更新它们,甚至将它们翻译成别的文字。
有很多方法可以做到这一点,但是它们有一个共同的特点,即将错误的代码映射到字符串上。你还可以创建一个从Exception类派生的子类,以便可以更加容易地处理具有错误代码的异常。
class DatabaseException extends Exception{ const ConnectFailed = 1; const LoginFailed = 2; const PermissionDenied = 3; public function __construct($code = 0){ switch($code){ case DatabaseException::ConnectFailed: $message = 'Database connection failed'; break; case DatabaseException::LoginFailed: $message = 'Login to the database was rejected'; break; case DatabaseException::PermissionDenied: $message = 'Permission denied'; break; default; $message = 'Unknown Error'; } parent::__construct($message,$code); } } try{ $config = [ 'host' => '127.0.0.1', 'port' => 306, 'user' => 'root', 'password' => 'root', 'database' => 'deruimu11' ]; if(!$conn = @mysqli_connect($config['host'],$config['user'],$config['password'],$config['database'],empty($config['port']) ? '' : $config['port'])){ throw new DatabaseException(DatabaseException::ConnectFailed); } }catch(DatabaseException $e){ echo $e->getMessage(); }
使用这个自定义异常,所有的错误信息字符串都会被集中到一个地方。这样就可以方便地更新数据库连接错误信息,而不需要在大量的代码中查找它。
类型提示和异常
在处理自定异常时,类型提示尤其的重要。通过使用类型提示和多个catch语句块,能够在捕捉其他错误之前捕捉某个特定类型的错误。
我们在之前讲到过,catch语句块允许指定要捕捉的异常类型,并且还可以有多个catch语句块。
class firstException extends Exception{ } class secondException extends Exception{ } try{ //抛出异常的代码 }catch(firstException $e){ //处理抛出的异常类型是firstException的情况 }catch(secondException $e){ //处理抛出的异常类型是secondException的情况 }catch(Exception $e){ //处理所有其他的异常类型 }