错误处理

未匹配的标注
本文档最新版为 11.x,旧版本可能放弃维护,推荐阅读最新版!

错误处理

简介

当你启动一个新的Laravel项目时,错误和异常处理已经为你配置好了;然而,在任何时候,你都可以在应用程序的 bootstrap/app.php 中使用 withExceptions 方法来管理应用程序如何报告和呈现异常。

传递给 withExceptions 闭包的 $exceptions 对象是 Illuminate\Foundation\Configuration\Exceptions 的一个实例,负责管理应用程序中的异常处理。在本文档中,我们将更深入地探讨这个对象。

配置

在你的config/app.php配置文件中,debug选项决定了向用户实际显示多少有关错误的信息。默认情况下,此选项设置为遵循存储在你的.env文件中的APP_DEBUG环境变量的值。

在本地开发过程中,你应将 APP_DEBUG 环境变量设置为 true。`在生产环境中,该值应始终设为 false。如果在生产环境中将该值设为 true,则可能会将敏感的配置值暴露给应用程序的最终用户。

处理异常

异常报告

在Laravel中,异常报告用于记录异常或将其发送到诸如 Sentry or Flare等外部服务。默认情况下,异常将根据您的 日志记录 配置进行记录。不过,您依然可以按照自己的意愿自由记录异常。

如果你需要以不同方式报告不同类型的异常,可以在应用程序的bootstrap/app.php中使用report异常方法来注册一个闭包,当需要报告特定类型的异常时,该闭包将会被执行。Laravel 将通过检查闭包的类型提示来确定闭包报告的异常类型:

use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    });
})

当你使用 report 方法注册自定义异常报告回调时,Laravel 仍会使用应用程序的默认日志配置记录异常。如果你希望阻止异常传播到默认日志堆栈,可以在定义报告回调时使用 stop 方法,或者从回调中返回 false

use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();

    $exceptions->report(function (InvalidOrderException $e) {
        return false;
    });
})

[!NOTE]
要为给定的异常自定义异常报告,你还可以使用可报告异常。 reportable exceptions.

全局日志上下文

如果可用,Laravel 会自动将当前用户的 ID 作为上下文数据添加到每个异常的日志消息中。你可以在应用程序的 bootstrap/app.php 文件中使用 context 异常方法定义自己的全局上下文数据。这些信息将包含在应用程序写入的每个异常日志消息中:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->context(fn () => [
        'foo' => 'bar',
    ]);
})

异常日志上下文

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

<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    // ...

    /**
     * 获取异常上下文信息
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

report 辅助函数

有时你可能需要报告一个异常,但继续处理当前请求。report 辅助函数允许你快速报告一个异常而不向用户渲染错误页面:

public function isValid(string $value): bool
{
    try {
        // 验证这个值...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}

去重复报告的异常

如果你在应用程序中到处使用 report 函数,你可能会偶尔多次报告相同的异常,导致日志中出现重复条目。

如果你想确保单个实例的异常只被报告一次,你可以在应用程序的 bootstrap/app.php 文件中调用 dontReportDuplicates 异常方法:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->dontReportDuplicates();
})

现在,当使用同一实例的异常调用 report 帮助器时,只有第一次调用会被报告:

$original = new RuntimeException('Whoops!');

report($original); // 报告了

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); //  忽略了
}

report($original); //  忽略了
report($caught); //  忽略了

异常日志级别

当信息被写入应用的日志时,它会使用指定的日志级别,该级别表示消息的重要性或严重性。

即使你使用 report 方法注册了自定义异常上报回调,Laravel 仍会使用应用的默认日志配置记录异常。不过,有时日志级别会影响消息记录到哪些通道,因此你可能希望为特定类型的异常配置日志级别。

可以在应用的 bootstrap/app.php 文件中使用 level 方法来实现。此方法接收异常类型作为第一个参数,日志级别作为第二个参数:

use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})

按类型忽略异常

在应用中,某些类型的异常你可能永远不想上报。你可以在 bootstrap/app.php 文件中使用 dontReport 方法指定这些异常类。被指定的异常不会上报,但仍可以自定义渲染逻辑:

use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})

另一种方式是让异常类实现 Illuminate\Contracts\Debug\ShouldntReport 接口。当异常实现此接口时,Laravel 的异常处理器不会上报它:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

Laravel 内部默认会忽略某些类型的异常,例如 404 HTTP 错误或由无效 CSRF 令牌触发的 419 HTTP 响应。如果你希望 Laravel 停止忽略某种异常类型,可以在 bootstrap/app.php 文件中使用 stopIgnoring 方法:

use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->stopIgnoring(HttpException::class);
})

异常渲染

默认情况下,Laravel 的异常处理器会将异常转换为 HTTP 响应。但你也可以为特定类型的异常注册自定义渲染闭包。可以在 bootstrap/app.php 文件中使用 render 方法实现。

传递给 render 方法的闭包应返回一个 Illuminate\Http\Response 实例,可以通过 response 辅助函数生成。Laravel 会根据闭包的类型提示确定要处理的异常类型:

use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})

你也可以使用 render 方法覆盖 Laravel 或 Symfony 内置异常的渲染行为,例如 NotFoundHttpException。如果闭包未返回值,则会使用 Laravel 的默认异常渲染:

use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
})

将异常渲染为 JSON

Laravel 在渲染异常时,会根据请求的 Accept 头自动判断是返回 HTML 还是 JSON 响应。如果你想自定义 Laravel 如何判断渲染 HTML 或 JSON 异常响应,可以使用 shouldRenderJsonWhen 方法:

use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true;
        }

        return $request->expectsJson();
    });
})

自定义异常响应

在少数情况下,你可能需要完全自定义 Laravel 异常处理器返回的 HTTP 响应。可以使用 respond 方法注册一个响应自定义闭包:

use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => 'The page expired, please try again.',
            ]);
        }

        return $response;
    });
})

可报告和可渲染异常

你也可以直接在自定义异常类中定义 reportrender 方法,而无需在 bootstrap/app.php 中进行注册。当这些方法存在时,框架会自动调用:

<?php

namespace App\Exceptions;

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

class InvalidOrderException extends Exception
{
    /**
     * 上报异常。
     */
    public function report(): void
    {
        // ...
    }

    /**
     * 将异常渲染为 HTTP 响应。
     */
    public function render(Request $request): Response
    {
        return response(/* ... */);
    }
}

如果你的异常继承了已经可渲染的异常(例如 Laravel 或 Symfony 内置异常),可以在 render 方法中返回 false,以使用默认的 HTTP 响应:

/**
 * 将异常渲染为 HTTP 响应。
 */
public function render(Request $request): Response|bool
{
    if (/** 判断异常是否需要自定义渲染 */) {

        return response(/* ... */);
    }

    return false;
}

如果你的异常只在特定条件下才需要自定义上报逻辑,你可以在异常类的 report 方法中返回 false,让 Laravel 使用默认的异常处理配置:

/**
 * 上报异常。
 */
public function report(): bool
{
    if (/** 判断异常是否需要自定义上报 */) {
        // 执行自定义上报逻辑...
        return true;
    }

    return false;
}

[!注意]
你可以为 report 方法声明任何依赖,Laravel 的服务容器会自动注入它们。

限流上报的异常

如果你的应用程序上报的异常数量非常大,你可能希望限制实际被记录或发送到外部错误追踪服务的异常数量。

要对异常进行随机采样,你可以在应用程序的 bootstrap/app.php 文件中使用 throttle 异常方法。throttle 方法接收一个闭包,该闭包应返回一个 Lottery 实例:

use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})

也可以基于异常类型有条件地进行采样。如果你只想对某个特定异常类进行采样,可以仅为该类返回 Lottery 实例:

use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof ApiMonitoringException) {
            return Lottery::odds(1, 1000);
        }
    });
})

你也可以通过返回 Limit 实例而不是 Lottery 来对记录或发送到外部错误追踪服务的异常进行速率限制。当你想防止异常突然激增淹没日志时,这非常有用,例如当应用依赖的第三方服务出现故障时。

use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})

默认情况下,限制将使用异常的类作为速率限制的键。你可以通过在 Limit 上使用 by 方法指定自定义键来修改这一行为:

use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300)->by($e->getMessage());
        }
    });
})

当然,你可以针对不同的异常返回 LotteryLimit 的混合实例

use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        return match (true) {
            $e instanceof BroadcastException => Limit::perMinute(300),
            $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
            default => Limit::none(),
        };
    });
})

HTTP 异常

有些异常表示来自服务器的 HTTP 错误码。例如,这可能是“页面未找到”错误(404)、“未授权”错误(401),甚至是开发者生成的 500 错误。为了在应用的任何地方生成这样的响应,你可以使用 abort 辅助函数

abort(404);

自定义 HTTP 错误页面

Laravel 使得为不同的 HTTP 状态码显示自定义错误页面变得简单。例如,要自定义 404 HTTP 状态码的错误页面,请创建一个 resources/views/errors/404.blade.php 视图模板。该视图将用于应用生成的所有 404 错误。该目录下的视图应以对应的 HTTP 状态码命名。由 abort 函数触发的 Symfony\Component\HttpKernel\Exception\HttpException 实例会作为 $exception 变量传递给视图:

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

你可以使用 vendor:publish Artisan 命令发布 Laravel 默认的错误页面模板。一旦模板被发布,你就可以根据需要自定义它们:

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

回退 HTTP 错误页面

你还可以为某一系列 HTTP 状态码定义“回退”错误页面。如果发生的具体 HTTP 状态码没有对应页面,就会渲染回退页面。为此,你可以在应用的 resources/views/errors 目录下定义 4xx.blade.php5xx.blade.php 模板。

在定义回退错误页面时,回退页面不会影响 404500503 错误响应,因为 Laravel 对这些状态码有内部专用页面。要自定义这些状态码的页面,需要为每个状态码单独定义自定义错误页面。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/12.x/er...

译文地址:https://learnku.com/docs/laravel/12.x/er...

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
贡献者:4