PHP 超實用系列 · 自動捕獲 Fatal Error

子路發表於2017-05-06

重要使命

經過十幾天的忙碌,張小五手上的專案終於如期上線,雖然很累,但內心無比的充實與喜悅。喝了杯熱咖啡,小五在椅子上慵懶地躺著,享受著這份靜謐的時光。

"嗨,小五,這幾天累壞了吧?"
"哈哈,是有點累,不過還好。"

"週末好好休息下吧,我先跟你討論個事兒啊。"
"好的,Z哥。"

"我們們線上執行的程式碼,出於各種各樣的情況,可能會有好多Fatal Error、Exception。有沒有辦法,在出現Fatal Error、Exception的時候,我們們能自動捕獲,並寫到Log檔案裡?"
"嗯...這個嘛,出現Fatal Error的時候,指令碼就終止了,不好捕獲啊。"

"對,是不好捕獲。但是對於出現的Fatal Error、Exception我們不知道的話,不能提前發現問題,就像身邊有個隱形的刺客一樣,讓人內心特別虛啊..."
"這樣啊,Z哥,那我這幾天試一下吧!"

"好的,小五,這個挺重要的,相信你!"
"哈哈,Z哥你還是不要抱太大希望,我努力試一下就是了。"

從Google到SO

對於碼農來說,從Google到Stackoverflow是解決問題的通途,當然張小五也不例外。

哈!不搜不知道,一搜嚇一跳,PHP還真有捕獲Error和Exception的函式。

//設定一個使用者的函式來處理指令碼中出現的錯誤。
set_error_handler($callback)
//設定一個使用者的函式來處理指令碼中出現的異常。
set_exception_handler($callback)複製程式碼

張小五不自覺的笑了笑:“哈哈,不愧是世界上最好的語言!”

說幹就幹,看看這兩個函式的威力怎樣,不一會,小五就寫出了測試程式碼。

<?php
//設定異常捕獲函式
set_exception_handler("my_exception");
function my_exception($exception){
    echo 'Exception Catched:'.$exception->getMessage();
}
//丟擲異常
throw new Exception("I am Exception");複製程式碼

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

Yes,丟擲的一個Exception真的被捕獲了!

"接下來再測下set_error_handler(),你可不能讓我失望啊!"小五心想。

<?php
set_error_handler("error_handler");
function error_handler($errno,$errstr,$errfile,$errline){
    $str=<<<EOF
         "errno":$errno
         "errstr":$errstr
         "errfile":$errfile
         "errline":$errline
EOF;
//獲取到錯誤可以自己處理,比如記Log、報警等等
    echo $str;
}
echo $test;//$test未定義,會報一個notice級別的錯誤複製程式碼

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

不錯,Notice級別的錯誤也捕獲到了!
接下來再測一下Fatal Error,如果Fatal Error也能捕獲到,這個需求就實現了!

抑制住激動的心情,小五很快寫完了測試程式碼。

<?php
set_error_handler("error_handler");
function error_handler($errno,$errstr,$errfile,$errline){
    $str=<<<EOF
         "errno":$errno
         "errstr":$errstr
         "errfile":$errfile
         "errline":$errline
EOF;
//獲取到錯誤可以自己處理,比如記Log、報警等等
    echo $str;
}
//呼叫一個不存在的函式,會出現Fatal Error
test();複製程式碼

小五屏住呼吸,等待著奇蹟的出現。"咣噹",手起指落,幾行報錯躍然屏上...

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

神馬?Fatal Error竟然沒捕獲到?怎麼可能?

正在小五陷入沉思的時候,不經意間,小五瞥見了函式的說明:

以下級別的錯誤不能由使用者定義的函式來處理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 呼叫 set_error_handler() 函式所在檔案中產生的大多數 E_STRICT。

也就是:set_error_handler($callback)只能捕獲系統產生的一些Warning、Notice級別的Error。

嗚呼悲催,好不容易找到了解決辦法,沒想到這函式竟然還是個半吊子,很多級別的錯誤捕獲不到...?

眾裡尋他千百度

王小五從不是輕言放棄的人,他又繼續搜尋,尋找著解決辦法...

"嗯?哈哈,SO上還真有人遇到這問題!"

小五專注地看著答案,邊看邊敲了起來:

要實現這個需求,需要用到兩個函式:register_shutdown_function()error_get_last()

register_shutdown_function()

register_shutdown_function($callback)複製程式碼

register_shutdown_function(),就把你要註冊進去的function放進【假裝是佇列吧】,等到指令碼正常退出或顯式呼叫exit()時,再把註冊進去的function拉出來執行.

register_shutdown_function()呼叫的3種情況:

  • 指令碼正常退出時;
  • 在指令碼執行(run-time not parse-time)出錯退出時;
  • 使用者呼叫exit方法退出時。

error_get_last()

error_get_last();//函式獲取最後發生的錯誤。複製程式碼

該函式以陣列的形式返回最後發生的錯誤。

