错误处理
简介
当你创建新的 Laravel 项目时,错误与异常处理已经为你配置好;不过,随时可在应用的 bootstrap/app.php 中使用 withExceptions 方法,管理应用如何报告与渲染异常。
传给 withExceptions 闭包的 $exceptions 对象是 Illuminate\Foundation\Configuration\Exceptions 的实例,负责管理应用中的异常处理。本文将深入介绍该对象。
配置
config/app.php 配置文件中的 debug 选项决定了向用户实际显示多少错误信息。默认情况下,该选项会遵循存储在 .env 文件中的 APP_DEBUG 环境变量的值。
在本地开发期间,应将 APP_DEBUG 环境变量设为 true。
WARNING
在生产环境中,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;
});
})INFO
要自定义给定异常的报告方式,也可使用可报告异常。
全局日志上下文
若可用,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
{
// ...
/**
* Get the exception's context information.
*
* @return array<string, mixed>
*/
public function context(): array
{
return ['order_id' => $this->orderId];
}
}report 辅助函数
有时你可能需要报告异常,但继续处理当前请求。report 辅助函数可让你快速报告异常,而无需向用户渲染错误页:
public function isValid(string $value): bool
{
try {
// Validate the value...
} 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); // reported
try {
throw $original;
} catch (Throwable $caught) {
report($caught); // ignored
}
report($original); // ignored
report($caught); // ignored异常日志级别
如上所述,即使使用 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
{
/**
* Report the exception.
*/
public function report(): void
{
// ...
}
/**
* Render the exception as an HTTP response.
*/
public function render(Request $request): Response
{
return response(/* ... */);
}
}若你的异常扩展了已经可渲染的异常(例如内置的 Laravel 或 Symfony 异常),可从异常的 render 方法返回 false,以渲染该异常的默认 HTTP 响应:
/**
* Render the exception as 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;
}INFO
可为 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 模板。
定义回退错误页时,回退页不会影响 404、500 与 503 错误响应,因为 Laravel 对这些状态码有内部专用页面。要自定义这些状态码渲染的页面,应为它们分别定义自定义错误页。