如何組織PHP中的異常

Exception

本文的主題是怎么組織php的異常?在大型項目中異常往往被我們忽略,但是如果前期沒有很好的規(guī)劃好,越到項目后期,重構(gòu)的成本會越大。

在實際工作中,對于錯誤的處理,我們一幫都是直接返回錯誤號,然后從最內(nèi)層一層一層往外面?zhèn)?,最后將錯誤返回給用戶,很少使用異常,可能是因為公司里最初寫代碼比較早,13,14年開始使用php,當時第一批使用者是從C轉(zhuǎn)過來的,從而沒有使用異常,導致現(xiàn)在都16年了,php都出7了,我們在實際代碼中還是沒有使用異常,我前不久在項目中引入了異常,但也是簡單的使用try catch,沒有很多的經(jīng)驗,網(wǎng)上搜索也只是簡單的一些使用例子,沒有說在大型項目中怎么去使用,最近也是在讀The Clean Architecture in php,深知代碼組織的重要性,如果前期沒有很好的組織好,后期的維護,重構(gòu)代價都會很大,今天看到兩篇文章:

Structuring PHP Exceptions

A Crash Course of Changes to Exception Handling in PHP 7

所以就有了本文。寫這篇文章的目的是探討一些在實際中怎么使用異常的方式,也希望得到大家的反饋,大家平時在開發(fā)中是怎么使用異常的?如何組織的。

為什么還使用異常?

在討論使用異常之前,我們得統(tǒng)一認識:使用異常對項目是有益的。我們看看沒有異常的時候,我們的處理方式。

返回錯誤號

function foo($arrInput) {
    if ($arrInput['user_id']<0){
        return -1; // 參數(shù)錯誤
    }
    // something else
}

當程序遇到錯誤時返回一個錯誤碼,使用這種方式的好處是:我們每次在調(diào)用完函數(shù)后,都會檢查返回值,當出現(xiàn)錯誤的時候,馬上進行處理。

但是壞處也很明顯:錯誤的處理和正常的業(yè)務邏輯耦合在了一起,我們平時開發(fā)中一個很惱人的感觸就是:寫一個業(yè)務邏輯,可能異常錯誤處理就占了2/3的代碼,愁人啊,于是有人就發(fā)明了異常。

在php中對錯誤的處理有兩種,一種是error和warnings,另一種是異常。

errors & warnings

php中的errors和warnings來源于過程式的代碼,在過程式代碼中,我們按照既定的步驟一步一步執(zhí)行,此時如果出現(xiàn)了錯誤,我們必須要將程序的控制權(quán)接管過來,在PHP中是通過 set_error_handler 方法來設(shè)置處理函數(shù)的,但是這種方式?jīng)]能提供一種有效的錯誤恢復手段,你可能除了打印下錯誤信息后,沒有足夠的錯誤發(fā)生時的上下文信息讓你來恢復錯誤了。

exceptions

一般我們使用異常的代碼如下:

try {
  find_slash(string);
} catch(AnException& e) {
    //Handle exception
}

這樣做的好處是:程序邏輯和錯誤處理分離了。你可以看到函數(shù)是如何工作的,同時也可以看到失敗時候是怎么處理的。另外,現(xiàn)在可以提供更多的異常發(fā)生的上下信息,幫助你從發(fā)生的異常中恢復出來。

舉個例子:當從數(shù)據(jù)庫中獲取一條記錄的時候發(fā)生了異常,我們可以根據(jù)異常的不同類型,采取不同的結(jié)果。如果異常時由于沒有我們想要的id記錄,我們可能返回一個NullObject 是更好的方式,但如果異常是由于數(shù)據(jù)庫連接的斷開,我們可能會繼續(xù)拋出異常,讓異常被更上層的函數(shù)看到,因為這個異常在此處我們已經(jīng)沒有能夠恢復的方法了。

通過SPL來構(gòu)建異常

Standard PHP Library (SPL) 標準庫中提供了一些predefined set of exceptions,我們可以基于這些預定于的異常進行擴展,得到滿足我們自己需求的代碼。這樣子做的好處是,我們能夠很方便的捕獲(catch)這些異常。

