TP5_介面開發之全域性異常控制
前言:
說到異常控制,也許很多會比較陌生,我身邊很少人會去寫拋異常的程式碼。但是異常用好了是非常的方便大家開發。首先我們來回顧下哪裡可以看到異常,首先我們用框架開發的時候,我們的程式碼出錯或者別的東西。如果開啟除錯模式的話,瀏覽器頁面會報出錯誤的位置,還有呼叫的順序,甚至還有記憶體的使用等等很多資訊。這就是框架在捕獲異常時候,將這些資料獲取然後渲染了一套html,才讓我們這麼直觀的看到錯誤。那麼既然開發框架的開發者為了方便我們使用,使用了丟擲異常,捕獲異常。我們也可以照貓畫虎,來學習下。
丟擲異常
在捕獲異常之前我們先來看看丟擲異常。
雖然可能不少朋友不常用丟擲異常,可是丟擲異常的方法,大家一定不會陌生
throw new Exception('錯誤');
沒錯,使用throw命令 後面跟個new Exception 就丟擲了。其實大家仔細觀察發現,其實這個new Exception其實就是 例項化了一個類的物件。那麼丟擲異常的本質,實際上就是 throw 一個異常類的物件。
那麼怎麼樣才算是一個異常類呢?
我們平時丟擲最多的異常類就是think\Exception 這是一個tp封裝的一個異常類。

