|
撰/看場人
PHP5新功能初探系列- 新對象模型(Object Model)
- 異常處理機制(Exception Handling)
- 新的擴充程序庫(Extensions)
- 由PHP4升級到PHP5
- PHP5,未來會是如何?
本篇文章的目錄- PHP5以前的異常處理
- Script-Level處理異常
- Error flag處理異常
- PHP5以前的問題…
- PHP5的異常處理
- 概觀
- 內建的 Exception 類
- throw 關鍵字
- try-catch
- 繼承 Exception 類來分辨異常種類
- 把 Exception 向上拋?!
0. 前言近年很多流行的編程語言如 Java 等都支援異常處理 (Exception Handling) 的語法,而全新推出的 PHP5 亦支援了這種方便的機制了。異常經常都會發生,例如打開文件失敗(可能是因為檔案不存在)、數據類型不符(type mismatch)、數據庫連接失敗等等,處理這些異常的程式碼,往往佔了你程序的一大部份,如何有效率地對異常進行處理便變成了重要的課題。在本篇文章中,我們會粗略看看PHP5在處理錯誤上如何比往時優勝。
1. PHP5以前的異常處理1.1 Script-Level處理異常在PHP5以前,你很可能只是用變數記錄程序是否發生異常,或者用 trigger_error() 和 die() 這些函數去處理異常,例如: <?php
function errorHandler($errnum, $errmsg, $file, $lineno) {
if($errnum == E_USER_ERROR) {
// Do Something...
exit();
}
}
if (!file_exists($file_path)) {
die("Cannot find $file_path\n");
}
if (!class_exists($cls)) {
trigger_error("class $cls does not exist", E_USER_ERROR);
}
$handler = set_error_handler('errorHandler');
?>
上例示範了使用trigger_error() 和 die() 兩個函數處理異常的方法,這樣真的很方便,但卻不夠彈性。因為一來你的異常處理程序便會受你的program logic正接影響,二來遇到異常時也不見得一定要把程序立即結束,程序的用戶(client)可能想自行處理這些異常也說不定呢!
1.2 Error flag處理異常當然,你亦可以用 error flag 去記錄程序有否出錯,那麼該程序的用戶便可自行定義如何去處理該異常了,例如: <?php
// Your module may contain this 2 functions:
function setError($method, $msg) {
global $error_str;
$error_str = "{$method}(): $msg";
}
function doSomething() {
$file_path = "foobar.php";
if (!file_exists($file_path)) {
setError(__FUNCTION__, "Cannot find $file_path\n");
return false;
}
return true;
}
?>
用上 global variable 當然不是好建議。你或許會將以上處理異常的段碼定義成一個 class ,然後讓其他 class 去繼承,然而在單一繼承的 PHP 中,你便可能要改為將它定義成 interface (關於class、interface、繼承可參考本系列第一章)。 雖然使用 error flag 會較具彈性,但這樣用戶必須每次使用你的函數時也檢查有沒有出現異常情況,這就「污染」了模組(或者類)的介面了。(在編程語言的理論裡這是造成「副作用」的一個情況)
1.3 PHP5以前的問題…總括來說,這種 inline 的 error checking 方法並不統一,這造成市面上混亂的異常處理局面,對於建立更彈性和可重用的系統來說是很不利的,事實上我們在處理異常的問題上: - 應在單一地方處理多種 error conditions,把 program flow 和異常處理分開,這是建立彈性及可重用程序的重要概念。
- 應容許將異常委託(delegate)給客戶作出彈性處理。
- 不應該佔用了函數的 return value。
2. PHP5的異常處理2.1 概觀PHP5建立了一套完整統一的異常處理機制,使用 try-catch 語法配合 throw 關鍵字和 Exception 這內部定義的類,我們可以將1.2的程式碼改寫一下: <?php
// Your module may contain this function:
function doSomething() {
$file_path = "foobar.php";
if (!file_exists($file_path)) {
throw new Exception("Cannot find $file_path");
}
}
?>
而這模組的用戶大約會這樣的使用 doSomething(): <?php
try {
doSomething();
} catch (Exception $e) {
print $e->getMessage();
exit();
}
?>
當程序遇上異常時,我們先用 new 建立一個 Exception 類的實例,再使用 throw 關鍵字將之拋出。這代表調用 doSomething() 這程序的用戶將有可能接收到一個 Exception 的實例,那麼我們就用 try { ... } catch() { ... } 來將這個異常接住並處理。 這樣的機制和 1.2 的情況很接近, Exception 就好像 error flag 一樣被傳回到用戶處,用戶調用 catch 就像之前使用 $error_str 一樣,可檢查到是否出現異常並提供處理的方法,但這次不但解決了佔用 return value 的問題,更可將異常處理和程式流程分開來了。
2.2 內建的 Exception 類這在PHP5內建的類有以下的方法:
| __construct() | 構造函數,需要提供一訊息字串及一 optional 的整數 error code。 | | getMessage() | 傳回構造時提供的訊息字串。 | | getCode() | 傳回構造時提供的 error code。 | | getFile() | 傳回發生異常的檔案的路徑。 | | getLine() | 傳回發生異常的所在行數。 | | getTrace() | 傳回一載有異常發生時的步驟的數組(array)。 | | getTraceAsString() | 同上,但以字串形式傳回。 |
任何時候你都可以創建一個 Exception 的對象:
$e = new Exception("Could not open $file_path\n"); 下面是個更詳細的例子 (php5.chp2.2.2.phps) 及其在 PHP5 server上的運行結果 (php5.chp2.2.2.php) : <?php
function shitHappens() {
// Always cause exception
throw new Exception("Shit Happens!!", 666);
}
function dummyFunc() {
try {
// Here we will get an exception
shitHappens();
} catch (Exception $e) {
// Caught! But I re-throw it to my caller
throw $e;
}
}
function mainFlow() {
try {
// This gets an exception, too
dummyFunc();
} catch (Exception $e) {
echo("<bi>".get_class($e)."</b><br>");
echo("<b>{$e->getMessage()}({$e->getCode()})</b><hr>");
echo("file: {$e->getFile()}<br>");
echo("line: {$e->getLine()}<br><br>");
echo($e->getTraceAsString());
die;
}
}
mainFlow();
?>
留意 getTraceAsString() 的運行結果,它會傳回由發生異常到異常被拋出傳送的經過,如上例中首先是 #0 的 shitHappens() 出現異常,再被拋到 #1 的dummyFunc() 中,接著由 #2 的 mainFlow() 接收並處理。至於為何 dummyFunc() 要將異常重新拋出,我會在 2.6 再談。 在此補充一下 getTrace() 的用法,它會傳回一個 multi-dimensional 的數組,第一格代表發生異常所在的資料(上例中的#0),接著是首個接收該異常的地方(上例中的#1),如忘類推。而每個資料本身也是一個數組,並有以下幾項:
| file | 發生異常的檔案的路徑。 | | line | 發生異常的所在行數。 | | function | 發生異常的所在的函數名稱。 | | class | 發生異常的所在的類的名稱。 | | type | 調用方式,'::'代表static,'->'代表instance。 | | args | 調用函數的參數。 |
2.3 throw 關鍵字你可以隨時用 throw 拋出一個你所創建的 Exception 對象:
throw new Exception("Something happened...", 666 ); 當 throw 被使用,其所在的程序會立即被打斷,並將拋出的Exception對象送到用戶(即送到調用發生異常程序的程序)手上,最後由用戶使用 catch 將之處理。 但若用戶沒有 catch 這異常時會怎樣呢?結果PHP5會 default 地將之以 fatal error 的形式處理。(2.6部份會有一些補充說明)
2.4 try-catch這套語法用一個 try{} 去包含一段可能會拋出異常的程式碼,並用一個或多個 catch() {} 去處理捕捉到的異常,提供解決的程式碼(例如釋放資源)。每當接收到 Exception 時,try{} 區塊便會立即停止執行,程序會立即進行對應的 catch() {} 區塊中。 try-catch 機制的最大好處是將異常處理的程式碼從正常流程中分離,避免異常處理陷入於程序流程中。另外,try{} 區塊可以包含很多行可能發生異常的程式碼,而全部都可用對應的 catch(){} 處理,達到統一處理的效果。範例如下: <?php
try {
$a = methodA(); // This may cause exception
$someObj->doSomething(); // This may also cause exception
} catch (Exception $e) {
// Here can handle either exceptions
echo( $e->getMessage() );
exit();
}
?>
2.5 繼承 Exception 類來分辨異常種類問題來了,若我們的 try{} 區塊內有很多句有機會拋出不同的異常,也我們 catch 的時候如何將之區分?其中一個可行的方法是使用 error code,前面提過 Exception 類在構造時可接受一個整數作 error code,那你便可在 catch 到 Exception 時檢查其 error code 以作分辨。 但以數字代表不同種類的異常並不便於人類作分辨,是以我們可以繼承 Exception 類來分辨異常種類,這個方法的好處是還包括:
- 可清楚地為你模組的用戶提供該異常的類型資料。
- 可擴展 Exception 類的功能,使你定義的子類能專門地處理特定情況下的異常情況。
以下是繼承 Exception 類來分辨異常的範例 (php5.chp2.2.5.phps) 及其在 PHP5 server上的運行結果 (php5.chp2.2.5.php) : <?php
// SubClassing Exception to handle specific errors
class fileNotExistException extends Exception { }
class notWellFormedException extends Exception {
function indicateSyntaxError() {
// Only a dummy function for demo
echo("<i>Um...The XML's line XX has syntax error XXXXXX...</i><br>\n");
}
}
class notValidException extends Exception {
function indicateValidationError() {
// Only a dummy function for demo
echo("<i>Ar...The XML element in line XX is not defined in the specified DTD...</i><br>\n");
}
}
// A demo class that could cause exceptions
class XMLReader {
function __construct($file_path) {
if (!file_exists($file_path)) {
throw new fileNotExistException("<b>Exception:</b> Cannot find $file_path!!<br>\n");
}
}
function parseXMLDocument($well_formed, $valid) {
// Only a dummy function to make exceptions
if (!$well_formed) {
throw new notWellFormedException("<b>Exception:</b> The XML isn't well formed!!<br>\n");
}
if (!$valid) {
throw new notValidException("<b>Exception:</b> The XML isn't valid!!<br>\n");
}
// Suppose there could be still some circumstances that causes exception
$something_goes_wrong = true;
if ($something_goes_wrong) {
throw new Exception("<b>Exception:</b> Unknow error occurs...<br>\n");
}
}
}
function demo($file, $well_formed, $valid) {
try {
$obj = new XMLReader($file);
$obj->parseXMLDocument($well_formed,$valid);
} catch (fileNotExistException $e) {
echo($e->getMessage());
} catch (notWellFormedException $e) {
echo($e->getMessage());
echo($e->indicateSyntaxError());
} catch (notValidException $e) {
echo($e->getMessage());
echo($e->indicateValidationError());
} catch (Exception $e) {
echo($e->getMessage());
}
echo("<hr>\n");
}
// 4 different versions of causing exceptions, all caught in demo()
demo('not_exist.xml',true,true);
demo('exist.xml',false,true);
demo('exist.xml',true,false);
demo('exist.xml',true,true);
?>
上例中,我們用繼承 Exception 的方法將其中3種異常分門別類,有些 ( notWellFormedException 和 notValidException ) 更提供了專門的處理方法 ( 如 indicateSyntaxError() ),當 XMLReader 遇到不同問題時就拋出不同的異常,而在 demo() 的 try-catch 區塊中,我們可定義多個 catch() 語句來接收不同類型的異常並加以處理。當遇到未有分類的異常時,我們可照以往的直接拋出 Exception 類,但請留意,catch(Exception $e) {} 必須是最後一個 catch 區塊,因為其他的異常均是 Exception 的子類,若你將該區塊置於前方,則所有 Exception 及其子類都會被該區塊接收!所以一般地 catch(Exception $e) {} 都會被置於最後,以網羅所有的異常(當然,你可以重新拋出該異常供客戶再處理,這會在 2.6 說明)。
2.6 把 Exception 向上拋?!有些情況下,一個模組的用戶可能無法或者不希望處理該模組拋出的某些異常,那怎麼辦呢?其實你大可以將這些不想處理的異常重新拋出(像2.2的例子裡的dummyFunc()一樣),讓再上一層的用戶來處理(這就叫推卸責任!),請看下面的例子,是2.5的修改版 (php5.chp2.2.6.phps) 及其在 PHP5 server上的運行結果 (php5.chp2.2.6.php) : <?php
// SubClassing Exception to handle specific errors
class fileNotExistException extends Exception { }
class notWellFormedException extends Exception {
function indicateSyntaxError() {
// Only a dummy function for demo
echo("<i>Um...The XML's line XX has syntax error XXXXXX...</i><br>\n");
}
}
class notValidException extends Exception {
function indicateValidationError() {
// Only a dummy function for demo
echo("<i>Ar...The XML element in line XX is not defined in the specified DTD...</i><br>\n");
}
}
// A demo class that could cause exceptions
class XMLReader {
function __construct($file_path) {
if (!file_exists($file_path)) {
throw new fileNotExistException("<b>Exception:</b> Cannot find $file_path!!<br>\n");
}
}
function parseXMLDocument($well_formed, $valid) {
// Only a dummy function to make exceptions
if (!$well_formed) {
throw new notWellFormedException("<b>Exception:</b> The XML isn't well formed!!<br>\n");
}
if (!$valid) {
throw new notValidException("<b>Exception:</b> The XML isn't valid!!<br>\n");
}
// Suppose there could be still some circumstances that causes exception
$something_goes_wrong = true;
if ($something_goes_wrong) {
throw new Exception("<b>Exception:</b> Unknow error occurs...<br>\n");
}
}
}
// Now demo() doesn't wanna handle notWellFormedException and notValidException
function demo($file, $well_formed, $valid) {
try {
$obj = new XMLReader($file);
$obj->parseXMLDocument($well_formed,$valid);
} catch (fileNotExistException $e) {
echo("<u>Inside demo():</u> ");
echo($e->getMessage());
echo("<hr>\n");
} catch (Exception $e) {
// Exception other than fileNotExistException will be rethrown
echo("<u>Inside demo(), rethrowing exception:</u> ");
echo($e->getMessage());
echo("<hr>\n");
// Rethrow exception!
throw $e;
}
}
// 4 different versions of causing exceptions, all caught in demo()
try {
demo('not_exist.xml',true,true);
demo('exist.xml',false,true); // Caught rethrown exception here
demo('exist.xml',true,false); // Won't be executed in this example
demo('exist.xml',true,true); // Won't be executed in this example
} catch (notWellFormedException $e) {
echo("<u>Outermost (Main):</u> ");
echo($e->getMessage());
echo($e->indicateSyntaxError());
} catch (notValidException $e) {
echo("<u>Outermost (Main):</u> ");
echo($e->getMessage());
echo($e->indicateValidationError());
}
?>
這個例子中,demo() 不想處理 notWellFormedException 和 notValidException 兩種異常,就可讓 catch (Exception $e) {} 把它們通通網羅,然後再新拋出往再上一層的用戶,最後最外層的程式碼便會接收到 demo('exist.xml',false,true); 所拋出的異常 notWellFormedException ,處理後整個程序變完結了,之後的句子就不會再執行 (這是try-catch的特點)。 有一點要提出,PHP5 會自動 re-throw 任何你沒有 catch 的異常往上一層用戶,所以上例中整個 catch (Exception $e) {...} 區塊(第49-56行)都是可以省略的。 這裡我們介紹了 exception flow 的概念,由引發異常的最底層可一直流到最外層去,這容許你在最適當的地方才處理異常。一般來說,在發生異常的地方就正接將之處理並不是個好做法。 結語總結來說,這套面善的異常處理機制可令你的錯誤處理代碼都可集中地編寫,使你的程式更readable。而你將不必再佔用 return value 作 error code,這表示你的程序將會更潔淨,你的 program logic 就不再被異常處理「污染」了。在下一章,我們會淺談PHP5的各個新增的重要 extension。
相關連結:
- PHP Official Web-site
- PHP5新功能初探系列(一) 新對象模型(Object Model)
- 內建 Exception 類示範 (php5.chp2.2.2.php)
- 繼承 Exception 類分辨異常示範 (php5.chp2.2.5.php)
- 把 Exception 向上層重拋示範 (php5.chp2.2.6.php)
相關文件:
- php5.chp2.2.2.phps源碼
- php5.chp2.2.5.phps源碼
- php5.chp2.2.6.phps源碼
發表日期:2004-08-08
|