乘风原创程序

  • Laravel如何实现适合Api的异常处理响应格式
  • 2020/6/14 9:40:34
  • 前言

    Laravel全局捕获异常后,会把异常转为相应的数据格式返回给用户。如果想要规定的数据格式相应,那我们只需重写异常捕获后的处理方法即可。

    异常处理流程

    Illuminate\Foundation\Exception\Handler 中的 render 方法用来将异常转化为响应。

    public function render($request, Exception $e)
    {
     if (method_exists($e, 'render') && $response = $e->render($request)) {
     return Router::toResponse($request, $response);
     } elseif ($e instanceof Responsable) {
     return $e->toResponse($request);
     }
    
     $e = $this->prepareException($e);
    
     if ($e instanceof HttpResponseException) {
     return $e->getResponse();
     } elseif ($e instanceof AuthenticationException) {
     return $this->unauthenticated($request, $e);
     } elseif ($e instanceof ValidationException) {
     return $this->convertValidationExceptionToResponse($e, $request);
     }
    
     return $request->expectsJson()
       ? $this->prepareJsonResponse($request, $e)
       : $this->prepareResponse($request, $e);
    }

    render() 中又调用了 prepareException() 对部分异常进行预处理,但并未执行转化为响应的操作。

    ModelNotFoundException 一般在模型查找不到抛出,prepareException() 中它被转为 Symfony 包中NotFoundHttpException,默认状态码404;

    AuthorizationException 在 Policy 权限未通过时抛出,prepareException() 中它被转为 Symfony 包中 AccessDeniedHttpException,默认状态码403;

    TokenMismatchException 在 CSRF 验证未通过时抛出,prepareException() 中它被转为 Symfony 包中 HttpException,给定状态码419;

    其他异常直接返回。

    protected function prepareException(Exception $e)
    {
     if ($e instanceof ModelNotFoundException) {
     $e = new NotFoundHttpException($e->getMessage(), $e);
     } elseif ($e instanceof AuthorizationException) {
     $e = new AccessDeniedHttpException($e->getMessage(), $e);
     } elseif ($e instanceof TokenMismatchException) {
     $e = new HttpException(419, $e->getMessage(), $e);
     }
    
     return $e;
    }

    在回到 render() ,预处理异常之后,又分别对 HttpResponseException、AuthenticationException 和 ValidationException 单独处理,并转为响应返回。

    除此以外的异常,都在 prepareJsonResponse() 或 prepareResponse() 处理 ,expectsJson() 用来判断返回 json 响应还是普通响应。

    修改异常响应格式

    了解了异常处理流程,接下来就处理异常响应格式。

    修改登录认证异常格式

    由上文可知,AuthenticationException 被捕获后,调用 unauthenticated() 来处理。

    protected function unauthenticated($request, AuthenticationException $exception)
    {
     return $request->expectsJson()
        ? response()->json(['message' => $exception->getMessage()], 401)
        : redirect()->guest($exception->redirectTo() ?? route('login'));
    }

    在 appExceptionsHandler.php 中重写 unauthenticated() 使其返回我们想要的数据格式。

    protected function unauthenticated($request, AuthenticationException $exception)
    {
     return $request->expectsJson()
      ? response()->json([
       'code' => 0,
       'data' => $exception->getMessage(),
      ], 401)
      : redirect()->guest($exception->redirectTo() ?? route('login'));
    }

    修改验证异常格式

    同样由上文可知,ValidationException 被捕获后交由 convertValidationExceptionToResponse() 处理,进入此方法后我们需要继续追踪,若是需要 json 响应,最终交由 invalidJson() 处理。

    protected function convertValidationExceptionToResponse(ValidationException $e, $request)
    {
     if ($e->response) {
      return $e->response;
     }
    
     return $request->expectsJson()
        ? $this->invalidJson($request, $e)
        : $this->invalid($request, $e);
    }
    protected function invalidJson($request, ValidationException $exception)
    {
     return response()->json([
      'message' => $exception->getMessage(),
      'errors' => $exception->errors(),
     ], $exception->status);
    }

    我们继续在 appExceptionsHandler.php 重写 invalidJson() 即可自定义返回格式。

    protected function invalidJson($request, ValidationException $exception)
    {
     return response()->json([
      'code' => 0,
      'data' => $exception->errors(),
     ], $exception->status);
    }

    修改其他异常格式

    其他异常是调用 prepareJsonResponse() 来处理,此方法又调用 convertExceptionToArray() 来处理响应格式。

    protected function prepareJsonResponse($request, Exception $e)
    {
     return new JsonResponse(
      $this->convertExceptionToArray($e),
      $this->isHttpException($e) ? $e->getStatusCode() : 500,
      $this->isHttpException($e) ? $e->getHeaders() : [],
      JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
     );
    }
    protected function convertExceptionToArray(Exception $e)
    {
     return config('app.debug') ? [
      'message' => $e->getMessage(),
      'exception' => get_class($e),
      'file' => $e->getFile(),
      'line' => $e->getLine(),
      'trace' => collect($e->getTrace())->map(function ($trace) {
       return Arr::except($trace, ['args']);
      })->all(),
     ] : [
      'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
     ];
    }

    在 appExceptionsHandler.php 中重写 convertExceptionToArray() 来自定义其他异常响应格式。

    protected function convertExceptionToArray(Exception $e)
    {
     return config('app.debug') ? [
      'code' => 0,
      'data' => $e->getMessage(),
      'exception' => get_class($e),
      'file' => $e->getFile(),
      'line' => $e->getLine(),
      'trace' => collect($e->getTrace())->map(function ($trace) {
       return Arr::except($trace, ['args']);
      })->all(),
     ] : [
      'code' => 0,
      'data' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
     ];
    }

    强制 json 响应

    代码中多次出现了 expectsJson() ,此方法是用来判断返回 json 响应还是普通响应。

    public function expectsJson()
    {
     return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
    }

    以下两种条件下,会返回json响应。

    非XML请求、非pjax并且 Headers 中 Accept 设置为接收所有格式响应;

    Headers Accept 设置为 /json、+json。如:Accept:application/json。

    除此之外的情况,将不会响应json。我们可以利用中间件强制追加 Accept:application/json,使异常响应时都返回json。(参考教程 L03 6.0 中提到的方法)

    创建中间件 AcceptHeader

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    
    class AcceptHeader
    {
     public function handle($request, Closure $next)
     {
      $request->headers->set('Accept', 'application/json');
    
      return $next($request);
     }
    }

    在 app/Http/Kernel.php 中,将中间件加入路由组即可。

    protected $middlewareGroups = [
     'web' => [
      .
      .
      .
     'api' => [
      \App\Http\Middleware\AcceptHeader::class,
      'throttle:60,1',
      'bindings',
     ],
    ];

    大功告成。

    总结