現在開發前后端分離變得越來越流行了,后端只提供介面回傳json格式的資料,即使是錯誤資訊也要以json格式來回傳,然而目前無論是Laravel框架還是ThinkPHP框架,都只提供了回傳json資料的方法,對例外的處理并不是以json格式來回傳給我們,所以這里就需要我們自己來改寫,
首先我們在app/Exceptions目錄新建一個ExceptionHandler.php繼承自Handler.php
namespace App\Exceptions;
class ExceptionHandler extends Handler
{
}
然后我們在bootstrap/app.php中,使用我們自定義的例外處理類ExceptionHandler替換掉默認的Handler類
//改為我們自定義的ExceptionHandler類
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\ExceptionHandler::class
);
接下來我們就開始重寫渲染方法
在render方法里,我們根據.env檔案中的APP_DEBUG來判斷,如果是除錯模式,我們還是按照默認方式來渲染錯誤,如果是非除錯模式,我們就回傳JSON格式的資訊
namespace App\Exceptions;
use Exception;
class ExceptionHandler extends Handler
{
public function render($request, Exception $exception)
{
if (env('APP_DEBUG')) {
return parent::render($request, $exception);
}
return response()->json([
'code' => $exception->getCode(),
'msg' => $exception->getMessage()
]);
}
}
這樣我們就可以根據APP_DEBUG的值設定是否回傳JSON格式的資料了,現在我們把.env的APP_DEBUG的值設為false來測驗一下,然后我們故意把代碼寫錯,通過postman或瀏覽器來訪問介面
Route::get('/', function () {
//這是一段缺少了分號的代碼,會報例外
echo 'Hello World!'
});

在APP_DEBUG=true的情況下還仍然是默認渲染,方便我們查找錯誤排錯
例外類默認會把例外以日志的形式記錄在storage/logs目錄下,并且以laravel-日期(YYYY-MM-DD)命名的形式,.log為后綴保存錯誤日志

我們打開這個日志檔案查看記錄的錯誤資訊,我們可以發現錯誤資訊記錄的非常詳細,除了錯誤說明之外,還記錄了呼叫堆疊,如下圖所示

基本上紅框里的資訊就夠我們排錯了,不需要像現在這樣記錄的這么詳細,所以要想不記錄呼叫堆疊,我們可以重寫report方法
首先我們看一下框架的report方法,代碼在(src/Illuminate/Foundation/Exceptions/Handler.php),我用紅框框起來的代碼就是呼叫堆疊資訊,我們在重寫這個方法時只需要完全拷貝這個方法里的所有代碼到我們自定義的report方法里,然后把紅框里的代碼去掉即可

我們在我們自定義的例外處理類ExceptionHandler.php中重寫report方法
public function report(Exception $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
return $this->container->call($reportCallable);
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $exception;
}
$logger->error(
$exception->getMessage()
);
}
然后我們再重新請求一下介面再去查看錯誤日志的記錄,可以發現確實沒有記錄呼叫堆疊資訊了,但是下面的資訊還是不夠,我們沒法根據下面的資訊判斷錯誤發生在哪一個檔案和哪一行,如果能在記錄錯誤資訊的時候同時記錄發生錯誤的檔案和行就更好了,所以借著修改report方法

public function report(Exception $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
return $this->container->call($reportCallable);
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $exception;
}
$logger->error(
$exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
);
}
在代碼里我通過exception的getFile()、getLine()方法加上了檔案和行數,保存代碼再次訪問介面,查看錯誤日志檔案我們可以看到發生錯誤的檔案和行數已經記錄下來了,有了這些資訊基本我們就可以找到錯誤

截止到這里實作最初的需求我們的ExceptionHandler.php只需要有這些代碼
namespace App\Exceptions;
use Exception;
use Illuminate\Support\Reflector;
use Psr\Log\LoggerInterface;
class ExceptionHandler extends Handler
{
public function render($request, Exception $exception)
{
if (env('APP_DEBUG')) {
return parent::render($request, $exception);
}
return response()->json([
'code' => $exception->getCode(),
'msg' => $exception->getMessage()
]);
}
public function report(Exception $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
return $this->container->call($reportCallable);
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $exception;
}
$logger->error(
$exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
);
}
}
然后還不夠,我們發現剛剛我們把服務器端的錯誤資訊以JSON格式回傳給客戶端了,這是不允許的,我們應該只把一些客戶端錯誤回傳給客戶端,比如密碼不足六位、身份證不合法諸如此類,而服務端出現錯誤時我們只回傳給客戶端一個模糊的資訊即可,比如“服務器錯誤”,把真實的服務器錯誤資訊記錄在日志里面方便開發人員排查錯誤
所以我們需要定義一個客戶端例外專門用戶回傳客戶端錯誤,使用如下命令在app/Exceptions目錄下生成一個ClientException.php檔案
php artisan make:exception ClientException
修改為構造方法為如下代碼
namespace App\Exceptions;
use Exception;
class ClientException extends Exception
{
public function __construct($code, $msg)
{
parent::__construct($msg, $code);
}
}
接著我們繼續修改ExceptionHandler.php
namespace App\Exceptions;
use Exception;
use Illuminate\Support\Reflector;
use Psr\Log\LoggerInterface;
class ExceptionHandler extends Handler
{
/**
* @var int 錯誤碼
*/
protected $code;
/**
* @var string 錯誤資訊
*/
protected $message;
protected $dontReport = [
ClientException::class
];
public function render($request, Exception $exception)
{
if ($exception instanceof ClientException) {
$this->code = $exception->getCode();
$this->message = $exception->getMessage();
} else {
if (env('APP_DEBUG')) {
return parent::render($request, $exception);
}
$this->code = 500;
$this->message = '服務器錯誤';
}
return response()->json([
'code' => $this->code,
'msg' => $this->message
]);
}
public function report(Exception $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
return $this->container->call($reportCallable);
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $exception;
}
$logger->error(
$exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
);
}
}
對于上面的修改做一下說明,laravel的$dontReport屬性的例外類都不會被上報,因為客戶端錯誤資訊我們不需要記錄,所以將其添加到$dontReport屬性里,并且在render方法里把例外大概分為了兩大類,一大類就是客戶端例外,另一大類就是服務器例外,我們把服務器例外統一code為500,錯誤資訊為服務器錯誤,將真實的錯誤資訊記錄在了錯誤日志里,避免把服務器資訊暴露給了客戶端,
現在我們來測驗我們重寫例外的結果
假如我們想回傳客戶端例外,比如沒有權限,這類客戶端例外在錯誤日志里都不會產生記錄,我們本身也不需要記錄
Route::get('/', function () {
throw new \App\Exceptions\ClientException(403, '你沒有權限');
});

對于服務器端的錯誤,如少些了分號,客戶端就只會知道服務器的某個介面出了問題,但是不清楚具體問題是什么
Route::get('/', function () {
echo 'Hello World!'
});

但是真實的錯誤資訊會記錄在錯誤日志里,我們仍舊可以通過錯誤日志來修改我們服務端的錯誤

我們還可以在render方法中加入告警代碼,如果是服務端錯誤就給管理員發送郵件,
至此,我們的重寫Laravel例外處理類就算完成啦,希望對正在準備使用Laravel做前后端分離專案的你有所幫助,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236920.html
標籤:PHP
上一篇:Redis的持久化
下一篇:PHP設計模式之原型模式
