PHP 優雅的捕獲處理錯誤 -- E_PARSE / E_ERROR

開發中使用的框架,大均可以作到優雅的回顯出語法級的錯誤,即 Parse Error(syntax error)E_PARSE,此錯誤做爲面向用戶代碼最底層的錯誤如何進行捕獲?php

下面主要講一下如何捕獲 E_PARSE & E_ERROR 錯誤,這裏我刻意的把 E_PARSE 錯誤放前位的,由於 E_PARSE 是面向用戶腳本第一位的錯誤,即如有必然最早發生。然後纔是 E_ERROR & E_WARNING & E_NOTICE ....一類的運行時錯誤。laravel

PHP 錯誤級別瀏覽器

# 系統級用戶代碼的一些錯誤類型 可由 try ... catch ... 捕獲
E_PARSE          解析時錯誤 語法解析錯誤 少個分號 多個逗號一類的 致命錯誤
E_ERROR          運行時錯誤 好比調用了未定義的函數或方法 致命錯誤

# 可由 set_error_handler 捕獲處理
E_WARNING        運行時警告 調用了未定義的變量
E_NOTICE         運行時提醒                  
E_DEPRECATED     運行時已廢棄的函數或方法

# Zend Engine 相關的一些錯誤 內存錯誤一類的 應該也能經過 try ... catch ... 捕獲 略難測試
E_CORE_ERROR
E_CORE_WARNING
E_COMPILE_ERROR
E_COMPILE_WARNING

# 用戶級自定義錯誤 可由 trigger_error 觸發 可由 set_error_handler 捕獲處理
E_USER_ERROR 用戶自定義錯誤 致命錯誤 未處理也會致使程序退出
E_USER_WARNING
E_USER_NOTICE
E_USER_DEPRECATED

#編碼標準化警告(建議如何修改以向前兼容)
E_STRICT 部分 捕獲的話 try ... catch ... 部分 set_error_handler
E_RECOVERABLE_ERROR

先看一些問題代碼服務器

天真的想法

一、想關閉全部的錯誤報告框架

<?php
// 不報告任何級別的錯誤
error_reporting(0);
// 關閉錯誤回顯
ini_set('display_errors', false);

echo 'i lost semicolon operator'

PHP 依然使用自身的錯誤機制報錯,緣由很簡單:語法解析 -- 解釋運行 -- 結束退出。當腳本最基本的語法存在問題時,Zend Engine 自身就會退出執行,並回顯 Parse ERROR 錯誤信息。此時還未解釋執行用戶代碼,即 error_reporting(0) 尚未在 Zend Engine 中對運行時作運行時環境的設定。函數

二、想使用 set_error_handler 捕捉錯誤oop

<?php
// 報告全部級別的錯誤
error_reporting(E_ALL);

// 自定義錯誤捕捉器
set_error_handler(function ($error_no, $error_str, $error_file, $error_line) {
}, E_ALL | E_STRICT);

echo 'i lost semicolon operator'

依然得不到理想的結果。測試

首先,這段代碼也是在解析階段就報錯了,Parse Error 直接退出了,尚未真的執行 set_error_handler()。ui

官方原話講解:this

若是錯誤發生在腳本執行以前(好比文件上傳時),將不會調用自定義的錯誤處理程序由於它還沒有在那時註冊。

