简介
当你启动一个新的 Laravel 项目时,错误与异常处理已经为你配置好了;然而,在任何时刻,你可以在应用程序的 bootstrap/app.php 中使用 withExceptions 方法,来管理应用程序对异常的上报与渲染方式。
提供给 withExceptions 闭包的 $exceptions 对象是 Illuminate\Foundation\Configuration\Exceptions 的实例,负责管理应用程序中的异常处理。在本文档中,我们将深入探讨该对象。
配置
config/app.php 配置文件中的 debug 选项决定了实际向用户显示的错误信息量。默认情况下,该选项会遵循存储在 .env 文件中的 APP_DEBUG 环境变量的值。
在本地开发过程中,你应该将 APP_DEBUG 环境变量设置为 true。
在生产环境中,APP_DEBUG 的值应始终为 false。如果在生产环境中将其设置为 true,会有将敏感配置值暴露给应用程序终端用户的风险。
异常处理
异常上报
在 Laravel 中,异常上报用于记录异常,或者将异常发送到外部服务,例如 Laravel Nightwatch、Sentry 或 Flare。默认情况下,异常会根据你的 日志记录 配置进行记录。不过,你可以按需自行记录异常。
如果需要对不同类型的异常采用不同的上报方式,你可以在应用程序的 bootstrap/app.php 中使用 report 异常方法,注册一个当需要上报特定类型的异常时执行的闭包。Laravel 会通过检查闭包的类型提示来确定该闭包上报的异常类型:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (InvalidOrderException $e) {
// ...
});
})当使用 report 方法注册自定义异常上报回调时,Laravel 仍会使用应用程序的默认日志配置记录该异常。如果你希望阻止异常传播到默认的日志堆栈,可以在定义上报回调时使用 stop 方法,或者在回调中返回 false:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (InvalidOrderException $e) {
// ...
})->stop();
$exceptions->report(function (InvalidOrderException $e) {
return false;
});
})若要自定义特定异常的异常上报方式,你也可以使用 可上报异常(reportable exceptions)。
全局日志上下文
如果可用,Laravel 会自动将当前用户的 ID 作为上下文数据,添加到每条异常日志消息中。你可以在应用程序的 bootstrap/app.php 文件中,使用 context 异常方法定义你自己的全局上下文数据。这些信息会被包含在你的应用程序写入的每一条异常日志消息中:
->withExceptions(function (Exceptions $exceptions): void {
$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): void {
$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): void {
$exceptions->level(PDOException::class, LogLevel::CRITICAL);
})按类型忽略异常
在构建应用程序时,有一些类型的异常你永远不想上报。若要忽略这些异常,你可以在应用程序的 bootstrap/app.php 文件中使用 dontReport 异常方法。提供给该方法的任何类都不会被上报;不过,它们仍可能具有自定义渲染逻辑:
use App\Exceptions\InvalidOrderException;
->withExceptions(function (Exceptions $exceptions): void {
$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
{
//
}如果你需要更精细地控制何时忽略特定类型的异常,可以向 dontReportWhen 方法提供一个闭包:
use App\Exceptions\InvalidOrderException;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontReportWhen(function (Throwable $e) {
return $e instanceof PodcastProcessingException &&
$e->reason() === 'Subscription expired';
});
})在内部,Laravel 已经为你忽略了一些类型的错误,例如由 404 HTTP 错误、源不匹配产生的 403 HTTP 响应或无效 CSRF 令牌产生的 419 HTTP 响应所引发的异常。如果你希望指示 Laravel 停止忽略特定类型的异常,可以在应用程序的 bootstrap/app.php 文件中使用 stopIgnoring 异常方法:
use Symfony\Component\HttpKernel\Exception\HttpException;
->withExceptions(function (Exceptions $exceptions): void {
$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): void {
$exceptions->render(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', status: 500);
});
})你也可以使用 render 方法来覆盖 Laravel 或 Symfony 内置异常(如 NotFoundHttpException)的渲染行为。如果传递给 render 方法的闭包未返回值,则会采用 Laravel 默认的异常渲染方式:
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
->withExceptions(function (Exceptions $exceptions): void {
$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): void {
$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): void {
$exceptions->respond(function (Response $response) {
if ($response->getStatusCode() === 419) {
return back()->with([
'message' => 'The page expired, please try again.',
]);
}
return $response;
});
})可上报与可渲染的异常
你不必在应用的 bootstrap/app.php 文件中定义自定义的上报与渲染行为,而是可以直接在应用的异常类中定义 report 和 render 方法。只要存在这些方法,框架就会自动调用它们:
<?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;
}如果你的异常包含仅在特定条件满足时才需要的自定义上报逻辑,你可能需要指示 Laravel 在某些时候使用默认的异常处理配置来上报该异常。要实现这一点,你可以从异常的 report 方法中返回 false:
/**
* 上报该异常。
*/
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): void {
$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): void {
$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): void {
$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): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof BroadcastException) {
return Limit::perMinute(300)->by($e->getMessage());
}
});
})当然,你也可以为不同的异常返回 Lottery 和 Limit 实例的混合形式:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$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.php 模板和 5xx.blade.php 模板。
定义备用错误页面时,备用页面不会影响 401、402、403、404、419、429、500 和 503 错误响应,因为 Laravel 针对这些状态码内置了专门的页面。若要自定义针对这些状态码渲染的页面,应分别为每个状态码单独定义自定义错误页面。