你知道PHP中Exception, Error Handler的這些細節(jié)嗎?

前言

最近項目中有一個功能需要實現(xiàn):
調(diào)試模式下, 將所有錯誤提前輸出, 再輸出頁面內(nèi)容.

為實現(xiàn)上述功能, 需使用到Exception, Error相關(guān)Handler方法, 發(fā)現(xiàn)有許多坑, 故寫此文與大家分享.

主要函數(shù)

此篇文章重點關(guān)注以下幾個函數(shù)

  • error_reporting()
  • set_error_handler()
  • set_exception_handler()
  • register_shutdown_function()
  • error_get_last()

這有什么難的?

哈~ 如果您現(xiàn)在有標(biāo)題中的感慨, 那么也請關(guān)注以下本文中將重點講述的問題列表:

  1. error_reporting()error_get_last() 有什么聯(lián)系?
  2. set_error_handler()set_exception_handler() 綁定的handler什么時候才會啟動? 它們有什么聯(lián)系?
  3. register_shutdown_function()通常跟Exception/Error有關(guān)系么?

上述問題描述模糊, 因此答案也可能千人千面.
因此, 本文只給出自己的答案與大家分享, 如有問題或不同的見解, 期待與您溝通.
如果以上問題, 并不能引起您的興趣, 或者您已理解透徹了, 就可以自行右上角小紅叉啦~

解疑:

1. error_reporting()error_get_last() 有什么聯(lián)系?

link: php.net - error_reporting()
link: php.net - error_get_last()

  • int error_reporting ([ int $level ] )
    大家應(yīng)該再熟悉不過了, 因此不再贅述.

  • array error_get_last ( void )

    獲取最后發(fā)生的錯誤.

    通常用來獲取PHP運行過程中的Fatal Error錯誤(PHP 5).

這兩個函數(shù)在字面上關(guān)聯(lián)性并不強, 但請觀察以下代碼及輸出

<?php
error_reporting(E_ALL & ~E_NOTICE);
$a = $b;  //E_NOTICE
print_r(error_get_last());

/* output:
Array
(
    [type] => 8
    [message] => Undefined variable: b
    [file] => /app/t.php
    [line] => 3
)
*/

error_get_last()雖然說明了獲取最后發(fā)生的錯誤, 實際上也是如此. 但卻沒有說明, 被error_reporting()忽略掉的錯誤是否有可能被獲取到, 因此, 當(dāng)我們使用error_get_last()時需要注意我平時忽略掉的錯誤, 如: E_DEPRECATED

2. set_error_handler()set_exception_handler() 綁定的handler什么時候才會啟動? 它們有什么聯(lián)系?

link: php.net - set_error_handler()
link: php.net - set_exception_handler()

  • mixed set_error_handler ( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] )

    設(shè)置用戶自定義的錯誤處理函數(shù).

    通常在PHP腳本運行過程中, 出現(xiàn)一些非中斷性錯誤時觸發(fā).
    我們會用這個來記錄錯誤日志或直接輸出等操作.
    注意:

    1. 參數(shù)$error_types大多設(shè)定為error_reporting(), 但建議設(shè)定為E_ALL, 具體哪些錯誤需要被處理, 哪些不需要, 在handler內(nèi)進行判斷明顯更加靈活.

    2. 以下級別的錯誤不能由用戶定義的函數(shù)來處理: E_ERROR丁存、 E_PARSE、 E_CORE_ERROR辽聊、 E_CORE_WARNING痴脾、 E_COMPILE_ERROR颤介、 E_COMPILE_WARNING,和在 調(diào)用 set_error_handler() 函數(shù)所在文件中產(chǎn)生的大多數(shù) E_STRICT

    3. handler被觸發(fā)后, 并不會中斷PHP運行.

    4. bool error_handler ( int $errno , string $errstr [, string $errfile [, int $errline [, array $errcontext ]]] )
      注意error_handler的返回值:

      • FALSE: 標(biāo)準(zhǔn)的錯誤處理依然會被執(zhí)行(標(biāo)準(zhǔn)錯誤處理根據(jù) display_errors = true/false 決定是否輸出到stderr)
  • callable set_exception_handler ( callable $exception_handler )

    設(shè)置用戶自定義的異常處理函數(shù)
    設(shè)置默認(rèn)的異常處理程序赞赖,用于沒有用 try/catch 塊來捕獲的異常滚朵。 在 exception_handler 調(diào)用后異常會中止。

    注意:

    1. exception_handler 調(diào)用后異常會中止(腳本終止).
    2. PHP 5, PHP 7exception_handler并不相同.
      PHP 5: void handler ( Exception $ex )
      PHP 7: void handler ( Throwable $ex )
    3. 自 PHP 7 以來前域,大多數(shù)錯誤拋出 Error 異常辕近,也能被捕獲。 Error 和 Exception 都實現(xiàn)了 Throwable 接口匿垄。

    注意點中2, 3項輕描淡寫了一下PHP 5/PHP 7之間的不同卻透露出重要的消息(坑..)
    PHP 7中, exception_handler 不再只接受Exception了, 并且接收了Error錯誤.
    link: php.net - PHP7 Errors列表