此處提供一個組織異常的方案:standard set of exception groupings 是一些預定義的異常,每次在使用的使用,通過composer引入。通過引入這一抽象層的目的是:讓我能更好的區(qū)分想要捕獲異常的粒度。

standard set of exception groupings 中的每個異常,都extend了SPL中的異常,而且實現(xiàn)了BrightNucleus\Exception\ExceptionInterface 接口,這么做可以方便我只捕獲框架相關(guān)的異常,通過只捕獲實現(xiàn)了接口的異常。

下面列舉了捕獲不同粒度的異常的方法:

  1. Catch all exceptions
    catch( Exception $exception ) {}
  2. Catch all exceptions thrown by a Bright Nucleus library
    catch( BrightNucleus\Exception\ExceptionInterface $exception ) {}
  3. Catch a specific SPL exception (BrightNucleus or not)
    catch( LogicException $exception )
  4. Catch a specific SPL exception thrown by a Bright Nucleus library
    catch( BrightNucleus\Exception\LogicException $exception ) {}

命名規(guī)范

目前命名的一個原則是:

  1. 該異常如果代表一個具體的錯誤,則使用一個過去時態(tài)的語句表明錯誤發(fā)生的原因
  2. 如果異常是一個基類,需要別的類進行擴展,則統(tǒng)一后綴Exception

看一個具體的例子:

假設(shè)我們有一個功能是從文件中讀取內(nèi)容,可能會有3種錯誤發(fā)生:

  1. 文件名不合法
  2. 文件不存在
  3. 文件不可讀

此時會有3種錯誤:

FileNameWasNotValid extends InvalidArgumentException
FileWasNotFound extends InvalidArgumentException
FileWasNotReadable extends RuntimeException

此時具體的錯誤都是過去式的句子,而基類都是帶有統(tǒng)一后綴的。

通過構(gòu)造函數(shù)捕獲異常邏輯

我們一般在實例化異常的時候,都是直接在使用的時候才去new出來,但是這種方式導致異常的代碼可能會比正常的業(yè)務邏輯還負雜,非常不適合閱讀,而且將相同的實例化邏輯放的到處都是,也不符合代碼重用的原則,我們舉個例子:

public function render( $view ) {
 
   if ( ! $this->views->has( $view ) ) {
      $message = sprintf(
         "The View "%s" does not exist.",
         json_encode( $view )
      );
 
      throw new ViewWasNotFound( $message );
   }
 
   echo $this->views->get( $view )->render();
}

上面的代碼中異常的處理邏輯比正常的業(yè)務邏輯還多,我們重構(gòu)下,將異常的構(gòu)建封裝起來:

class ViewWasNotFound extends InvalidArgumentException {
 
   public static function fromView( $view, $code = null, Exception $previous = null ) {
      $message = sprintf(
         "The View "%s" does not exist.",
         json_encode( $view )
      );
 
      return new static( $message, $code, $previous );
   }
}

我們可能會有多個構(gòu)造函數(shù),每個構(gòu)造函數(shù)有不同的應用場景,此時我們再來寫我們的render函數(shù):

public function render( $view ) {
 
   if ( ! $this->views->has( $view ) ) {
      throw ViewWasNotFound::fromView( $view );
   }
 
   echo $this->views->get( $view )->render();
}

現(xiàn)在代碼就非常簡潔了。

異常捕獲

問:我們需要捕獲什么異常?

答:只捕獲當前上下文下能夠處理的異常。

如果當前操作返回NullObject也ok,那在最外層套一個catch( Exception $exception ) {}就完全ok。但是如果當前操作只有正確才能保證后續(xù)操作繼續(xù),那你可能就需要捕獲那些你當前能恢復的異常,那些不能恢復的異常,則讓它往更上層去。

在SPL中,我們定義了兩大類異常:

  1. Logic exceptions

    邏輯異常是那些由于開發(fā)者的錯誤而導致的異常。你可能在請求一些不存在的值,或者調(diào)用傳遞的參數(shù)不對等等。這些異常在開發(fā)中都需要我們馬上處理掉的。在理想情況下,這些邏輯異常在實際生產(chǎn)系統(tǒng)中是不應該出現(xiàn)的。

  2. runtime exception

    運行時異常是一些在開發(fā)中不能控制的異常,如:數(shù)據(jù)庫鏈接的異常斷開,文件的讀寫權(quán)限不對等等。這些錯誤是無法避免的,我們不可能開發(fā)一個沒有錯誤的系統(tǒng),我們能做得只是當這些錯誤發(fā)生的時候,盡快的去通知系統(tǒng)管理員,而不是代碼出現(xiàn)fatal。

