thinkphp原始碼分析(四)—錯誤及異常處理篇

weixin_34127717發表於2019-02-21

原始碼分析

錯誤及異常處理機制

錯誤及異常處理機制檔案是/thinkphp/library/think/Error.php,在框架引導檔案的的基礎檔案base.php中註冊(不知道的可以去看《《原始碼分析(二)—入口篇》》),通過thinkError::register()進行的註冊。


    /**
     * 註冊異常處理
     * @access public
     * @return void
     */
    public static function register()
    {
        error_reporting(E_ALL);
        set_error_handler([__CLASS__, 'appError']);
        set_exception_handler([__CLASS__, 'appException']);
        register_shutdown_function([__CLASS__, 'appShutdown']);
    }

該方法做了四件事情:

  1. 設定報錯級別 E_ALL為E_STRICT所有報錯。
  2. 設定錯誤處理函式,set_error_handler([__CLASS__, 'appError'])
  3. 設定異常處理函式,set_exception_handler([__CLASS__, 'appException']);
  4. 設定程式異常終止處理函式,register_shutdown_function([__CLASS__, 'appShutdown']);

PHP報錯級別

php的報錯級別有:E_STRICT,E_ALL, E_USER_WARNING等,具體可檢視[php
預定義常量]。

錯誤處理函式

thinkphp中註冊了thinkError::appError()方法對錯誤進行處理。


    /**
     * 錯誤處理
     * @access public
     * @param  integer $errno      錯誤編號
     * @param  integer $errstr     詳細錯誤資訊
     * @param  string  $errfile    出錯的檔案
     * @param  integer $errline    出錯行號
     * @return void
     * @throws ErrorException
     */
    public static function appError($errno, $errstr, $errfile = '', $errline = 0)
    {
        $exception = new ErrorException($errno, $errstr, $errfile, $errline);

        // 符合異常處理的則將錯誤資訊託管至 think\exception\ErrorException
        if (error_reporting() & $errno) {
            throw $exception;
        }

        self::getExceptionHandler()->report($exception);
    }

在appError方法中,把符合異常處理的則將錯誤資訊託管至系統的ErrorException,其他的異常通過thinkexceptionHandle進行處理。


//think\exception\ErrorException檔案

/**
 * ThinkPHP錯誤異常
 * 主要用於封裝 set_error_handler 和 register_shutdown_function 得到的錯誤
 * 除開從 think\Exception 繼承的功能
 * 其他和PHP系統\ErrorException功能基本一樣
 */
class ErrorException extends Exception
{
    /**
     * 用於儲存錯誤級別
     * @var integer
     */
    protected $severity;

    /**
     * 錯誤異常建構函式
     * @param integer $severity 錯誤級別
     * @param string  $message  錯誤詳細資訊
     * @param string  $file     出錯檔案路徑
     * @param integer $line     出錯行號
     * @param array   $context  錯誤上下文,會包含錯誤觸發處作用域內所有變數的陣列
     */
    public function __construct($severity, $message, $file, $line, array $context = [])
    {
        $this->severity = $severity;
        $this->message  = $message;
        $this->file     = $file;
        $this->line     = $line;
        $this->code     = 0;

        empty($context) || $this->setData('Error Context', $context);
    }

    /**
     * 獲取錯誤級別
     * @return integer 錯誤級別
     */
    final public function getSeverity()
    {
        return $this->severity;
    }
}

errorException設定錯誤級別,錯誤資訊,出錯檔案路徑,行號,上下文。

對exception進行處理的是thinkexceptionHandle的report()方法:self::getExceptionHandler()->report($exception);


    //self::getExceptionHandler()

    /**
     * 獲取異常處理的例項
     * @access public
     * @return Handle
     */
    public static function getExceptionHandler()
    {
        static $handle;

        if (!$handle) {
            // 異常處理 handle
            $class = Config::get('exception_handle');

            if ($class && is_string($class) && class_exists($class) &&
                is_subclass_of($class, "\\think\\exception\\Handle")
            ) {
                $handle = new $class;
            } else {
                $handle = new Handle;

                if ($class instanceof \Closure) {
                    $handle->setRender($class);
                }

            }
        }

        return $handle;
    }

這裡有一個關鍵的地方是:static $handle; 宣告該變數是靜態變數時候,當賦值給該變數後,函式呼叫結束後不會銷燬,直到指令碼結束才會銷燬。

這個邏輯就是判斷$handle是否已經賦值,沒有賦值,獲取預設配置檔案是否設定處理handle,如果設定,這個handle必須是\think\exception\Handle的子類(is_subclass_of($class, "\think\exception\Handle")),如果沒有設定,那麼用預設的thinkexceptionHandle呼叫report方法進行處理, 記錄到日誌檔案中。


    /**
     * Report or log an exception.
     *
     * @param  \Exception $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        if (!$this->isIgnoreReport($exception)) {
            // 收集異常資料
            if (App::$debug) {
                $data = [
                    'file'    => $exception->getFile(),
                    'line'    => $exception->getLine(),
                    'message' => $this->getMessage($exception),
                    'code'    => $this->getCode($exception),
                ];
                $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";
            } else {
                $data = [
                    'code'    => $this->getCode($exception),
                    'message' => $this->getMessage($exception),
                ];
                $log = "[{$data['code']}]{$data['message']}";
            }

            if (Config::get('record_trace')) {
                $log .= "\r\n" . $exception->getTraceAsString();
            }

            Log::record($log, 'error');
        }
    }

把errorException的資料組裝成對應的字串,寫入日誌。

異常處理函式

thinkphp中註冊了thinkError::appException()方法對錯誤進行處理。


    /**
     * 異常處理
     * @access public
     * @param  \Exception|\Throwable $e 異常
     * @return void
     */
    public static function appException($e)
    {
        if (!$e instanceof \Exception) {
            $e = new ThrowableError($e);
        }

        $handler = self::getExceptionHandler();
        $handler->report($e);

        if (IS_CLI) {
            $handler->renderForConsole(new ConsoleOutput, $e);
        } else {
            $handler->render($e)->send();
        }
    }

方法和appError處理差不多,基本都是通過獲取ExceptionHandle再呼叫handle的report方法,但是多了一步把異常呈現,如果是命令列寫到命令列輸出,如果是web的就把錯誤資訊通過reponse響應返回客戶端。

異常中止時執行的函式

thinkphp中註冊了thinkError::appShutdown()方法對錯誤進行處理。


    /**
     * 異常中止處理
     * @access public
     * @return void
     */
    public static function appShutdown()
    {
        // 將錯誤資訊託管至 think\ErrorException
        if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) {
            self::appException(new ErrorException(
                $error['type'], $error['message'], $error['file'], $error['line']
            ));
        }

        // 寫入日誌
        Log::save();
    }

通過error_get_last()獲取最後丟擲的錯誤,把資訊託管至thinkErrorException,在通過異常處理函式進行記錄資訊。最後寫入日誌。

總結

整體整個錯誤處理機制都是通過獲取ExceptionHandle再呼叫handle的report方法,但是多了一步把異常呈現,如果是命令列寫到命令列輸出,如果是web的就把錯誤資訊通過reponse響應返回客戶端。預設的處理handle是thinkexceptionHandle,當然也可以自定義handle,但是必須是thinkexceptionHandle的子類, 通過self::getExceptionHandler的is_subclass_of($class, "\think\exception\Handle")可以知。

相關文章