再說,退一步講, set_error_handler 是用來自定義用戶級錯誤 E_USER_ERROR & E_USER_WARNING & E_USER_NOTICE & E_USER_DEPRECATED 和 部分運行時系統錯誤 E_WARING & E_NOTICE & E_DEPRECATED 的捕獲器,即語法解析錯誤 E_PARSE (Parse 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 的 handler 最後返回了 false,則此錯誤信息會繼續被 PHP 的標準錯誤處理程序處理:經過 error_reporting 的級別設定,該回顯的回顯(display_errors),該寫入錯誤日誌的寫入錯誤日誌(log_errors & error_log)

官方原話講解:

重要的是要記住 error_types 裏指定的錯誤類型都會繞過 PHP 標準錯誤處理程序, 除非回調函數返回了 FALSE。

注意,set_error_handler 是有本身的捕獲級別的,默認 E_ALL | E_STRICT,不過要出去上文說的那幾個級別,且不受 error_reporting() 設定的級別影響,即便你 error_reporting(0),set_error_handler 依然能捕捉到相應的錯誤。

若干問題

一、爲什麼不少框架均可以優雅的捕獲到語法或致命錯誤(E_PARSE & E_ERROR)呢?好比 laravel 標配的 whoops 
二、E_PARSE & E_ERROR 到底如何才能捕捉到?
三、以上示例貌似都在說 E_PARSE & E_ERROR 這種錯誤沒法捕獲,那讓用戶來自定義告警的
error_reporting() 的級別裏爲什麼還有它倆?

既然有相應的級別設置,那就說明是能夠被捕捉的,先簡單說明一下,E_ERROR 的捕捉其實很簡單,E_PARSE 的捕捉則需解釋和理解一下,會涉及到 PHP 解析和運行腳本的機制流程。

剖析 PHP 基本的運做機制

其實很是簡單,看一遍就理解了(下文中一些運行機制用詞可能不許確,還請大佬放過,一切爲了讓你們能容易理解)。

一、php 在解釋運行用戶代碼時,會以主腳本爲載入點,Zend Engine 首先對其進行語法解析(Parse),這裏必定要理解,Zend Engine 此時是對腳本的語法進行解析,腳本中的任何 ini 設置都對其無效(還沒解釋載入執行初始化),因此你設置的什麼 error_reporting, display_errors, set_error_handler。只有當語法解析無誤,Zend Engine 開始載入並解釋腳本,腳本里的一些參數設置項纔會開始生效。

二、php 沒有 //連接依賴庫 -- 編譯 -- 運行// 一說。當 php 在主腳本中 「引入依賴」 時,Zend Engine 並不會在對主腳本作語法解析時將其 「依賴」 也載入解析。Zend Engine 只會對當前的主腳本作語法解析,在解析經過後,便開始解釋執行用戶代碼,即使 「依賴」 中有 Parse Error,那也得等到真的執行到載入命令時纔會加載解析-解釋-運行。

因此,咱們首先要構建一個 Parse OK 的容器,初始化 Zend Engine 的一些運行時配置,好比關閉錯誤報告,這樣整個運行時就是關閉了錯誤報告的上下文,即使後續有 E_PAESE & E_ERROR 也不會回顯錯誤信息了。但咱們的目的是要捕捉。

使用 try ... catch 捕獲 E_PARSE & E_ERROR

<?php
error_reporting(E_ALL);

// main script
echo "this is main script" . PHP_EOL;

// 只有當 main script 語法解析 ok,開始載入解釋執行到此處時
// lib.php 纔會開始被 Zend Engine 作語法解析/解釋運行
require_once __DIR__ . '/lib.php';

echo "hello world!" . PHP_EOL;

解析過程:

1:error_reporting(E_ALL); 語法無誤 繼續
2:echo "this is main script" . PHP_EOL; 語法無誤 繼續
3:require_once __DIR__ . '/lib.php'; 此語法無誤 繼續
(注意:此時並不會去載入並對 lib.php 作語法解析檢查)
4:echo "hello world!" . PHP_EOL; 語法無誤繼續

解析完成,語法經過,開始解釋執行

執行過程:

1:error_reporting(E_ALL); 將執行環境的錯誤告警設爲用戶定義的級別,運行時用戶上下文已開始造成
2:echo "this is main script" . PHP_EOL; 輸出個字符串
3:require_once __DIR__ . '/lib.php'; 加載不曾載入過的腳本?開始加載執行 解析 - 解釋 的流程
4:echo "hello world!" . PHP_EOL; 要在 lib.php 被 解析 - 解釋 完成後纔會回到此處繼續執行

是否是發現了?在 lib.php 被載入前,main script 的一些運行時的參數設置已經生效,好比這裏的 error_reporting(E_ALL),lib.php 解析/解釋運行時已是在咱們自定義好錯誤告警級別的上下文中了,Zend Engine 會根據咱們設定的錯誤告警級別對 lib.php 進行載入。這時就能夠明白 E_PARSE & E_ERROR 錯誤可被用戶設定的含義了吧。

即:你首先要有一個絕對正確的容器,負責將一些必要的用戶設定傳遞給 Zend Engine 初始化好運行時上下文,此後再載入執行的用戶代碼都將在此上下文中執行,其後的業務邏輯。

合理的代碼組織結構

示例:
一、關閉全部的錯誤報告

main.js

<?php
// main.js as a ini init container
error_reporting(0);

echo "this is main script" . PHP_EOL;

require_once __DIR__ . "/lib.php";

echo "hello world!" . PHP_EOL;

lib.js

<?php
// lib.js as logic

echo 'i lost semicolon operator'

那麼 lib.php 的任何錯誤都不會被報告出來,由於 main 運行到載入 lib 時,其已向 Zend Engine 發送了 error_reporting(0); 的指令,因此 lib 中的 Parse Error 不會被報告出來。但這並非咱們想要的,咱們要捕獲纔對。

二、捕獲系統級錯誤 E_PARSE & E_ERROR

首先咱們要有一個容器,讓 Zend Engine 載入並初始化運行時候,開始執行。
而後咱們可使用 try ... catch 捕捉錯誤,以下:

<?php
// main.js as a ini init container
error_reporting(E_ALL);

try {
    require_once __DIR__ . '/lib.php';
} catch (\Exception $exception) {
    var_export($exception);
} catch (\Error $error) {
    // 就是這裏了,try catch 捕捉了 Error
    var_export($error);
}

輸出結果:

ParseError::__set_state(array(
   'message' => 'syntax error, unexpected end of file, expecting \',\' or \';\'',
   'string' => '',
   'code' => 0,
   'file' => '...\lib.php',
   'line' => 2,
   'trace' => array (),
   'previous' => NULL,
))

這樣便優雅的拿到了 Parse Error 錯誤,包裝一下輸出給用戶便可。

Parse Error 能夠說是用戶級的最高一級錯誤了,Parse Error 了用戶腳本就退出了。

然後咱們纔會可能遇到 E_ERROR & E_WARNING & E_NOTICE & E_DEPRECATED 等,以下:

<?php
error_reporting(E_ALL);

try {
    // 調用一個不存在的方法
    func_not_exists("hello world");
} catch (\Exception $exception) {
    var_export($exception);
} catch (\Error $error) {
    // 就是這裏了,try catch 捕捉了 Error
    var_export($error);
}

運行結果

Error::__set_state(array(
    'message'  => 'Call to undefined function func_not_exists()',
    'string'   => '',
    'code'     => 0,
    'file'     => '...main.php',
    'line'     => 47,
    'trace'    => array(),
    'previous' => null,
))

如上,語法沒有問題,因此不會有 Parse Error,Zend Engine 開始載入腳本解釋執行,由於調用了不存在的方法,E_ERROR 觸發後被咱們捕獲。

完善的錯誤採集

try ... catch 能夠捕捉 E_PARSE & E_ERROR

set_error_handler 能夠捕捉 E_WARNING & E_NOTICE & E_DEPRECATED & E_USER_*

兩者聯合起來便可捕捉大部分的用戶代碼層面的錯誤

<?php
// 設定錯誤監聽的級別
// 但不會影響 set_error_handler 和 try ... catch 的捕獲
// set_error_handler 和 try ... catch 是將錯誤處理交給用戶
// 只有當用戶沒有對錯誤作處理時
// 錯誤纔會根據 error_reporting / display_errors / log_errors / error_log 進行 php 標準的錯誤處理流程
error_reporting(E_ALL);

// 是否回顯錯誤信息,默認 true
// 則會將全部監聽到的錯誤信息回顯到標準輸出:瀏覽器或者命令行
// 線上環境強烈建議關閉 錯誤信息會暴露服務器相關信息
ini_set('display_errors', false);

// 開啓錯誤日誌
// 線上環境強烈建議開啓 記錄錯誤日誌
ini_set('log_errors', true);
// 錯誤日誌的位置 注意:若是 error_log 的路徑有誤的話 display_errors 會被強制打開 回顯錯誤到標準輸出
ini_set('error_log', __DIR__ . '/error.log');

// 以上爲 php 標準錯誤處理 的設定

// E_WARNING E_NOTICE E_DEPRECATED E_USER_* E_STRICT 捕獲
set_error_handler(function ($error_no, $error_str, $error_file, $error_line) {
    echo "erro_no: " . $error_no . " error_str: " . $error_str . PHP_EOL;
    //注意 程序並不會在這裏退出執行
    //注意 若是返回了 false 錯誤會被 php 標準錯誤處理流程處理
}, E_ALL | E_STRICT);

// E_PARSE & E_ERROR 捕捉
try {
    // E_WARNING 被 set_error_handler 捕獲
    echo $variable_not_exists;

    // E_ERROR 被 try ... catch 捕獲
    func_not_exists("function not exists!");

    // E_PARSE 被 try ... catch 捕獲 lib.php 中有語法錯誤 
    require_once __DIR__ . '/lib.php';
} catch (\Exception $exception) {
    echo var_export($exception, true) . PHP_EOL;
} catch (\Error $error) {
    echo var_export($error, true) . PHP_EOL;
}

echo "run finished" . PHP_EOL;

注意 set_error_handler 和 try ... catch 對錯誤捕獲後程序會繼續執行下去,並不會當即退出。

總結

E_ERROR & E_PARSE 使用 try ... catch 捕獲

E_WARNING & E_NOTICE & E_DEPRECATED & E_USER_* 使用 set_error_handler 捕捉

若沒有作相應的處理,則錯誤信息會提交至 PHP 標準錯誤處理流程,根據 error_reporting / display_errors / log_errors / error_log 的設定進行處理。

相關文章
相關標籤/搜索