因此, set_error_handler()set_exception_handler() 之間的關(guān)系也迎刃而解:

  • PHP 5:

    • set_error_handler(): 負(fù)責(zé)非中斷行錯誤.
    • set_exception_handler(): 負(fù)責(zé)沒有被catch的異常(會中斷).
    • Fatal Error等: 并不會被兩者管理, 正常輸出到屏幕上(弊端).
  • PHP 7:

    • set_error_handler(): 負(fù)責(zé)非中斷行錯誤.
    • set_exception_handler(): 負(fù)責(zé)沒有被catch的異常, Error(會中斷)
    • Fatal Error等: 由set_exception_handler()管理.

3. register_shutdown_function()通常跟Exception/Error有關(guān)系么?

link: php.net - register_shutdown_function()

注冊一個 callback 移宅,它會在腳本執(zhí)行完成或者 exit() 后被調(diào)用。

根據(jù)說明可以得出結(jié)論, 它與Exception/Error完全沒關(guān)系.
提出這個問題, 主要是因為, 在PHP5Fatal Error并沒有明確的接收地點, 所以我們通常配合error_get_last()來接收Fatal Error

<?php 
register_shutdown_function('shutdown_function');
unknown_function();

function shutdown_function() {
  print_r(error_get_last());
}

/* output:
Array
(
    [type] => 1
    [message] => Uncaught Error: Call to undefined function unknown_function() in /app/t.php:3
Stack trace:
#0 {main}
  thrown
    [file] => /app/t.php
    [line] => 3
)
*/

然而隨著PHP 7的到來, Error已經(jīng)可以被set_exception_handler()捕捉了, 再通過error_get_last()就多余了. shutdown中更多的是一些版本冗余的工作.

栗子

前言中的需求: 調(diào)試模式下, 將所有錯誤提前輸出, 再輸出頁面內(nèi)容.
以下是demo, 省去了環(huán)境判斷(debug環(huán)境), 大家可以根據(jù)下面這段代碼, 了解本文中所說的各種handler的觸發(fā)和調(diào)用情況.

<?php

/*
要求: 將所有異常打印在屏幕最上方
*/

/* Fatal Error 中斷腳本 -> shutdown_handler */

//設(shè)置錯誤級別
define("END_ERRORS", '--END ERRORS--' . PHP_EOL . PHP_EOL);
ini_set('display_errors', true);
ini_set('error_reporting', E_ALL & ~E_DEPRECATED);

set_error_handler('usr_err_handler', error_reporting()); //注冊錯誤處理函數(shù)
set_exception_handler('usr_ex_handler'); //注冊異常處理函數(shù)
register_shutdown_function('shutdown_handler');    //注冊會在php中止時執(zhí)行的函數(shù)


$global_errors = [];    //用于記錄所有錯誤
$errnos = [             //錯誤級別
    0 => 'ERROR',//PHP7 ERROR的CODE
    1 => 'E_ERROR',//FATAL ERROR(PHP5), E_ERROR
    2 => 'E_WARNING',
    4 => 'E_PARSE',
    8 => 'E_NOTICE',
    16 => 'E_CORE_ERROR',
    32 => 'E_CORE_WARNING',
    64 => 'E_COMPILE_ERROR',
    128 => 'E_COMPILE_WARNING',
    256 => 'E_USER_ERROR',
    512 => 'E_USER_WARNING',
    1024 => 'E_USER_NOTICE',
    2048 => 'E_STRICT',
    4096 => 'E_RECOVERABLE_ERROR',
    8192 => 'E_DEPRECATED',
    16384 => 'E_USER_DEPRECATED',
    30719 => 'E_ALL',
];

function reset_errors()
{
    global $global_errors;
    $global_errors = [];
}

function get_errnostr($errno)
{
    global $errnos;
    return $errnos[$errno];
}

function set_errnos($errno, $errstr)
{
    global $global_errors;
    $global_errors[] = [
        'errno' => $errno,
        'errnostr' => get_errnostr($errno),
        'errstr' => $errstr,
    ];
}

