错误处理


简介

Laravel 默认已经为我们配置好了错误和异常处理,我们在 App\Exceptions\Handler 类中触发异常并将响应返回给用户。在本文档中我们将深入探讨这个类。

配置

配置文件 config/app.php 中的 debug 配置项控制浏览器显示的错误信息数量。默认情况下,该配置项通过 .env 文件中的环境变量 APP_DEBUG 进行设置。

对本地开发而言,你应该设置环境变量 APP_DEBUG 值为 true。在生产环境,该值应该被设置为 false。如果在生产环境被设置为 true,就有可能将一些敏感的配置值暴露给终端用户。

异常处理器

报告异常

所有异常都由 App\Exceptions\Handler 类处理。这个类包含了一个 register 方法用于注册自定义的异常报告器和渲染器回调,接下来我们会详细介绍这些概念。我们可以通过异常报告记录异常或者将它们发送给外部服务,比如 FlareBugsnag 以及 Sentry。默认情况下,会基于日志配置记录异常,不过,你也可以按照自己期望的方式进行自定义。

例如,如果你需要以不同方式报告不同类型的异常,可以使用 reportable 方法注册一个闭包,该闭包会在给定类型异常需要被报告时执行。Laravel 会通过检查闭包的参数类型提示推断该闭包报告的异常类型:

use App\Exceptions\CustomException;

/**
 * Register the exception handling callbacks for the application.
 */
public function register()
{
    $this->reportable(function (CustomException $e) {
        //
    });
}

当你使用 reportable 方法注册自定义异常报告回调时,Laravel 仍然会使用应用默认的日志配置记录异常。如果你想要停止异常传播到默认的日志栈(不记录到日志系统),可以 在定义异常回调时使用 stop 方法或者从异常回调函数返回 false

$this->reportable(function (InvalidOrderException $e) {
    // ...
})->stop();
 
$this->reportable(function (InvalidOrderException $e) {
    return false;
});

可以考虑使用可报告的异常来取代在 reports 方法中进行大量的 instanceof 检查。

全局日志上下文

如果当前用户 ID 可用的话,Laravel 会自动将其添加到每一条异常日志信息中作为上下文数据。你也可以通过重写 App\Exceptions\Handler 类的 context 方法来定义自己的全局上下文数据,该信息会被包含在应用写入的每一条异常日志信息中:

/**
 * Get the default context variables for logging.
 */
protected function context()
{
    return array_merge(parent::context(), [
        'foo' => 'bar',
    ]);
}

异常日志上下文

在每个日志消息中添加上下文信息很有用,但有时特定的异常可能具有独特的上下文,您希望在日志中包含这些上下文。通过在应用程序的自定义异常上定义一个 context 方法,您可以指定与该异常相关的任何数据,这些数据应添加到异常的日志条目中:

<?php
 
namespace App\Exceptions;
 
use Exception;
 
class InvalidOrderException extends Exception
{
    // ...
 
    /**
     * Get the exception's context information.
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

report 辅助函数

有时候你可能需要报告一个异常并继续处理当前请求。辅助函数 report 允许你使用异常处理器的 report 方法快速报告一个异常而不会渲染错误页:

public function isValid($value)
{
    try {
        // Validate the value...
    } catch (Throwable $e) {
        report($e);
    
        return false;
    }
}

异常日志级别

当日志消息被写入你的应用程序的日志文件中时,这些消息会被写入指定的日志级别,这指示了被记录的消息的严重性或重要性。

正如上面所提到的,即使你使用 reportable 方法注册了自定义异常报告回调,Laravel 仍然会使用应用程序的默认日志记录配置来记录异常。然而,由于日志级别有时会影响消息被记录的通道,你可能希望配置某些异常被记录的日志级别。

为了实现这一点,你可以在应用程序的异常处理器的 $levels 属性中定义一个异常类型及其关联的日志级别数组:

use PDOException;
use Psr\Log\LogLevel;
 
/**
 * A list of exception types with their corresponding custom log levels.
 *
 * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
 */
protected $levels = [
    PDOException::class => LogLevel::CRITICAL,
];

通过类型忽略异常

当构建应用程序时,可能有一些异常类型您希望忽略并且不会报告。您的应用程序异常处理程序包含一个 $dontReport 属性,该属性被初始化为空数组。您可以将要忽略报告的类添加到该属性中,这些类将不会被报告;但是,它们仍然可能具有自定义渲染逻辑:

use App\Exceptions\InvalidOrderException;
 
/**
 * A list of the exception types that are not reported.
 *
 * @var array<int, class-string<\Throwable>>
 */
protected $dontReport = [
    InvalidOrderException::class,
];

在内部,Laravel 已经为您忽略了一些类型的错误,例如由 404 HTTP 错误或由无效 CSRF 令牌生成的 419 HTTP 响应引发的异常。如果您想指示 Laravel 停止忽略某种类型的异常,可以在异常处理程序的 register 方法中调用 stopIgnoring 方法:

use Symfony\Component\HttpKernel\Exception\HttpException;
 
/**
 * Register the exception handling callbacks for the application.
 */
public function register(): void
{
    $this->stopIgnoring(HttpException::class);
 
    // ...
}

渲染异常

默认情况下,Laravel 异常处理程序会将异常转换为 HTTP 响应。但是,您可以自由地为特定类型的异常注册自定义渲染闭包。您可以在异常处理程序中调用 renderable 方法来实现此功能。

传递给 renderable 方法的闭包应该返回一个 Illuminate\Http\Response 实例,该实例可以通过 response 辅助函数生成。Laravel 将通过检查闭包的类型提示来推断闭包渲染的异常类型:

use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;
 
/**
 * Register the exception handling callbacks for the application.
 */
public function register(): void
{
    $this->renderable(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', [], 500);
    });
}

