簡介:這是PHP 5.0異常處理機制深度探索的詳細頁面,介紹了和php,有關的知識、技巧、經驗,和一些php源碼等。
class='pingjiaF' frameborder='0' src='http://biancheng.dnbcw.info/pingjia.php?id=324026' scrolling='no'>
本文面向希望瞭解PHP5異常處理機制的程式員。閱讀本文你需要具有一定物件導向編程和PHP基礎。
PHP5內建的異常類需要有以下成員方法:
__construct() |
建構函式,需要一個出錯資訊和一個可選的整型錯誤標記作參數 |
getMessage() |
取得出錯資訊 |
getCode() |
出錯的代碼 |
getFile() |
異常發生的檔案 |
getLine() |
異常發生的行數 |
getTrace() |
跟蹤異常每一步傳遞的路線,存入數組,返回該數組 |
getTraceAsString() |
和getTrace()功能一樣,但可以將數組中的元素轉成字串並按一定格式輸出 |
可以看出來,Exception 類的結構和Pear_Error 很相似。當你的指令碼中遇到一個錯誤,你可以建立你的異常對象:
$ex = new Exception( "Could not open $this->file" );
Exception類的建構函式將接受一個出錯資訊和一個錯誤碼。
使用 throw關鍵字
建立一個Exception對象後你可以將對象返回,但不應該這樣使用,更好的方法是用throw關鍵字來代替。throw用來拋出異常:
throw new Exception( "my message", 44 );
throw 將指令碼的執行中止,並使相關的Exception對象對客戶代碼可用。
以下是改進過的getCommandObject() 方法:
index_php5.php
<?php // PHP 5
require_once('cmd_php5/Command.php');
class CommandManager {
private $cmdDir = "cmd_php5";
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
throw new Exception("Cannot find $path");
}
require_once $path;
if (!class_exists($cmd)) {
throw new Exception("class $cmd does not exist");
}
$class = new ReflectionClass($cmd);
if (!$class->isSubclassOf(new ReflectionClass('Command'))) {
throw new Exception("$cmd is not a Command");
}
return new $cmd();
}
}
?>
代碼中我們使用了PHP5的反射(Reflection)API來判斷所給的類是否是屬於Command 類型。在錯誤的路徑下執行本指令碼將會報出這樣的錯誤:
Fatal error: Uncaught exception 'Exception' with message 'Cannot find command/xrealcommand.php' in /home/xyz/BasicException.php:10
Stack trace:
#0 /home/xyz/BasicException.php(26):
CommandManager->getCommandObject('xrealcommand')
#1 {main}
thrown in /home/xyz/BasicException.php on line 10
預設地,拋出異常導致一個fatal error。這意味著使用異常的類內建有安全機制。而僅僅使用一個錯誤標記,不能擁有這樣的功能。處理錯誤標記失敗只會你的指令碼使用錯誤的值來繼續執行。
Try-catch 語句
為了進一步處理異常,我們需要使用try-catch語句―包括Try語句和至少一個的catch語句。任何調用 可能拋出異常的方法的代碼都應該使用try語句。Catch語句用來處理可能拋出的異常。以下顯示了我們處理getCommandObject()拋出的異常的方法:
index_php5.php 後半段
<?php
// PHP 5
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
} catch (Exception $e) {
print $e->getMessage();
exit();
}
?>
可以看到,通過結合使用throw關鍵字和try-catch語句,我們可以避免錯誤標記“汙染”類方法返回的值。因為“異常”本身就是一種與其它任何對象不同的PHP內建的類型,不會產生混淆。
如果拋出了一個異常,try語句中的指令碼將會停止執行,然後馬上轉向執行catch語句中的指令碼。
如果異常拋出了卻沒有被捕捉到,就會產生一個fatal error。
處理多個錯誤
在目前為止異常處理看起來和我們傳統的作法―檢驗返回的錯誤標識或對象的值沒有什麼太大區別。讓我們將CommandManager處理地更謹慎,並在建構函式中檢查command目錄是否存在。
index_php5_2.php
<?php
// PHP 5
require_once('cmd_php5/Command.php');
class CommandManager {
private $cmdDir = "cmd_php5";
function __construct() {
if (!is_dir($this->cmdDir)) {
throw new Exception("directory error: $this->cmdDir");
}
}
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
throw new Exception("Cannot find $path");
}
require_once $path;
if (!class_exists($cmd)) {
throw new Exception("class $cmd does not exist");
}
$class = new ReflectionClass($cmd);
if (!$class->isSubclassOf(new ReflectionClass('Command'))) {
throw new Exception("$cmd is not a Command");
}
return new $cmd();
}
}
?>
這裡有兩個地方的調用可能導致程式出錯(__construct()和getCommandObject())。儘管如此,我們不需要調整我們的客戶代碼。你可以在try語句中增添眾多內容,然後在catch中統一處理。如果CommandManager 對象的建構函式拋出一個異常,則try語句中的執行中止,然後catch語句被調用捕捉相關的異常。同樣地,getCommandObject()也是如此。這樣,我們有同時存在兩個潛在的引發錯誤的地方,和一個唯一的語句來處理所有的錯誤。這讓我們的代碼看起來更加整潔,又可以滿足錯誤處理的要求。和前面提到的PHP的傳統的錯誤方法相比,顯然很有優勢。
index_php5_2.php 後半段
注意:儘管和index_php5.php相比,前半段代碼有兩個可能出錯的地方,這段代碼和index_php5.php的後半段完全相同。
<?php
// PHP 5
try {
$mgr = new CommandManager(); // potential error
$cmd = $mgr->getCommandObject('realcommand');
// another potential error
$cmd->execute();
} catch (Exception $e) {
// handle either error here
print $e->getMessage();
exit();
}
?>
還有一個地方我們沒有提到。我們怎樣區分不同類型的錯誤?例如,我們可能希望用一種方法來處理找不到目錄的錯誤,而用另一種方法來處理非法的command類。
Exception類可以接受一個可選的整型的錯誤標識,這是在catch語句中區分不同錯誤類型的一個方法。
index_php5_3.php
<?php
// PHP 5
require_once('cmd_php5/Command.php');
class CommandManager {
private $cmdDir = "cmd_php5";
const CMDMAN_GENERAL_ERROR = 1;
const CMDMAN_ILLEGALCLASS_ERROR = 2;
function __construct() {
if (!is_dir($this->cmdDir)) {
throw new Exception("directory error: $this->cmdDir", self::CMDMAN_GENERAL_ERROR);
}
}
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
throw new Exception("Cannot find $path", self::CMDMAN_ILLEGALCLASS_ERROR);
}
require_once $path;
if (!class_exists($cmd)) {
throw new Exception("class $cmd does not exist", self::CMDMAN_ILLEGALCLASS_ERROR);
}
$class = new ReflectionClass($cmd);
if (!$class->isSubclassOf(new ReflectionClass('Command'))) {
throw new Exception("$cmd is not a Command", self::CMDMAN_ILLEGALCLASS_ERROR);
}
return $class->newInstance();
}
}
?>
通過傳遞 CMDMAN_ILLEGALCLASS_ERROR和 CMDMAN_GENERAL_ERROR其中之一的參數給我們拋出的異常對象,我們就可以讓客戶代碼區分不同類型的錯誤,並定義不同的處理策略。
index_php5_3.php
<?php // PHP 5
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
} catch (Exception $e) {
if ($e->getCode() == CommandManager::CMDMAN_GENERAL_ERROR) {
// no way of recovering
die($e->getMessage());
} else if ($e->getCode() == CommandManager::CMDMAN_ILLEGALCLASS_ERROR) {
error_log($e->getMessage());
print "attempting recovery\n";
// perhaps attempt to invoke a default command?
}
}
?>
我們也可以用另一種方法來實現這樣的效果―從最根本的Exception類中派生出代表不同類型異常的子類,再拋出和捕捉。
Exception類的子類
有兩個理由讓我們想要從Exception類中派生中子類:
1. 讓子類提供自訂的功能;
2. 區分不同類型的異常;
看第二個例子。使用CommandManager類時我們可能會產生兩個錯誤:一個是一般性的錯誤如找不到目錄,另一個是找不到或無法產生Command對象。這樣我們需要針對這兩個錯誤來定義兩種異常子類型。
index_php5_4.php
<?php
// PHP 5
require_once('cmd_php5/Command.php');
class CommandManagerException extends Exception{}
class IllegalCommandException extends Exception{}
class CommandManager {
private $cmdDir = "cmd_php5";
function __construct() {
if (!is_dir($this->cmdDir)) {
throw new CommandManagerException("directory error: $this->cmdDir");
}
}
function getCommandObject($cmd) {
$path = "{$this->cmdDir}/{$cmd}.php";
if (!file_exists($path)) {
throw new IllegalCommandException("Cannot find $path");
}
require_once $path;
if (!class_exists($cmd)) {
throw new IllegalCommandException("class $cmd does not exist");
}
$class = new ReflectionClass($cmd);
if (!$class->isSubclassOf(new ReflectionClass('Command'))) {
throw new IllegalCommandException("$cmd is not a Command");
}
return $class->newInstance();
}
}
?>
當我們的類不能找到正確的command目錄時,將拋出一個CommandManagerException異常;當在產生Command對象時產生錯誤,則getCommandObject()方法將拋出一個IllegalCommandException異常。注意存在多個可能導致拋出IllegalCommandException異常的原因(如未找到檔案,或在檔案中未找到正確的類)。我們將前兩個例子結合起來並為IllegalCommandException提供整型的錯誤標識常量來代表不同類型的出錯原因。
現在CommandManager類已經具備了處理這多種出錯情況的能力,我們可以增加新的catch語句來匹配不同的錯誤類型。
index_php5_4.php 後半段
<?php // PHP 5
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
} catch (CommandManagerException $e) {
die($e->getMessage());
} catch (IllegalCommandException $e) {
error_log($e->getMessage());
print "attempting recovery\n";
// perhaps attempt to invoke a default command?
} catch (Exception $e) {
print "Unexpected exception\n";
die($e->getMessage());
}
?>
如果CommandManager 對象拋出一個CommandManagerException異常,則相對應的catch語句將會執行。每個catch語句的參數就像是一個匹配測試一樣,第一個發生匹配的catch語句將會執行,而不執行其它的catch語句。所以,你應當將針對特定異常的catch語句寫在前面,而將針對一般性的異常的catch語句寫在後面。
如果你將catch語句這樣寫:
<?php
// PHP 5
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
} catch (Exception $e) {
print "Unexpected exception\n";
die($e->getMessage());
} catch (CommandManagerException $e) {
die($e->getMessage());
} catch (IllegalCommandException $e) {
error_log($e->getMessage());
print "attempting recovery\n";
// perhaps attempt to invoke a default command?
}
?>
那麼當異常拋出時,不管是什麼異常第一個catch語句catch (Exception $e){}將總是被執行。這是由於任何異常都從屬於Exception類型,所以總是匹配。這就達不到我們所要的針對特定異常進行不同處理的目的。
如果你在捕捉特定類型的異常,那麼在最後一個catch語句中捕捉Exception類型的異常是一個好主意。最後一個catch語句表示catch-all,捕捉所有異常。當然,你可能不想馬上處理異常,而是想要將它傳遞,然後在適當的時候處理。這是PHP的異常機制中另一個需要討論的地方。
異常的傳遞、重擲異常
如果我們已經觸發了一些在發生時無法馬上處理的異常,有一個很好的解決方案―將處理異常的責任交回給調用當前方法的代碼,也就是在catch語句中再次拋出異常(重擲異常)。這將使異常沿著方法的調用鏈向上傳遞。
index_php5_5.php
<?php
// PHP 5
class RequestHelper {
private $request = array();
private $defaultcmd = 'defaultcmd';
private $cmdstr;
function __construct($request_array=null) {
if (!is_array($this->request = $request_array)) {
$this->request=$_REQUEST;
}
}
function getCommandString() {
return ($this->cmdstr ? $this->cmdstr : ($this->cmdstr=$this->request['cmd']));
}
function runCommand() {
$cmdstr = $this->getCommandString();
try {
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject($cmdstr);
$cmd->execute();
} catch (IllegalCommandException $e) {
error_log($e->getMessage());
if ($cmdstr != $this->defaultcmd) {
$this->cmdstr = $this->defaultcmd;
$this->runCommand();
} else {
throw $e;
}
} catch (Exception $e) {
throw $e;
}
}
}
$helper = new RequestHelper(array(cmd=>'realcommand'));
$helper->runCommand();
?>
以上我們使用了RequestHelper類中的一段客戶代碼。RequestHelper用來處理使用者提供的請求資料。在建構函式中我們接受一個用來debug的數組。如果沒有接受到這個數組,類將使用$_REQUEST數組。無論哪個數組被使用,它都將分配給名為$request的變數。客戶代碼通過給出一個request數組的cmd元素,告知它想要執行的command。getCommandString()方法則測試一個名為$cmdstr的屬性。如果它是空的,則方法將$request中的cmd元素的內容分配給$cmdstr,並傳回值。如果不是空的,方法直接返回$cmdstr屬性的值。通過這樣的機制,command字串可以在RequestHelper類中被覆寫。
在最後我們將除IllegalCommandException外的所有異常對象都將交給更高一級的類來延後處理。我們在最後一個catch語句中再次拋出異常。
} catch (Exception $e) {
throw $e;
}
如果我們捕捉到一個IllegalCommandException 異常,我們首先嘗試去調用 一個預設的command。我們通過將$cmdstr屬性設定為與$defaultcmd等值,並重複地調用runCommand方法。如果$cmdstr和$defaultcmd字串已經相等,我們沒有什麼需要做的,則重擲異常。
事實上在 Zend引擎II將會自動重擲所有未匹配的異常,所以我們可以省略最後一個catch語句。這是CommandManager::getCommandObject()的最後一行:
return $class->newInstance();
這裡要注意兩個問題:
首先,我們假設CommandManager類的建構函式不需要參數。在本文中我們不討論需要參數的情況。
其次,我們假設command類(這裡是指我們自訂的realcommand)可以被執行個體化。如果建構函式被聲明為private,這個語句將拋出一個ReflectionException對象。如果我們沒有在RequestHelper中處理異常,則這個異常將被傳遞到調用RequestHelper的代碼中。如果一個異常被隱性地拋出,你最好在文檔中說明一下,或者手動地拋出這個異常--這樣其他的程式員使用你的代碼時容易處理可能發生的異常情況。
獲得異常相關的更多資訊
以下是用來格式化輸出異常資訊的代碼:
index_php5_6.php
<?php
// PHP 5
class Front {
static function main() {
try {
$helper = new RequestHelper(array(cmd=>'realcommand'));
$helper->runCommand();
} catch (Exception $e) {
print "<h1>".get_class($e)."</h1>\n";
print "<h2>{$e->getMessage()}
({$e->getCode()})</h2>\n\n";
print "file: {$e->getFile()}<br />\n";
print "line: {$e->getLine()}<br />\n";
print $e->getTraceAsString();
die;
}
}
}
Front::main();
?>
如果你的realcommand類無法被執行個體化(例如你將它的建構函式聲明為private)並運行以上代碼,你可以得到這樣的輸出:
ReflectionException Access to non-public constructor of class realcommand (0)
file: c:\MyWEB\Apache\htdocs\php5exception\index_php5_4.php
line: 31
#0 c:\MyWEB\Apache\htdocs\php5exception\index_php5_5.php(25): CommandManager->getCommandObject()
#1 c:\MyWEB\Apache\htdocs\php5exception\index_php5_6.php(10): RequestHelper->runCommand('realcommand')
#2 c:\MyWEB\Apache\htdocs\php5exception\index_php5_6.php(23): Front::main()
#3 {main}
你可以看到getFile()和getLine()分別返回傳生異常的檔案和行數。GetStackAsString()方法返回每一層導致異常發生的方法調用的細節。從#0一直到#4,我們可以清楚地看到異常傳遞的路線。
你也可以使用getTrace()方法來得到這些資訊,getTrace()返回一個多維陣列。第一個元素包含有異常發生的位置,第二個元素包含外部方法調用的細節,直到最高一層的調用。這個數組的每個元素本身也是一個數組,包含有以下幾個鍵名(key):
key |
含義 |
file |
產生異常的檔案 |
line |
產生異常的類方法所在行數 |
function |
產生異常的函數/方法 |
class |
調用的方法所在類 |
type |
調用類型:'::' 表示調用靜態類成員 '->' 表示執行個體化調用(先執行個體化產生對象再調用) |
args |
類方法接受的參數 |
總結
異常機制提供了幾個非常關鍵的好處:
(1) 通過將錯誤處理集中於catch語句中,你可以將錯誤處理從應用流程中獨立出來。這也使代碼的可讀性提高,看起來令人愉快。我通常採取非常嚴格的策略來捕捉所有異常並中止指令碼執行。這樣可以獲得所需的附加的彈性,同時實現安全易用的異常管理。
(2) 重擲異常,將異常資料流從低層傳遞至高層,就是說異常被傳回最適合決定如何處理異常的地方。這看起來會顯得有點奇怪,但實際情況中很經常我們在異常發生的時候無法立刻決定如何處理它。
(3) 異常機制提供的Throw/catch避免了直接返回錯誤標識,方法的傳回值是可以由你的類來決定的。其它程式員使用你的代碼時,可以指定返回一個他希望的形式,而不需要令人疲倦的不停地測試。
“PHP 5.0異常處理機制深度探索”的更多相關文章 》
愛J2EE關注Java邁克爾傑克遜視頻站JSON線上工具
http://biancheng.dnbcw.info/php/324026.html pageNo:15