function print_errors($prefix)
{
    global $global_errors;
    foreach ($global_errors as $err) {//由于handler中依然有可能有error 因此放最后
        printf("[%s]: %s, %d, %s\n",
            $prefix, $err['errnostr'], $err['errno'], $err['errstr']);
    }
}

//用戶異常處理函數(shù) (進來就中斷腳本) PHP5只有Exception進來   PHP7Error和Exception
//PHP7中 void handler (Throwable $ex) 可捕獲Error和Exception兩種異常, 暫不管
//http://php.net/manual/en/language.errors.php7.php PHP7 Error閱讀
//內(nèi)部如果有Error則觸發(fā)Error函數(shù), 再回到錯誤行繼續(xù)執(zhí)行
function usr_ex_handler($ex)
{
    $content = ob_get_clean();  //讓Exception/Error提前展示

    print_errors('EX ERROR');
    reset_errors();

    $errnostr = get_errnostr($ex->getCode());
    $errno = $ex->getCode();
    $errstr = $ex->getMessage();

    if ($ex instanceof Exception) {
        printf("[EXCEPTION]: %s, %d, %s\n", $errnostr, $errno, $errstr);
    } else {//針對PHP7  $ex instanceof Error
        printf("[EX FATAL ERROR]: %s, %d, %s\n", $errnostr, $errno, $errstr);
    }

    //由于handler中依然有可能有error 因此放最后
    print_errors('EX ERROR');
    reset_errors();

    echo END_ERRORS;
    echo $content;

    return;
}

//用戶錯誤處理函數(shù)
//E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING不能被用戶處理
function usr_err_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
    set_errnos($errno, $errstr);
    return true;    //如果函數(shù)返回 FALSE椿疗,標(biāo)準(zhǔn)錯誤處理處理程序?qū)^續(xù)調(diào)用漏峰。
}

//用戶PHP終止函數(shù)
function shutdown_handler()
{
    $content = ob_get_clean();  //讓Exception/Error提前展示
    $err = error_get_last();//檢查一下是否有遺漏掉的錯誤 php5 fatal error
    if ($err['type'] & error_reporting()) {
        set_errnos($err['type'], $err['message']);
    }
    print_errors('ST ERROR');
    reset_errors();

    echo $content;
}

ob_start();

echo 'Main function...', PHP_EOL;

//搞事情
//throw new Exception('這是一個異常');
trigger_error('這是一個用戶error');//E_USER_NOTICE

if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
    mcrypt_encrypt();//E_WARNING, E_DEPRECATED
} else {
    mysql();
}
unknown_function(); //fatal error


$content = ob_get_clean();

//優(yōu)先輸出錯誤
print_errors('MA ERROR');
if (!empty($global_errors)) {
    echo END_ERRORS;
}
reset_errors();

//輸出正文內(nèi)容
echo $content;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市届榄,隨后出現(xiàn)的幾起案子浅乔,更是在濱河造成了極大的恐慌,老刑警劉巖铝条,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件童擎,死亡現(xiàn)場離奇詭異,居然都是意外死亡攻晒,警方通過查閱死者的電腦和手機顾复,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鲁捏,“玉大人芯砸,你說我怎么就攤上這事「罚” “怎么了假丧?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長动羽。 經(jīng)常有香客問我包帚,道長,這世上最難降的妖魔是什么运吓? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任渴邦,我火速辦了婚禮疯趟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谋梭。我一直安慰自己信峻,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布瓮床。 她就那樣靜靜地躺著盹舞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪隘庄。 梳的紋絲不亂的頭發(fā)上踢步,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音丑掺,去河邊找鬼贾虽。 笑死,一個胖子當(dāng)著我的面吹牛吼鱼,可吹牛的內(nèi)容都是我干的蓬豁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼菇肃,長吁一口氣:“原來是場噩夢啊……” “哼地粪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起琐谤,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蟆技,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后斗忌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體质礼,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年织阳,在試婚紗的時候發(fā)現(xiàn)自己被綠了眶蕉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡唧躲,死狀恐怖造挽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弄痹,我是刑警寧澤饭入,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站肛真,受9級特大地震影響谐丢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一乾忱、第九天 我趴在偏房一處隱蔽的房頂上張望讥珍。 院中可真熱鬧,春花似錦饭耳、人聲如沸串述。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衰腌,卻和暖如春新蟆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背右蕊。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工琼稻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饶囚。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓帕翻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親萝风。 傳聞我的和親對象是個殘疾皇子嘀掸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容