Laravel 错误处理:自定义异常响应 2 个改进

问题

程序出现或主动扔出异常,如何自定义这些异常的响应,以便为用户提供更友好的错误提示?

答案

Laravel 中所有异常都是由 App\Exceptions\Handler 类处理。打开此类文件,你可以发现 render 方法,render 方法负责将异常转换为 HTTP 响应。默认情况下,异常将传递给为你生成响应。

这里把异常分为「 内置异常」和「自定义异常」两大类别来分别处理。

内置异常

内置异常类包括了 PHP 和 Laravel 的内置异常。

例如需要自定义的 404 页面返回内容,先找到对应的异常类是 NotFoundHttpException

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class Handler extends ExceptionHandler
.
.
.
    /**
     * 将异常转换为 HTTP 响应。
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if ($exception instanceof NotFoundHttpException) {
            return response('抱歉,未找到数据!', 404);
        }

        return parent::render($request, $exception);
    }
.
.
.

这时候如果访问一个未定义的 URL:

抱歉,未找到数据

Laravel 可以轻松显示各种 HTTP 状态代码的自定义错误模板页面。文档详情

自定义异常

实际开发时,常常需要自定义一些异常类。

例如定义一个 ExampleException 异常类:

<?php

namespace App\Exceptions;

use Exception;

class ExampleException extends Exception
{
    /**
     * 转换异常为 HTTP 响应
     *
     * @param  \Illuminate\Http\Request
     * @return \Illuminate\Http\Response
     */
    public function render($request)
    {
        return response($this->getMessage() ?: '发生异常啦');
    }
}

当在自定义的异常类定义了 render 方法,它会被框架自动调用。

为了简单就在 web.php 路由的闭包中扔出该异常:

Route::get('/example', function () {
    throw new \App\Exceptions\ExampleException('我是一个异常啦');
});

到浏览器中访问:

我是一个异常啦

更多内置异常处理

App\Exceptions\Handler 的父类提供了异常更易理解的处理方法,重写这些方法能轻松处理对应的异常:

将身份验证异常转换为响应

/**
 * 将身份验证异常转换为响应。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Auth\AuthenticationException  $exception
 * @return \Symfony\Component\HttpFoundation\Response
 */
protected function unauthenticated($request, AuthenticationException $exception)
{
    return $request->expectsJson()
                ? response()->json(['message' => $exception->getMessage()], 401)
                : redirect()->guest($exception->redirectTo() ?? route('login'));
}

将给定的验证异常创建响应对象

/**
 * Create a response object from the given validation exception.
 *
 * @param  \Illuminate\Validation\ValidationException  $e
 * @param  \Illuminate\Http\Request  $request
 * @return \Symfony\Component\HttpFoundation\Response
 */
protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
    if ($e->response) {
        return $e->response;
    }

    return $request->expectsJson()
                ? $this->invalidJson($request, $e)
                : $this->invalid($request, $e);
}

/**
 * 将验证异常转换为JSON响应。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Validation\ValidationException  $exception
 * @return \Illuminate\Http\JsonResponse
 */
protected function invalidJson($request, ValidationException $exception)
{
    return response()->json([
        'message' => $exception->getMessage(),
        'errors' => $exception->errors(),
    ], $exception->status);
}

/**
 * 将验证异常转换为响应。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Validation\ValidationException  $exception
 * @return \Illuminate\Http\Response
 */
protected function invalid($request, ValidationException $exception)
{
    return redirect($exception->redirectTo ?? url()->previous())
                ->withInput(Arr::except($request->input(), $this->dontFlash))
                ->withErrors($exception->errors(), $exception->errorBag);
}

为给定的异常准备 JSON 响应。

/**
 * 为特定的异常准备 JSON 响应。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Exception $e
 * @return \Illuminate\Http\JsonResponse
 */
protected function prepareJsonResponse($request, Exception $e)
{
    //TODO 这里可以处理更多的异常并返回 JSON 响应
    return new JsonResponse(
        $this->convertExceptionToArray($e),
        $this->isHttpException($e) ? $e->getStatusCode() : 500,
        $this->isHttpException($e) ? $e->getHeaders() : [],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
    );
}

/**
 * 将给定的异常转换为数组。
 *
 * @param  \Exception  $e
 * @return array
 */
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',
    ];
}

准备给定异常的响应。

/**
 * 准备给定异常的响应。
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Exception $e
 * @return \Symfony\Component\HttpFoundation\Response
 */
protected function prepareResponse($request, Exception $e)
{
    //TODO 这里可以处理更多的异常并返回响应
    if (! $this->isHttpException($e) && config('app.debug')) {
        return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
    }

    if (! $this->isHttpException($e)) {
        $e = new HttpException(500, $e->getMessage());
    }

    return $this->toIlluminateResponse(
        $this->renderHttpException($e), $e
    );
}

注意

为了保持 App\Exceptions\Handlerrender 方法的洁净,不应该在该方法中进行太多的 instanceof 检查,这样当异常类多的时候就显得臃肿,自定义的异常类应该在类中定义 render 方法。

参考

本文为 Wiki 文章,邀您参与纠错、纰漏和优化
讨论数量: 1

:pensive:怎么感觉还是没理解

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!