image.png
我們發現這個異常類繼承一個基礎異常類,由此可知,只有 直接繼承 或 鏈式繼承 這個最最最基礎的Exception類的類才算做一個異常類。
那麼我業務需要,我們需要來構建我們自己的異常類,來方便我們丟擲。
在寫自己的異常類之前,我們需要了解,我們的異常類需要包含哪些資訊。我這裡寫出3個資訊在介面開發中我認為是足夠了。
首先我們建立一個基礎異常類,我認為在開發中,只要是新的型別的類,都應該去建立一個Base類用來繼承,先不管用不用的上,當用上時確實會節省很多時間,這也是面向物件程式設計的優勢
class BaseException extends Exception { //預設返回碼為400引數錯誤 public $code = 400; //預設返回資訊為引數錯誤 public $msg = 'parameter error'; //預設返回通用錯誤碼 public $errorCode = 10000; }
我們有了基類之後我們新建自定義異常類時繼承一下就好了。後來我發現有些同類錯誤,但是錯誤資訊又有點小差異這種去建立兩個異常類又有點傻逼。所以我在基類中加上一個構造方法
//基礎異常類,用於被各種不同的異常繼承 class BaseException extends Exception { //預設返回碼為400引數錯誤 public $code = 400; //預設返回資訊為引數錯誤 public $msg = 'parameter error'; //預設返回通用錯誤碼 public $errorCode = 10000; //設計建構函式,方便某些異常類需要傳入引數修改 public function __construct($params = []) { if (!is_array($params) || empty($params)) { //如果不是陣列或為空,則代表不修改當前的類成員變數,也就是用預設的值來返回給客戶端 return; } if (key_exists('code', $params)) { $this->code = $params['code']; } if (key_exists('msg', $params)) { $this->msg = $params['msg']; } if (key_exists('errorCode', $params)) { $this->errorCode = $params['errorCode']; } } }
這樣的話,我們只需要寫一些比較大體的異常類,然後在建構函式中傳入我想修改的資訊就可以。
什麼是全域性異常控制
然後我們需要思考,我們 為什麼要丟擲異常 , 丟擲異常和返回false有什麼區別
下面我們設想下一個場景:
假如我們現在有個控制器層,控制器去呼叫一個服務層的方法,服務層程式碼中又呼叫了模型層的方法,在這個方法中間,我們判斷有個什麼不太對的地方,我們需要返回個客戶端一個報錯資訊,比如,引數錯誤或者別的東西。那麼如果我們要使用返回false的話,則需要,從Model層的方法中返回false,然後在service層中接收,再返回false,然後控制器裡接收,再根據返回的false的地方構造報錯資訊,轉換為json,在返回給客戶端。
那麼丟擲異常的優勢就提現出來了,首先我們丟擲的異常物件可以包含一些報錯資訊,其次,丟擲異常會直接 中斷 後面的所有程式碼的執行,非常的乾脆。
現在來看看沒有錯誤的情況我們的操作流程

正常不出錯的情況
也許沒有這麼多層,可能就是一個模型就完了 我只是打個比方。
那麼如果出錯的情況,流程應該怎麼走呢?

異常控制
我們知道框架有一個異常控制,會將丟擲的異常處理成html頁面。我們希望有個類似的東西來幫我們捕獲我們丟擲的異常,並且,將錯誤資訊直接返回給客戶端。這樣我們就不用一層一層的往控制器傳。
那麼事實上,TP5的確給了我們這樣的東西,在手冊中名字叫異常處理接管。從名字不難看出,這個就是我們想要的功能,只是tp的文件中寫的比較生澀,不太容易懂。必須要結合案例來學習。
TP5異常接管的使用
我們要接管tp5的異常控制,我們需要知道tp5之前異常控制的地方在哪。tp5將這個路徑寫到了配置裡了

原本tp異常控制類
我們將我們自己的異常控制類建立好之後將完整的帶名稱空間的路徑配置到這裡。
再看看自己的異常控制類如何寫
其實對於異常控制來說,捕獲異常,分析異常類。。。。還是非常複雜的,我們實際上只需要把最後一步渲染成html這一步改成我們需要的返回客戶端資料。所以我們將之前的tp的異常控制繼承,然後重寫他渲染html那個方法供我們使用就好了

tp異常控制的渲染方法
那麼繼承了tp的Handle類,重寫這個render方法,當然同樣的render方法傳入的異常物件$e 我們繼承之後也回收到
class ExceptionHandler extends Handle { //同樣的這三個引數,建立起來,方便使用 private $code; private $msg; private $errorCode; public function render(Exception $e) { } }
在書寫我們的程式碼之前,我們需要理解一個非常重要的概念: 異常的分類

異常分類
我們將我們自己設計的異常,分為一類。
將我們不可控的異常,分為一類。
我在圖中有舉了一些例子。
那麼,我們如何 區分 這兩類異常呢?
細心的朋友肯定會發現,我們自己設計的異常我們都會繼承我們自己寫BaseException類,通過這一點就可以區分,我們捕獲的異常到底是哪一類的。如果不是我們控制範圍之內的異常,我們就應該異常他的異常資訊,報一個通用的異常信心,比如未知錯誤,錯誤碼500 那種,這樣也能保護我們自己的一些資訊。
除了報通用的錯誤資訊之外,我們還應該記錄日誌,方便我們排查我們程式碼的錯誤
那麼我們現在需要思考一個新的問題,這個功能是屬於錦上添花的功能
那就是,我們把異常接管了之後,遇到非我們設計的異常,就會報通用錯誤,這個設定,在生產模式下沒有問題。但是在開發階段,我們更希望的是看到框架給我們設計好的html報錯頁面,方便我們定位錯誤。
基於以上的考慮,我通過判斷debug是否開啟來判斷是否處於生產模式,如果是開發模式的話,就呼叫父類的方法render方法,這樣就可以渲染出友好的html報錯頁面。
說了這麼多也來看看程式碼吧(涉及到記錄日誌方法,大家可以根據自己的需求來,記錄資料庫也可以,我就不過多介紹,不是本文重點)
namespace app\lib\exception; //用於繼承tp5的全域性異常處理類,用來重寫其中的render方法來做最終的異常處理 use think\Config; use think\exception\Handle; use Exception; use think\Log; //總的異常處理類 class ExceptionHandler extends Handle { private $code; private $msg; private $errorCode; public function render(Exception $e) { //如果這個傳入的異常類是我們自定義的異常類的話,就說明這個異常在我們的控制之中 if ($e instanceof BaseException) { //將該異常設定好的屬性給賦值到總的異常處理類 $this->code = $e->code; $this->msg = $e->msg; $this->errorCode = $e->errorCode; } else { //判斷配置中的dbug是否開啟確定開發或生產模式 if (Config::get('app_debug')) { //如果是開發模式 return parent::render($e); } else { //如果是生產模式,則返回與設定好的未知錯誤的json $this->code = 500; $this->msg = 'Unknown Error'; $this->errorCode = 999; } //全域性的記錄日誌 $this->recordErrorLog($e); } $request = request(); $result = [ 'errorCode' => $this->errorCode, 'msg' => $this->msg, 'url' => $request->url() ]; //返回異常資訊到客戶端 return json($result, $this->code); } /** * @param $e * 傳入異常物件 */ private function recordErrorLog(Exception $e) { //由於在config檔案中關閉了tp5自己的日誌系統,我們需要重新初始化下 Log::init([ 'type' => 'file', 'path' => LOG_PATH, 'level' => ['error'] ]); //記錄日誌,傳入異常的資訊 Log::record($e->getMessage(), 'error'); } }
最後將方法寫好之後,不要忘了在config檔案中配置你的異常控制類
應用丟擲異常
那麼說了這麼多,現在拿出一個例項來展示下。
這次測試的介面是一個非常簡單的請求資源介面。我們設計的異常有兩個,第一就是客戶端傳遞過來的id不是正整數。第二個異常就是請求的資源為空。同樣的我也故意寫一個程式碼錯誤丟擲一個非我們自己設計的異常。
- 我們先看控制器,很明顯能看出來,當我去呼叫模型方法查出來的資料為空時,我會丟擲一個BannerMisssException異常。
/** * @url http://local.jxshop.com/api/v1/banner/1 * @http GET * @param $id integer banner的id * @throws BannerMissException * @return mixed json格式的banner資料 */ public function getBanner($id) { //例項化id驗證器物件並呼叫上面的goCheck方法,來獲取並驗證資料 IdMustBePositiveInt::instance()->goCheck(); //使用模型上的獲取banner資料方法 $banner=BannerModel::getBannerInfoById($id); if (!$banner) { throw new BannerMissException(); } return $banner; }
-
我們來看看異常類是怎麼寫的
BannerMiss異常類
-
拿postMan來測試一下,我們傳遞一個數據庫沒有的banner_id
測試結果
大家可以看到我們的異常控制起作用了。我們控制器中拿到banner_id 10000 然後到資料庫中去尋找,資料庫沒有查到,返回一個空值,控制器中對返回值進行判斷,如果為空,丟擲異常。這時,異常物件就會被我們設計好的異常控制捕獲,並將異常物件中包含的報錯資訊取出,轉換為json。返回給客戶端。
如果傳入的banner_id 在資料庫中能查到,則不丟擲異常,返回應該查詢到的資料

不拋異常
那麼我們再試一試傳遞非正整數的值去呢?

傳遞負數
同樣的會丟擲異常,這個異常有別於剛才的BannerMiss。這是一個 引數錯誤異常 。
也許有人會有疑問,這個異常是從哪裡跑出來的呢?
其實答案就在goCheck()方法中。這個方法是一個通用的驗證資料方法,我在之前的 ofollow,noindex">TP5巧用驗證器有過介紹,這裡就不介紹了。直接貼程式碼
/** * 獲取傳遞引數,並驗證 * @return array * @throws Exception * @throws ParameterException */ public function goCheck() { //接收引數 $request = Request::instance(); //通過param方法獲取到所有的引數 $params = $request->param(); //由哪個物件來呼叫goCheck方法,就是由哪個物件來呼叫check方法,將接收的所有引數傳遞進去 $result = $this->batch()->check($params); if (!$result) { //如果結果為false,呼叫getError方法獲取錯誤資訊 $error = $this->getError(); //丟擲引數錯誤異常 throw new ParameterException(['msg' => $error]); } else { //呼叫獲取過濾引數的方法,返回給控制器 return $this->getDataByRule($params); } }
這又展示了丟擲異常的好處,異常是直接中斷程式程序,將異常物件直接拋到最頂端的全域性異常控制裡,在model裡可以拋,在service裡也可以,控制器裡也可以,驗證器裡也行。有不正確的地方就丟擲異常,給客戶端友好提示。
之前展示的都是我們設計好的異常,那麼如果是我們程式碼寫的不對,或者別的什麼我們沒有考慮到的異常出現怎麼辦呢?本文之前也有提過,如果是開發模式,異常控制捕獲後會渲染框架自己的報錯html。如果是生產模式,會返回給客戶端一個通用錯誤資訊。並記錄日誌。
那麼我們現在演示一下。

非設計異常
我們在控制器中加入一個除數為0的程式碼。我們都知道這樣寫肯定是要報錯的。
首先我們看開發模式下

開發模式下的非設計異常
伺服器返回了我們熟悉的tp報錯頁面。準確的定位,還有程式碼執行的堆疊資料
那麼現在我將程式碼改為生產模式試試

關閉debug

生產模式下非設計異常
這時,返回的就是一個通用的錯誤資訊。讓客戶端收到比較友好的json資訊,而不是一個HTML程式碼。也保護了我們程式碼和路徑不被暴露。
之前認真看了程式碼的朋友一定記得,我們除了丟擲通用錯誤資訊之外,我們還記錄日誌,那麼我們去看看日誌裡有沒有我們想要的內容。

日誌
我們看到根目錄中log檔案,根據日期生成了日誌檔案

日誌內容
日誌記錄錯誤時間,請求ip 請求地址。錯誤資訊。方便我們開發者回溯錯誤,修改bug
好了,兩種例子也展示完了,這個全域性異常控制,其實我想寫了很久了,一直沒有寫的原因還是感覺自己的理解不夠深刻,希望在文章中更多的表達清除自己的意思。如果有疑問的地方,歡迎郵件[email protected] 有沒有寫對的地方,也希望能得到大神的指點。感謝
以上