返回的陣列包含 4 個鍵和值:

[type] - 錯誤型別
[message] - 錯誤訊息
[file] - 發生錯誤所在的檔案
[line] - 發生錯誤所在的行

強烈注意

在parse-time出錯的時候,是不會呼叫register_shutdown_function()函式的。只有在run-time出錯的時候,才會呼叫register_shutdown_function()。

為了更好的理解,下面我們舉例說明:

NO.1

error_handler.php
<?php
register_shutdown_function("error_handler");
function error_handler(){
    echo "Yeah,it's worked!";
}
function test(){}
function test(){}複製程式碼

執行結果如下:

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

原因分析

在執行error_handler.php的時候,由於重複定義了兩個函式test(),在php的parse-time就出錯了(不是run-time),所以不能回撥register_shutdown_function()中註冊的函式。

NO.2

error_handler.php
<?php
register_shutdown_function("error_handler");
function error_handler(){
    echo "Yeah,it's worked!";
}
if(true){
   function test(){}
}
function test(){}複製程式碼

執行結果如下:

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

原因分析

我們看到,上面回撥了register_shutdown_function()中註冊的函式。
因為我們加了一個if()判斷,if()裡面的test()方法,相當於一個閉包,與外面的test()名稱不衝突。
也就是,上面的程式碼在parse-time沒有出錯,而是在run-time的時候出錯了,所以我們能夠獲取到fatal error。

NO.3

error_handler.php
<?php
register_shutdown_function("error_handler");
function error_handler(){
    echo "Yeah,it's worked!";
}複製程式碼
test.php
<?php
include './error_handler.php';
function test(){}
function test(){}複製程式碼

執行 test.php的結果如下

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

原因分析

當我們在執行test.php的時候,因為redeclare了兩個test()方法,所以php的語法解析器在parse-time的時候就出錯了。 所以不能回撥register_shutdown_function()中的方法,不能catch住這個fatal error。

NO.4

error_handler.php
<?php
register_shutdown_function("error_handler");
function error_handler(){
    echo "Yeah,it's worked!";
}複製程式碼
test.php
<?php
function test(){}
function test(){}複製程式碼
include_all.php
<?php
require './error_handler.php';
require './test.php';複製程式碼

執行 include_all.php的結果如下

PHP 超實用系列 · 自動捕獲 Fatal Error
執行結果

結果分析

上面我們捕獲了fatal_error。
因為在執行include_all.php的時候,include_all.php本身語法並沒有出錯,也就是在parse-time的時候並沒有出錯,而是include的檔案出錯了,也就是在run-time的時候出錯了,這個時候是能回撥register_shutdown_function()中的函式的。

強烈建議:如果我們要使用register_shutdown_function進行錯誤捕捉,使用NO.4,最後一種方法,可以確保錯誤都能捕捉到。

驀然回首解需求

"哇塞,原來可以這樣啊!"
王小五按答案中舉的例子認真的敲完程式碼,瞬間明白瞭解決的辦法。

真可謂"眾裡尋他千百度,驀然回首,那人卻在燈火闌珊處。"小二不自覺的感嘆道!

"好了,我自己就寫一個error_handler指令碼吧,確保每次都能獲取到想要的Fatal Error。"

<?php
register_shutdown_function( "fatal_handler" );
set_error_handler("error_handler");

define('E_FATAL',  E_ERROR | E_USER_ERROR |  E_CORE_ERROR | 
        E_COMPILE_ERROR | E_RECOVERABLE_ERROR| E_PARSE );

//獲取fatal error
function fatal_handler() {
    $error = error_get_last();
    if($error && ($error["type"]===($error["type"] & E_FATAL))) {
        $errno   = $error["type"];
        $errfile = $error["file"];
        $errline = $error["line"];
        $errstr  = $error["message"];
        error_handler($errno,$errstr,$errfile,$errline);
  }
}
//獲取所有的error
function error_handler($errno,$errstr,$errfile,$errline){
    $str=<<<EOF
         "errno":$errno
         "errstr":$errstr
         "errfile":$errfile
         "errline":$errline
EOF;
//獲取到錯誤可以自己處理,比如記Log、報警等等
    echo $str;
}複製程式碼

有了這個指令碼,我再按SO上說的第四種方法去執行,那這個需求就實現了!

不負眾望

王小五興沖沖的找到Z哥,詳細的說明了自己的研究成果。

第二天,小五按照公司現有的框架規則,結合上面的解決辦法,不一會就實現了需求。

"不錯啊,小五,我就說你可以吧!" Z哥高興的說到。
"哈哈,Z哥,這下所有的錯誤都在掌握之中了!"


轉載宣告:本文轉載自「聊聊程式碼」,搜尋「talkpoem」即可關注。

關注「聊聊程式碼」,讓我們一起聊聊“左手程式碼右手詩”的事兒。

PHP 超實用系列 · 自動捕獲 Fatal Error

相關文章