這就是為什么我們在開發(fā)中需要在某一軟件層捕獲運行時錯誤,而對于邏輯錯誤,我們盡可能讓它在開發(fā)時就讓他們暴露出來,好讓我們在開發(fā)時就解決它。

中心化的Error處理函數(shù)

我們將邏輯異常都pass through,沒有去捕獲,那么作為一個web應用,我們不能讓用戶無響應啊,因此我們需要通過一個中心化的處理函數(shù)來捕獲所有我們沒有處理的異常。

捕獲后,我們一般的工作是:記錄這些異常,記錄調(diào)用棧,方便我們?nèi)シ治鼋鉀Q這些問題。

對于這個工作,我推薦使用 BooBoo 來做。

總結(jié)

此處總結(jié)下我們的原則:

  • 對于運行時異常,我們盡量捕獲然后進行處理,重要的上報錯誤,讓管理員知道系統(tǒng)異常,而對于邏輯異常我們則是將其盡可能詳細的記錄下來,因為這些錯誤理論上是不應該出現(xiàn)在生產(chǎn)環(huán)境中。
  • 我們在捕獲異常的時候,只捕獲在該層級能處理的異常,對于不能處理的則讓它到上一層上去。
  • 我們需要一個全局的異常處理函數(shù),處理如返回html,json這種格式問題,以及處理錯誤信息的轉(zhuǎn)換(隱藏系統(tǒng)內(nèi)部錯誤信息),錯誤的記錄,現(xiàn)場環(huán)境的保存等公共邏輯。

一個示例

講了這么多,還是那句話

talk is cheap, show me the code

我們基于的一個基本代碼是:

$user = $this->usersGateway->fetchOneById($userId);

if (!$user) {
    throw new Exception('User with the ID: ' . $userId . ' does not exist');
}

用戶定義異常

上面針對找不到user的情況,我們只是簡單的拋出了異常。但是上面的問題是:僅僅拋出異常不足以幫助我們定位問題,單一的異常類型,不能讓我們針對不同的類型做出不同行為,因此解決方法是自定義異常。

class UserNotFoundException extends RuntimeException
{
}

//...

throw new UserNotFoundException('User with the ID: ' . $userId . ' does not exist');

格式化異常

現(xiàn)在我們已經(jīng)有了異常類,并且異常的生成和異常消息都是異常類本身的職責,因此我們根據(jù)單一職責(SRP)將其組織到異常類中:

class UserNotFoundException extends RuntimeException
{
    public static function forUserId(string $userId) : self
    {
        return new self(sprintf(
            'User with the ID: %s does not exist',
            $userId
        ));
    }
}

在使用異常的地方我們簡單的調(diào)用下面的代碼:

throw UserNotFoundException::forUserId($userId);

聚合異常

根據(jù)單一職責(SRP)我們將相同異常放到一起,不同的功能拆分出來,看例子:

class UserException extends Exception
{
    public static function forEmptyEmail() : self
    {
        return new self("User's email must not be empty");
    }

    public static function forInvalidEmail(string $email) : self
    {
        return new self(sprintf(
            '%s is not a valid email address',
            $email
        ));
    }

    public static function forNonexistentUser(string $userId) : self
    {
        return new self(sprintf(
            'User with the ID: %s does not exist',
            $userId
        ));
    }
}

在上面的例子中,異常類UserException有兩個功能,第一個負責User的驗證異常,另一個則是沒有用戶的異常,因此我們應該拆分為兩個:

class InvalidUserException extends DomainException
{
    public static function forEmptyEmail() : self
    {
        return new self("User's email address must not be empty");
    }

