章 20. 异常处理

PHP 5 添加了类似于其它语言的异常处理模块,可以在 PHP 内检测(try)、抛出(throw)和捕获(catch)异常。一个 try 至少要有一个与之对应的 catch。定义多个 catch 可以捕获不同的对象。PHP 会按这些 catch 被定义的顺序执行,直到完成最后一个为止。而在这些 catch 内,又可以抛出新的异常。

当一个异常被抛出时,其后(译者注:指抛出异常时所在的代码块)的代码将不会继续执行,而 PHP 就会尝试查找第一个能与之匹配的 catch。如果一个异常没有被捕获,而且又没用使用 set_exception_handler() 作相应的处理的话,那么 PHP 将会产生一个严重的错误,并且输出未能捕获异常的提示信息。

例子 20-1. 抛出一个异常

<?php
try
{
    
$error = 'Always throw this error';
    
throw new Exception($error);

    
// 从这里开始,tra 代码块内的代码将不会被执行
    
echo 'Never executed';

}
catch (Exception $e) {
    echo
'Caught exception: ',  $e->getMessage(), "\n";
}

// 继续执行
echo 'Hello World';
?>

扩展 PHP 内置的异常处理类

用户可以用自定义的异常处理类来扩展 PHP 内置的异常处理类。以下的代码说明了在内置的异常处理类中,哪些属性和方法在子类中是可访问和可继承的。译者注:以下这段代码只为说明内置异常处理类的结构,它并不是一段有实际意义的可用代码。

例子 20-2. 内置的异常处理类

<?php
class Exception
{
    
protected $message = 'Unknown exception';   // 异常信息
    
protected $code = 0;                        // 用户自定义异常代码
    
protected $file;                            // 发生异常的文件名
    
protected $line;                            // 发生异常的代码行号

    
function __construct($message = null, $code = 0);

    
final function getMessage();                // 返回异常信息
    
final function getCode();                   // 返回异常代码
    
final function getFile();                   // 返回发生异常的文件名
    
final function getLine();                   // 返回发生异常的代码行号
    
final function getTrace();                  // backtrace() 数组
    
final function getTraceAsString();          // 已格成化成字符串的 getTrace() 信息

    /* 可重载的方法 */
    
function __toString();                       // 可输出的字符串
}
?>

如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 parent::__construct() 来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载 __toString() 并自定义输出的样式。

例子 20-3. 扩展 PHP 内置的异常处理类

<?php
/**
* 自定义一个异常处理类
*/
class MyException extends Exception
{
    
// 重定义构造器使 message 变为必须被指定的属性
    
public function __construct($message, $code = 0) {
        
// 自定义的代码

        // 确保所有变量都被正确赋值
        
parent::__construct($message, $code);
    }

    
// 自定义字符串输出的样式 */
    
public function __toString() {
        return
__CLASS__ . ": [{$this->code}]: {$this->message}\n";
    }

    
public function customFunction() {
        echo
"A Custom function for this type of exception\n";
    }
}


/**
* 创建一个用于测试异常处理机制的类
*/
class TestException
{
    
public $var;

    const
THROW_NONE    = 0;
    const
THROW_CUSTOM  = 1;
    const
THROW_DEFAULT = 2;

    function
__construct($avalue = self::THROW_NONE) {

        switch (
$avalue) {
            case
self::THROW_CUSTOM:
                
// 抛出自定义异常
                
throw new MyException('1 is an invalid parameter', 5);
                break;

            case
self::THROW_DEFAULT:
                
// 抛出默认的异常
                
throw new Exception('2 isnt allowed as a parameter', 6);
                break;

            default:
                
// 没有异常的情况下,创建一个对象
                
$this->var = $avalue;
                break;
        }
    }
}


// 例子 1
try {
    
$o = new TestException(TestException::THROW_CUSTOM);
}
catch (MyException $e) {      // 捕获异常
    
echo "Caught my exception\n", $e;
    
$e->customFunction();
}
catch (Exception $e) {        // 被忽略
    
echo "Caught Default Exception\n", $e;
}

