- 寫在前面:tp3.2中每次載入入口文件時都會進行錯誤和異常的捕獲,解讀這一部分代碼可以對以后的優化很有好處。
- 處理概覽:

- 錯誤捕獲與處理:
- 致命錯誤捕獲:
我們嘗試在 Home/Index/index 下調用一個未定義的函數,會看到這樣的提示頁面:

我們可以看到tp3.2處理了致命異常的輸出,並且生成了一個提示頁面,我們可以通過入口文件很容易地找到tp3.2的致命錯誤的捕獲方法 Think/Library/Think/Think.class.php:
-
public static function start()
-
{
-
// 注冊AUTOLOAD方法
-
spl_autoload_register( 'Think\Think::autoload');
-
// 設定錯誤和異常處理
-
register_shutdown_function( 'Think\Think::fatalError');
-
set_error_handler( 'Think\Think::appError');
-
set_exception_handler( 'Think\Think::appException');
-
-
.........................
tp使用了 register_shutdown_function()來注冊一個在php中止時執行的函數,通過這個回調函數來捕獲了致命異常:
fatalError:
-
// 致命錯誤捕獲
-
public static function fatalError()
-
{
-
Log::save();
-
-
if ($e = error_get_last()){
-
switch ($e[ 'type']) {
-
case E_ERROR: //通常會顯示出來,也會中斷程序執行
-
case E_PARSE: //語法解析錯誤
-
case E_CORE_ERROR: //在PHP啟動時發生的致命錯誤
-
case E_COMPILE_ERROR: //編譯時發生的致命錯誤,指出腳本的錯誤
-
case E_USER_ERROR: //用戶產生的錯誤信息
-
ob_end_clean();
-
self::halt($e);
-
-
break;
-
}
-
}
-
}
為什么ob_end_clean可以阻止頁面輸出錯誤信息呢?這還得從php的緩沖區說起,
當PHP自身的緩沖區接到指令,指示要輸出緩沖區的內容時,將會把緩沖區內的數據輸出到apache上, apache接受到PHP輸出的數據,然后再把該數據存在到apache自身的緩沖區內,等到輸出
當apache接受到指令,只是要輸出緩沖區的內容時, 將會把緩沖區的內容輸出,返回到瀏覽器。而中止回調是作為請求的一部分被執行的,因此可以在它們中進行輸出或者讀取輸出緩沖區,我們此時用ob_end_clean丟掉緩沖區的內容,就阻止了頁面的輸出顯示。(關於緩沖區:傳送門)
獲取到當前錯誤后,tp講這個錯誤傳遞個halt($e)這個靜態方法,這個方法其實就是tp的錯誤輸出處理了,我們可以從“處理概覽”圖中可以看到,tp根據php不同的運行模式進行錯誤信息的處理與顯示:
halt:
-
/**
-
* 錯誤輸出
-
* @param mixed $error 錯誤
-
* @return void
-
*/
-
public static function halt($error)
-
{
-
$e = array();
-
if (APP_DEBUG || IS_CLI) {
-
//調試模式下輸出錯誤信息
-
if (!is_array($error)) {
-
$trace = debug_backtrace();
-
$e[ 'message'] = $error;
-
$e[ 'file'] = $trace[ 0][ 'file'];
-
$e[ 'line'] = $trace[ 0][ 'line'];
-
ob_start();
-
debug_print_backtrace();
-
$e[ 'trace'] = ob_get_clean();
-
} else {
-
$e = $error;
-
}
-
if (IS_CLI) {
-
exit((IS_WIN ? iconv( 'UTF-8', 'gbk', $e[ 'message']) : $e[ 'message']) . PHP_EOL . 'FILE: ' . $e[ 'file'] . '(' . $e[ 'line'] . ')' . PHP_EOL . $e[ 'trace']);
-
}
-
} else {
-
//否則定向到錯誤頁面
-
$error_page = C( 'ERROR_PAGE');
-
if (! empty($error_page)) {
-
redirect($error_page);
-
} else {
-
$message = is_array($error) ? $error[ 'message'] : $error;
-
$e[ 'message'] = C( 'SHOW_ERROR_MSG') ? $message : C( 'ERROR_MESSAGE');
-
}
-
}
-
// 包含異常頁面模板
-
$exceptionFile = C( 'TMPL_EXCEPTION_FILE', null, THINK_PATH . 'Tpl/think_exception.tpl');
-
include $exceptionFile;
-
exit;
-
}
define('IS_CLI', PHP_SAPI == 'cli' ? 1 : 0); //=='cli' 是在說明php在命令行中運行。
(運行環境監測:
傳送門)
halt這個靜態方法內,根據php不同的運行環境處理傳遞過來的錯誤,命令行環境就這就退出打印,其他模式就將錯誤信息返回給模塊頁面顯示。
調試模式中我們可以修改think_exception.tpl來調整我們的頁面提示,非調試模式你也可以調整think_exception.tpl模板,tp也給了一個錯誤頁面的配置,這些配置在慣例配置文件里,我們可以自定義錯誤信息,也可以指定錯誤后顯示的頁面。配置如下:
convention.php:
-
/* 錯誤設置 */
-
'ERROR_MESSAGE' => '頁面錯誤!請稍后再試~', //錯誤顯示信息,非調試模式有效
-
'ERROR_PAGE' => '', // 錯誤定向頁面
-
'SHOW_ERROR_MSG' => false, // 顯示錯誤信息
register_shutdown_down是處理“down”的,set_error_handler是處理“error”的,php的崩潰類型多種多樣,就拿錯誤類型的“E_USER_ERROR”來講,文前調用的一個未定義函數testErr()就是觸發的“down”里面的 E_USER_ERROR,而我們通過 trigger_error(‘’,E_USER_ERROR)就是觸發的“error”里面的E_USER_ERROR,所有說自定義一個錯誤處理是很有必要的,況且還有“NOTICE”這種類型的錯誤不會中止php執行就不能用“down”處理了呢?
我們首先通過trigger_error()手動生成一個錯誤來看看tp是如何處理的,我們嘗試在 Home/Index/index 里寫下這樣一句代碼:
trigger_error ( "用戶自定義錯誤信息提示" , E_USER_ERROR );
運行結果如下:

從運行結果來看,與之前的致命錯誤"down"相比,這個錯誤提示頁面多了TRACE來顯示代碼執行流程,並且錯誤位置也放在了錯誤信息里面(這個不重要,這個可以隨便你拼接的),那么我們來看看 tp的自定義 錯誤處理:
appErr:
-
/**
-
* 自定義錯誤處理
-
* @access public
-
* @param int $errno 錯誤類型
-
* @param string $errstr 錯誤信息
-
* @param string $errfile 錯誤文件
-
* @param int $errline 錯誤行數
-
* @return void
-
*/
-
public static function appError($errno, $errstr, $errfile, $errline)
-
{
-
switch ($errno) {
-
case E_ERROR:
-
case E_PARSE:
-
case E_CORE_ERROR:
-
case E_COMPILE_ERROR:
-
case E_USER_ERROR:
-
ob_end_clean();
-
$errorStr = "$errstr " . $errfile . " 第 $errline 行.";
-
if (C( 'LOG_RECORD')) {
-
Log::write( "[$errno] " . $errorStr, Log::ERR);
-
}
-
-
self::halt($errorStr);
-
break;
-
default:
-
$errorStr = "[$errno] $errstr " . $errfile . " 第 $errline 行.";
-
self::trace($errorStr, '', 'NOTIC');
-
break;
-
}
-
}
我們先來看看第一個,傳一個字符串給用於顯示錯誤的halt()方法,我們從上面的halt代碼塊中可以看到這樣一段:
-
if (!is_array($error)) {
-
$trace = debug_backtrace();
-
$e[ 'message'] = $error;
-
$e[ 'file'] = $trace[ 0][ 'file'];
-
$e[ 'line'] = $trace[ 0][ 'line'];
-
ob_start();
-
debug_print_backtrace();
-
$e[ 'trace'] = ob_get_clean();
-
} else {
-
$e = $error;
-
}
think_exception.tpl:
-
if( isset($e[ 'trace'])) {
-
<div class="info">
-
<div class="title">
-
<h3>TRACE</h3>
-
</div>
-
<div class="text">
-
<p><?php echo nl2br($e['trace']);?></p>
-
</div>
-
</div>
-
<?php }?>
那tp是如何去追溯這個代碼執行的呢?其實是通過debug_backtrace()這個函數,debug_backtrace()產生一條回溯追蹤,說簡單點,就是我的這個錯誤是如何運行到這里來的(由於是回溯,一般返回的第一條就是產生錯誤的地方)。然后通過debug_print_backtrace()打印信息,在通過ob_get_clean得到緩沖區內容並關閉緩沖區阻止瀏覽器的輸出,最后就在模板里判斷是否存在$e['trace']來做輸出顯示。(不得不說,debug_backtrace是個調試神器)
如果是NOTICE級別的錯誤,就傳到了trace()方法,做日志記錄。
trace:
-
/**
-
* 添加和獲取頁面Trace記錄
-
* @param string $value 變量
-
* @param string $label 標簽
-
* @param string $level 日志級別(或者頁面Trace的選項卡)
-
* @param boolean $record 是否記錄日志
-
* @return void|array
-
*/
-
public static function trace($value = '[think]', $label = '', $level = 'DEBUG', $record = false)
-
{
-
static $_trace = array();
-
if ( '[think]' === $value) {
-
// 獲取trace信息
-
return $_trace;
-
} else {
-
$info = ($label ? $label . ':' : '') . print_r($value, true);
-
$level = strtoupper($level);
-
-
if ((defined( 'IS_AJAX') && IS_AJAX) || !C( 'SHOW_PAGE_TRACE') || $record) {
-
Log::record($info, $level, $record);
-
} else {
-
if (! isset($_trace[$level]) || count($_trace[$level]) > C( 'TRACE_MAX_RECORD')) {
-
$_trace[$level] = array();
-
}
-
$_trace[$level][] = $info;
-
}
-
}
-
}
- 異常處理
tp自定義了異常的處理,使用set_exception_handler()函數,設置了一個appException方法處理異常,我們嘗試拋出一個異常,看tp的運行結果:
throw new \Exception('拋出一個異常')

可以看到運行結果和“error”級別的處理很類似,我們可以看看使用set_exception_handler()設置的appException()方法:
-
public static function appException($e)
-
{
-
$error = array();
-
$error[ 'message'] = $e->getMessage();
-
$trace = $e->getTrace();
-
if ( 'E' == $trace[ 0][ 'function']) {
-
$error[ 'file'] = $trace[ 0][ 'file'];
-
$error[ 'line'] = $trace[ 0][ 'line'];
-
} else {
-
$error[ 'file'] = $e->getFile();
-
$error[ 'line'] = $e->getLine();
-
}
-
$error[ 'trace'] = $e->getTraceAsString();
-
Log::record($error[ 'message'], Log::ERR);
-
// 發送404信息
-
header( 'HTTP/1.1 404 Not Found');
-
header( 'Status:404 Not Found');
-
self::halt($error);
-
}
首先我們要知道,$e就是當前的異常對象,$e可以調用該異常對象是方法,其中$e->getTrace()是追蹤包含異常信息的數組,追蹤信息中包含觸發異常的函數,tp判斷觸發該異常的函數是不是tp自帶的 E()函數,從而組裝異常信息發送給halt顯示,我們可以看到傳遞給halt是通過$e->getTraceAsString()獲取的字符串,所以halt后面又會用debug_backtrace()追溯異常,最后在頁面上生成TRACE信息。