    public static function forInvalidEmail(string $email) : self
    {
        return new self(sprintf(
            '%s is not a valid email address',
            $email
        ));
    }
}
class UserNotFoundException extends RuntimeException
{
    public static function forUserId(string $userId) : self
    {
        return new self(sprintf(
            'User with the ID: %s does not exist',
            $userId
        ));
    }
}

此時我們就能針對不同的異常類采取不同的措施,可能我們會根據(jù)異常類返回合適的 HTTP status codes。

異常代碼

異常的構(gòu)造函數(shù)接受code作為第二個參數(shù),所以我們可以通過不同的錯誤碼來標志不同的錯誤。

class UserNotFoundException extends RuntimeException
{
    public static function forUserId(string $userId) : self
    {
        return new self(
            sprintf(
                'User with the ID: %s does not exist',
                $userId
            ), 
            ErrorCodes::ERROR_USER_NOT_FOUND
        );
    }
}

我們會將所有的錯誤碼都放到一個文件中,方便管理。

組件級別的異常

當我們提供一個庫給別人使用的時候,我們可能希望能夠捕獲我們庫級別的異常,這通過一個模式Marker Interface可以實現(xiàn):

namespace App\Domain\Exception;

interface ExceptionInterface
{
}

class UserNotFoundException extends RuntimeException impements ExceptionInterface
{
    public static function forUserId(string $userId) : self
    {
        return new self(
            sprintf(
                'User with the ID: %s does not exist',
                $userId
            ), 
            ErrorCodes::ERROR_USER_NOT_FOUND
        );
    }
}

我們通過在每個命名空間都聲明一個ExceptionInterface類來實現(xiàn),這樣我們就可以通過代碼try{}catch(ExceptionInterface $e){}來捕獲所有本庫的錯誤。

錯誤處理

上代碼:

class UserController extends BaseController
{
    public function viewUserAction(RequestInterface $request)
    {
        try {
            $user = $this->userService->get($request->get('id'));

            return new JsonResponse($user->toArray()); 
        } catch (\Exception $ex) {
            return new JsonResponse([
                'error' => $ex->getCode(),
                'message' => $ex->getMessage(),
            ], 500);
        }
    }
}

上面的處理中,我們在controller中通過一個最外層的try{}catch{}捕獲了所有異常,但是我們針對不同的需求可能會有不同的返回格式的要求,可能我們需要針對參數(shù)的不同返回html或者json格式,另外我們也不希望底層的錯誤信息,如:數(shù)據(jù)庫連接失敗,這樣子的錯誤信息直接返回給調(diào)用方,那怎么解決呢?

這就要用到PHP的全局異常處理函數(shù)了,通過set_exception_handler來設(shè)置,另外推薦除了 BooBoo 另外一個開源庫:Whoops,能很好的解決這個問題。

你的觀點

相信你在實際工作中肯定也遇到過好多類似的困擾,你在實際工作中也有你自己的一套解決方案,期待你的分享,讓更多的人知道好的優(yōu)秀的方案,所以期待你在評論區(qū)寫下你的方案。

這是 php異常系列 的第一篇,你的鼓勵是我繼續(xù)寫下去的動力,期待我們共同進步。

參考文章

Structuring PHP Exceptions

A Crash Course of Changes to Exception Handling in PHP 7

Best practices for handling exceptional behavior

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 異常(Exception)用于在指定的錯誤發(fā)生時改變腳本的正常流程。 什么是異常? PHP 5 提供了一種新的面向...
    josephok閱讀 619評論 0 7
  • 異常與錯誤的區(qū)別 關(guān)于異常處理這一塊,在官方的手冊上介紹的不夠詳細,所以我在這里再做一個相對詳細一點的總結(jié)...
    四月不見閱讀 2,519評論 0 22
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,697評論 19 139
  • 1 那天考長途車試,由于只有我一個女學員,只能安排我和陌生女學員同住。教練問我是否介意,我想了一下,覺得應該問題...
    風楊柳飄閱讀 397評論 0 0
  • 時間近八點,達康書記都已經(jīng)開始在電視上部署工作了,我才驚覺:呀!今天還沒有去鮮奶店取牛奶。因為急著回來看漢東boy...
    不愛看文獻的兔兔閱讀 1,251評論 2 2

友情鏈接更多精彩內(nèi)容