// 执行后续代码
var_dump($o);
echo
"\n\n";


// 例子 2
try {
    
$o = new TestException(TestException::THROW_DEFAULT);
}
catch (MyException $e) {      // 不能匹配异常的种类,被忽略
    
echo "Caught my exception\n", $e;
    
$e->customFunction();
}
catch (Exception $e) {        // 捕获异常
    
echo "Caught Default Exception\n", $e;
}

// 执行后续代码
var_dump($o);
echo
"\n\n";


// 例子 3
try {
    
$o = new TestException(TestException::THROW_CUSTOM);
}
catch (Exception $e) {        // 捕获异常
    
echo "Default Exception caught\n", $e;
}

// 执行后续代码
var_dump($o);
echo
"\n\n";


// 例子 4
try {
    
$o = new TestException();
}
catch (Exception $e) {        // 没有异常,被忽略
    
echo "Default Exception caught\n", $e;
}

// 执行后续代码
var_dump($o);
echo
"\n\n";
?>

add a note add a note User Contributed Notes
dexen at google dot me dot up
18-Sep-2006 09:45
Summary:
 * use destructors to perform a cleanup in case of exception.

PHP calls method __destruct()  on instance of class when variable storing the instance goes out-of-scope (or gets unset). This works for function leave by Exception, aside of plain return. (same as for C++, AFAIK)

aFunction() {
$i = new LockerClass();
throw new MinorErrorEx('Warn user & perform some other activity');
// $i->__destruct() gets called before stack unwind begins, unlocking whatever get locked by new LockerClass();
 return $bar;
}

(A lengthy) example:

Let's say you need to perform a series of operaions on SQL database that should not get disrupted. You lock the tables:
<?php
function updateStuff() {
  
DB::query('LOCK TABLES `a`, `b`, `c` WRITE');
  
/* some SQL Operations */
  
someFunction();
  
/* more SQL Operations */
  
DB::query('UNLOCK TABLES');
}
?>

Now, let's supouse that someFunction() may throw an exception. This would leave us with the tables locked, as the second DB::query() will not get called. This pretty much will cause the next query to fail. You can do it like:
<?php
function updateStuff() {
  
DB::query('LOCK TABLES `a`, `b` WRITE');
  
/* some SQL Operations */
  
try {
      
someFunction(); }
  
catch ( Exception $e ) {
      
DB::query('UNLOCK TABLES');
      
throw $e;
   }
  
/* more SQL Operations */
  
DB::query('UNLOCK TABLES')
}
?>

However, this is rather ugly as we get code duplication. And what if somebody later modifies updateStuff() function in a way it needs another step of cleanup, but forget to add it to catch () {}? Or when we have multiple things to be cleaned up, of which not all will be valid all the time?
 
My solution using destructor: i create an instance of class DB holding a query unlocking tables which will be executed on destruction.

<?php
function updateStuff() {
  
$SQLLocker = DB::locker( /*read lock list*/array('a', 'b'), /*write lock list*/array('b') );
  
/* some SQL Operations */
  
someFunction();
  
/* $SQLLocker gets destructed there if someFunction() throws an exception */
  
DB::query('UNLOCK TABLES');
  
/* other SQL Operations */
   /* $SQLLocker gets destructed there if someFunction() does not throw an exception */
}

class
DB {
   function
locker ( $read, $write ) {
      
DB::query( /*locks*/);
      
$ret = new DB;
      
$ret->onDestruct = 'UNLOCK TABLES';
       return
$ret;
   }

   function
_destructor() {
       if (
$this->onDestruct )
          
DB::query($this->onDestruct);
   }
}
?>
jazfresh at hotmail.com
08-Aug-2006 01:18
Sometimes you want a single catch() to catch multiple types of Exception. In a language like Python, you can specify multiple types in a catch(), but in PHP you can only specify one. This can be annoying when you want handle many different Exceptions with the same catch() block.