您还可以使用 renderable 方法来覆盖 Laravel 或 Symfony 内置异常(例如 NotFoundHttpException)的渲染行为。如果传递给 renderable 方法的闭包没有返回值,将使用 Laravel 的默认异常渲染:

use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
/**
 * Register the exception handling callbacks for the application.
 */
public function register(): void
{
    $this->renderable(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
}

可报告 & 可渲染的异常

除了在异常处理器的 reportrender 方法中进行异常类型检查外,还可以在自定义异常中直接定义 reportrender 方法。当异常中存在这些方法时,框架会自动调用它们:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class RenderException extends Exception
{
    /**
     * Report the exception.
     */
    public function report(): void
    {
        //
    }

    /**
     * Render the exception into an HTTP response.
     */
    public function render(Request $request): Response
    {
        return response(...);
    }
}

如果您的异常扩展了已经可以渲染的异常,例如内置的 Laravel 或 Symfony 异常,您可以在异常的 render 方法中返回 false,以渲染异常的默认 HTTP 响应:

/**
 * Render the exception into an HTTP response.
 */
public function render(Request $request): Response|bool
{
    if (/** Determine if the exception needs custom rendering */) {
 
        return response(/* ... */);
    }
 
    return false;
}

如果您的异常包含了仅在特定条件下需要使用的自定义报告逻辑,可能需要指示 Laravel 在某些情况下使用默认异常处理配置报告异常。为了实现这一点,您可以在异常的 report 方法中返回 false

/**
 * Report the exception.
 */
public function report(): bool
{
    if (/** Determine if the exception needs custom reporting */) {
 
        // ...
 
        return true;
    }
 
    return false;
}

注:你可以在 report 方法中注入任何需要的依赖,它们会通过 Laravel 服务容器自动解析。

HTTP 异常

有些异常描述来自服务器的 HTTP 错误码,例如,这可能是一个“页面未找到”错误(404),“认证失败错误”(401)亦或是程序出错造成的 500 错误,为了在应用中生成这样的响应,可以使用 abort 辅助函数:

abort(404);

abort 辅助函数会立即引发一个会被异常处理器渲染的异常,此外,你还可以像这样提供响应描述:

abort(403, '未授权操作');

该方法可在请求生命周期的任何时间点使用。

自定义 HTTP 错误页面

在 Laravel 中,返回不同 HTTP 状态码的错误页面很简单,例如,如果你想要自定义 404 错误页面,创建一个 resources/views/errors/404.blade.php 文件,该视图文件用于渲染程序返回的所有 404 错误。需要注意的是,该目录下的视图命名应该和相应的 HTTP 状态码相匹配。abort 函数触发的 HttpException 异常会以 $exception 变量的方式传递给视图:

<h2>{{ $exception->getMessage() }}</h2>

你可以使用 Artisan 命令 vendor:publish 发布 Laravel 的错误页面模板:

php artisan vendor:publish --tag=laravel-errors

模板被发布后,就可以按照自己的喜好对其进行自定义。

兜底 HTTP 错误页面

您还可以为给定的一系列 HTTP 状态码定义一个“fallback”错误页面。如果没有针对特定 HTTP 状态码的对应页面,将呈现此页面。为了实现这一点,可以在您的应用程序的 resources/views/errors 目录中定义一个 4xx.blade.php 模板和一个 5xx.blade.php 模板。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 表单验证

>> 下一篇: 日志