错误处理
介绍
当你启动一个新的 Laravel 项目时,错误和异常处理已经为你配置好了。App\Exceptions\Handler
类是所有应用程序抛出的异常被记录并呈现给用户的地方。我们将在本文档中更深入地探讨这个类。
配置
config/app.php
配置文件中的 debug
选项决定了向用户显示多少错误信息。默认情况下,该选项设置为遵循存储在 .env
文件中的 APP_DEBUG
环境变量的值。
在本地开发期间,你应该将 APP_DEBUG
环境变量设置为 true
。在生产环境中,这个值应该始终为 false
。如果在生产中设置为 true
,你可能会将敏感的配置值暴露给应用程序的最终用户。
异常处理器
报告异常
所有异常都由 App\Exceptions\Handler
类处理。这个类包含一个 register
方法,你可以在其中注册自定义异常报告和渲染回调。我们将详细检查这些概念。异常报告用于记录异常或将其发送到外部服务,如 Flare、Bugsnag 或 Sentry。默认情况下,异常将根据你的日志配置进行记录。不过,你可以随意记录异常。
如果你需要以不同的方式报告不同类型的异常,可以使用 reportable
方法注册一个闭包,当需要报告给定类型的异常时,该闭包将被执行。Laravel 将通过检查闭包的类型提示来确定闭包报告的异常类型:
use App\Exceptions\InvalidOrderException;
/**
* 为应用程序注册异常处理回调。
*/
public function register(): void
{
$this->reportable(function (InvalidOrderException $e) {
// ...
});
}
当你使用 reportable
方法注册自定义异常报告回调时,Laravel 仍将使用应用程序的默认日志配置记录异常。如果你希望停止异常传播到默认日志堆栈,可以在定义报告回调时使用 stop
方法或从回调中返回 false
:
$this->reportable(function (InvalidOrderException $e) {
// ...
})->stop();
$this->reportable(function (InvalidOrderException $e) {
return false;
});
要自定义给定异常的异常报告,你还可以使用可报告异常。
全局日志上下文
如果可用,Laravel 会自动将当前用户的 ID 作为上下文数据添加到每个异常的日志消息中。你可以通过在应用程序的 App\Exceptions\Handler
类上定义一个 context
方法来定义自己的全局上下文数据。此信息将包含在应用程序写入的每个异常的日志消息中:
/**
* 获取日志的默认上下文变量。
*
* @return array<string, mixed>
*/
protected function context(): array
{
return array_merge(parent::context(), [
'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
函数,可能会偶尔多次报告同一异常,从而在日志中创建重复条目。
如果你希望确保异常的单个实例仅被报告一次,可以在应用程序的 App\Exceptions\Handler
类中将 $withoutDuplicates
属性设置为 true
:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* 指示异常实例应仅报告一次。
*
* @var bool
*/
protected $withoutDuplicates = true;
// ...
}
现在,当使用同一异常实例调用 report
辅助函数时,只有第一次调用会被报告:
$original = new RuntimeException('Whoops!');
report($original); // reported
try {
throw $original;
} catch (Throwable $caught) {
report($caught); // ignored
}
report($original); // ignored
report($caught); // ignored
异常日志级别
当消息写入应用程序的日志时,消息会以指定的日志级别写入,该级别指示消息的严重性或重要性。
如上所述,即使你使用 reportable
方法注册自定义异常报告回调,Laravel 仍将使用应用程序的默认日志配置记录异常;然而,由于日志级别有时会影响消息记录的通道,你可能希望配置某些异常记录的日志级别。
为此,你可以在应用程序的异常处理程序上定义一个 $levels
属性。此属性应包含异常类型及其关联的日志级别的数组:
use PDOException;
use Psr\Log\LogLevel;
/**
* 具有相应自定义日志级别的异常类型列表。
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
PDOException::class => LogLevel::CRITICAL,
];
按类型忽略异常
在构建应用程序时,会有一些类型的异常你永远不想报告。要忽略这些异常,请在应用程序的异常处理程序上定义一个 $dontReport
属性。你添加到此属性的任何类都不会被报告;然而,它们可能仍然具有自定义渲染逻辑:
use App\Exceptions\InvalidOrderException;
/**
* 不报告的异常类型列表。
*
* @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;
/**
* 为应用程序注册异常处理回调。
*/
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;
/**
* 为应用程序注册异常处理回调。
*/
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;
/**
* 为应用程序注册异常处理回调。
*/
public function register(): void
{
$this->renderable(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
}
可报告和可渲染的异常
你可以直接在应用程序的异常上定义 report
和 render
方法,而不是在异常处理程序的 register
方法中定义自定义报告和渲染行为。当这些方法存在时,框架将自动调用它们:
<?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 的服务容器自动注入到方法中。
限制报告的异常
如果你的应用程序报告了大量异常,你可能希望限制实际记录或发送到应用程序的外部错误跟踪服务的异常数量。
要对异常进行随机采样,可以从异常处理程序的 throttle
方法返回一个 Lottery
实例。如果你的 App\Exceptions\Handler
类不包含此方法,你可以简单地将其添加到类中:
use Illuminate\Support\Lottery;
use Throwable;
/**
* 限制传入的异常。
*/
protected function throttle(Throwable $e): mixed
{
return Lottery::odds(1, 1000);
}
还可以根据异常类型有条件地采样。如果你只想对特定异常类的实例进行采样,可以仅为该类返回一个 Lottery
实例:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;
/**
* 限制传入的异常。
*/
protected function throttle(Throwable $e): mixed
{
if ($e instanceof ApiMonitoringException) {
return Lottery::odds(1, 1000);
}
}
你还可以通过返回 Limit
实例而不是 Lottery
来限制记录或发送到外部错误跟踪服务的异常。这在你想要防止突然的异常激增淹没你的日志时很有用,例如,当应用程序使用的第三方服务宕机时:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
/**
* 限制传入的异常。
*/
protected function throttle(Throwable $e): mixed
{
if ($e instanceof BroadcastException) {
return Limit::perMinute(300);
}
}
默认情况下,限制将使用异常的类作为速率限制键。你可以通过在 Limit
上使用 by
方法指定自己的键来自定义此键:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
/**
* 限制传入的异常。
*/
protected function throttle(Throwable $e): mixed
{
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;
/**
* 限制传入的异常。
*/
protected function throttle(Throwable $e): mixed
{
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
模板。