However, you can replicate the functionality somewhat, because catch(<classname> $var) will match the given <classname> *or any of it's sub-classes*.

For example:

<?php
class DisplayException extends Exception {};
class
FileException extends Exception {};
class
AccessControl extends FileException {}; // Sub-class of FileException
class IOError extends FileException {}; // Sub-class of FileException

try {
  if(!
is_readable($somefile))
    
throw new IOError("File is not readable!");
  if(!
user_has_access_to_file($someuser, $somefile))
    
throw new AccessControl("Permission denied!");
  if(!
display_file($somefile))
    
throw new DisplayException("Couldn't display file!");

}
catch (FileException $e) {
 
// This block will catch FileException, AccessControl or IOError exceptions, but not Exceptions or DisplayExceptions.
 
echo "File error: ".$e->getMessage();
  exit(
1);
}
?>

Corollary: If you want to catch *any* exception, no matter what the type, just use "catch(Exception $var)", because all exceptions are sub-classes of the built-in Exception.
linus at flowingcreativity dot net
20-Jul-2006 05:06
Here is a basic example of a way to mimick the convenience of exception handling in PHP4:

<?php

do {
   if (!
$test_condition) {
      
$error = 'test condition failed';
       break;
   }
  
   if (!
test_function()) {
      
$error = 'test function failed';
       break;
   }
  
   echo
'success!';
  
} while (
false);

if (isset(
$error)) {
   echo
$error;
}

?>

Obviously this falls far short of PHP5 real exception handling in terms of normalisation.  Also, $error won't propogate up the call stack like a real exception (i.e. test_function() can't itself throw an exception, nor call 'break').  But for me, the most important thing about exception handling is to be able to run through code and deal with errors separately, and not have to have a million nested IFs.  Compare that code to this:

<?php

if (!$test_condition) {
  
$error = 'test condition failed';
} elseif (!
test_function()) {
  
$error = 'test function failed';
} else {
   echo
'success!';
}

if (isset(
$error)) {
   echo
$error;
}

?>

At first this seems no more cumbersome, but what if test_function took arguments that required complicated preparation?  You would need a mess like this:

<?php

if (!$test_condition) {
  
$error = 'test condition failed';
} else {
  
$fooRes = mysql_query('SELECT foo FROM bar LIMIT 1');
  
$fooRow = mysql_fetch_assoc($fooRes);
  
   if (!
test_function($fooRow)) {
      
$error = 'test function failed';
   } else {
       echo
'success!';
   }
}

?>

Obviously this could get out of hand quickly.  In the first example, you can just prepare the argument before the 'if (!test_function($fooRow))', and on the same nesting level.

