如今开发先后端分离变得愈来愈流行了,后端只提供接口返回json格式的数据,即便是错误信息也要以json格式来返回,然而目前不管是Laravel框架仍是ThinkPHP框架,都只提供了返回json数据的方法,对异常的处理并非以json格式来返回给咱们,因此这里就须要咱们本身来改写。php
首先咱们在app/Exceptions
目录新建一个ExceptionHandler.php
继承自Handler.php
laravel
namespace App\Exceptions; class ExceptionHandler extends Handler { }
而后咱们在bootstrap/app.php
中,使用咱们自定义的异常处理类ExceptionHandler替换掉默认的Handler类shell
//改成咱们自定义的ExceptionHandler类 $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\ExceptionHandler::class );
接下来咱们就开始重写渲染方法json
在render方法里,咱们根据.env文件中的APP_DEBUG来判断,若是是调试模式,咱们仍是按照默认方式来渲染错误,若是是非调试模式,咱们就返回JSON格式的信息bootstrap
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为后缀保存错误日志服务器
咱们打开这个日志文件查看记录的错误信息,咱们能够发现错误信息记录的很是详细,除了错误说明以外,还记录了调用栈,以下图所示app
基本上红框里的信息就够咱们排错了,不须要像如今这样记录的这么详细,因此要想不记录调用栈,咱们能够重写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作先后端分离项目的你有所帮助。