This method is also somewhat more flexible in that you can generate 'exceptions', and then deal with them much later (which may be desirable if they aren't really threatening).  In PHP5, a catch block must always follow a try block directly.
tatarynowicz at gmail dot com
10-Jul-2006 09:26
Carlos Konstanski: You can't directly extract previous lexical environment in non-functional languages.

What you probably _can_ do is to create a function that dumps the current lexical environment (to an external file or to stdout) and then use it to recreate past states.

function log_lex ($vars, $functions) {
  $f = fopen('lex_env.log', 'a');
  fwrite($f, serialize(array($vars, $functions, get_defined_constants(), debug_backtrace()));
  fclose($f);
}

log_lex (get_defined_vars(), get_defined_functions());

Writing the parser/browser for dumps is the difficult part. Then just sprinkle the code with log_lex() and see how the program runs.

Sure, it works better in Lisp, but Lisp isn't supported on the webservers I work with, and making PHP more like Lisp would probably make it as popular as Lisp is.
14-May-2006 07:49
@armenio at inmidiaweb dot com dot br:
When echoing into JavaScript code must use htmlspecialchars(addslashes($this->msg)) - think what would happen if msg was "')</script>";
armenio at inmidiaweb dot com dot br
11-May-2006 04:23
<?php
class alert{
  
  
public $return;
  
public $msg;
  
  
public function __construct($value){
      
$this->msg = $value;
   }
  
public function OutPut(){
      
$this->return  = '<script language="JavaScript">';
      
$this->return .= '    alert("'.$this->msg.'")';
      
$this->return .= '</script>';
       return
$this->return;
   }
}
$msg = new alert('classe criada com sucesso');
$msg->OutPut();
?>
fjoggen at gmail dot com
27-Apr-2006 04:58
This code will turn php errors into exceptions:

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
  
throw new ErrorException($message, 0, $severity, $filename, $lineno);
}

set_error_handler('exceptions_error_handler');
?>

However since <?php set_error_handler()?> doesn't work with fatal errors, you will not be able to throw them as Exceptions.
gerry03 at 4warding dot com
14-Nov-2005 01:39
Good PHP5 article about exceptions. I have read quite a few now and this is the only one I've liked:

http://www.andreashalter.ch/phpug/20040115/
Carlos Konstanski
30-Sep-2005 01:17
Being able to catch the exception up the call stack from where it is thrown is a good idea, in that it lets you handle the exception closer to where it ought to be handled - in the calling code.  Not as good as common lisp though, where you can call a condition handler that resides up the stack without actually unwinding the stack.  With this additional feature, you have the lexical environment at the point where the exception occured, combined with the relocation of the handler to a place where the lower-level function's failure can be addressed from the standpoint of the calling code.

Even so, this fluke of PHP that lets you place the try...catch block up the stack from the actual point of error could be used to implement a neat shift in error-handling architecture, where the caller gets to decide how to proceed after an error.
jd at wuputah dot com
07-May-2005 10:15
PHP5 supports exception throwing inside a function, and catching it outside that function call. There is no mention of this in documentation but it works just fine, as tested by this sample code:

<?php

function exceptionFunction() {
      
throw new Exception("Throwing an exception!");
}

try {
      
exceptionFunction();
}
catch (Exception $e) {
       echo
"Exception caught!\n";
}

?>

The result in PHP 5.0.3 is "Exception caught!"

Further tests show that nested functions with exceptions, methods throwing exceptions, etc all work the same way. This is like declaring all classes (or methods) in Java as "class ClassName throws Exception". While I consider this a good thing, you should be aware that any thrown exception will propagate up your stack until it is either caught or runs out of stack.
ravindra at gatewaytechnolabs dot com
29-Oct-2004 04:34
<?php
/*I add here example for nested Exception Handling.*/
 
try
 
{
  
try
  
{
    
throw new exception();
   }
  
catch(exception $m)
   {
   print
$m;
   print
"<br>";
   print
"inner exception"."<br>";
   }
  
throw new exception();
 }
 
catch(exception $e)
 {
  print
$e;
  print
"outer exception";
 }
 
?>
gomikochar at yahoo dot com
10-Oct-2004 10:00
To re-throw an exception, you must specify the name of the variable after throw in the catch block, i.e.,

<?php

try
{
 
try {
  
throw new Exception("Unknown error");
  }
 
catch (Exception $ie) {  // Inner Catch Block
  
throw// this will NOT work; parse error
  
throw $ie// this will re-throw the exception to the outer catch block
 
}
}
catch (Exception $oe) {  // Outer Catch Block
 
echo $oe;
}

?>
php5 at grapio dot nl
07-Oct-2004 09:16
The base exception class, which is build in PHP5 has also a function getLine(). This is as expected if you look at the class define. But it is not noticed there.
I had just this code:
<?php
try
{
    
throw new Exception("test")
}
catch (Exception $e)
{
  echo
$e->getLine()
}
?>
And this worked.
ravindra at gatewaytechnolabs dot com
01-Oct-2004 07:23
Like Java php5 also supports nesting of try catch.
moirae at centrum dot cz
21-Aug-2004 09:16
Really good article about exceptions in php5: http://www.zend.com/php5/articles/php5-exceptions.php