错误处理
简介
当你创建一个新的 Laravel 项目时,错误和异常处理已经为你配置好了;不过,你可以随时在应用程序的 bootstrap/app.php 文件中使用 withExceptions 方法来管理应用程序如何报告和渲染异常。
提供给 withExceptions 闭包的 $exceptions 对象是 Illuminate\Foundation\Configuration\Exceptions 的一个实例,负责管理应用程序中的异常处理。我们将在本文档中深入探讨该对象。
配置
config/app.php 配置文件中的 debug 选项决定了向用户显示多少关于错误的信息。默认情况下,此选项设置为遵循 APP_DEBUG 环境变量的值,该变量存储在你的 .env 文件中。
在本地开发过程中,你应该将 APP_DEBUG 环境变量设置为 true。
在生产环境中,APP_DEBUG 的值应始终为 false。如果在生产环境中将其设置为 true,你可能会面临将敏感配置值暴露给应用程序最终用户的风险。
处理异常
报告异常
在 Laravel 中,异常报告用于记录异常或将其发送到外部服务,如 Sentry 或 Flare。默认情况下,异常将根据你的 日志 配置进行记录。不过,你可以随心所欲地记录异常。
如果你需要以不同方式报告不同类型的异常,可以在应用程序的 bootstrap/app.php 中使用 report 异常方法来注册一个闭包,当需要报告特定类型的异常时,该闭包将被执行。Laravel 将通过检查闭包的类型提示来确定闭包要报告的异常类型。
1use App\Exceptions\InvalidOrderException;2 3->withExceptions(function (Exceptions $exceptions): void {4 $exceptions->report(function (InvalidOrderException $e) {5 // ...6 });7})
当你使用 report 方法注册自定义异常报告回调时,Laravel 仍会使用应用程序的默认日志配置记录该异常。如果你希望停止将异常传播到默认日志堆栈,可以在定义报告回调时使用 stop 方法,或者从回调中返回 false。
1use App\Exceptions\InvalidOrderException; 2 3->withExceptions(function (Exceptions $exceptions): void { 4 $exceptions->report(function (InvalidOrderException $e) { 5 // ... 6 })->stop(); 7 8 $exceptions->report(function (InvalidOrderException $e) { 9 return false;10 });11})
若要自定义特定异常的报告方式,你还可以利用 可报告异常 (reportable exceptions)。
全局日志上下文
如果可用,Laravel 会自动将当前用户的 ID 作为上下文数据添加到每个异常的日志消息中。你可以使用应用程序 bootstrap/app.php 文件中的 context 异常方法来定义自己的全局上下文数据。此信息将包含在应用程序写入的每个异常日志消息中。
1->withExceptions(function (Exceptions $exceptions): void {2 $exceptions->context(fn () => [3 'foo' => 'bar',4 ]);5})
异常日志上下文
虽然向每条日志消息添加上下文很有用,但有时某个特定的异常可能具有你希望包含在日志中的独特上下文。通过在应用程序的异常类中定义 context 方法,你可以指定与该异常相关的任何数据,这些数据将被添加到该异常的日志条目中。
1<?php 2 3namespace App\Exceptions; 4 5use Exception; 6 7class InvalidOrderException extends Exception 8{ 9 // ...10 11 /**12 * Get the exception's context information.13 *14 * @return array<string, mixed>15 */16 public function context(): array17 {18 return ['order_id' => $this->orderId];19 }20}
report 辅助函数
有时你可能需要报告一个异常,但仍要继续处理当前的请求。report 辅助函数允许你快速报告异常,而无需向用户渲染错误页面。
1public function isValid(string $value): bool 2{ 3 try { 4 // Validate the value... 5 } catch (Throwable $e) { 6 report($e); 7 8 return false; 9 }10}
对报告的异常进行去重
如果你在整个应用程序中频繁使用 report 函数,有时可能会多次报告同一个异常,从而在日志中创建重复条目。
如果你想确保同一个异常实例只被报告一次,可以在应用程序的 bootstrap/app.php 文件中调用 dontReportDuplicates 异常方法。
1->withExceptions(function (Exceptions $exceptions): void {2 $exceptions->dontReportDuplicates();3})
现在,当使用同一个异常实例调用 report 辅助函数时,只有第一次调用会被报告。
1$original = new RuntimeException('Whoops!'); 2 3report($original); // reported 4 5try { 6 throw $original; 7} catch (Throwable $caught) { 8 report($caught); // ignored 9}10 11report($original); // ignored12report($caught); // ignored
异常日志级别
当消息被写入应用程序的 日志 时,消息会以指定的 日志级别 写入,这表明了所记录消息的严重性或重要性。
如上所述,即使你使用 report 方法注册了自定义异常报告回调,Laravel 仍会使用应用程序的默认日志配置记录该异常;然而,由于日志级别有时会影响消息记录所在的频道,你可能希望配置特定异常记录时的日志级别。
为此,你可以在应用程序的 bootstrap/app.php 文件中使用 level 异常方法。此方法接收异常类型作为第一个参数,日志级别作为第二个参数。
1use PDOException;2use Psr\Log\LogLevel;3 4->withExceptions(function (Exceptions $exceptions): void {5 $exceptions->level(PDOException::class, LogLevel::CRITICAL);6})
忽略特定类型的异常
在构建应用程序时,有些类型的异常你可能永远不想报告。要忽略这些异常,可以在应用程序的 bootstrap/app.php 文件中使用 dontReport 异常方法。提供给此方法的任何类都将永远不会被报告;不过,它们仍然可以拥有自定义的渲染逻辑。
1use App\Exceptions\InvalidOrderException;2 3->withExceptions(function (Exceptions $exceptions): void {4 $exceptions->dontReport([5 InvalidOrderException::class,6 ]);7})
或者,你可以简单地使用 Illuminate\Contracts\Debug\ShouldntReport 接口来“标记”一个异常类。当一个异常被标记为该接口时,它将永远不会被 Laravel 的异常处理器报告。
1<?php 2 3namespace App\Exceptions; 4 5use Exception; 6use Illuminate\Contracts\Debug\ShouldntReport; 7 8class PodcastProcessingException extends Exception implements ShouldntReport 9{10 //11}
如果你需要更精细地控制何时忽略特定类型的异常,可以为 dontReportWhen 方法提供一个闭包。
1use App\Exceptions\InvalidOrderException;2use Throwable;3 4->withExceptions(function (Exceptions $exceptions): void {5 $exceptions->dontReportWhen(function (Throwable $e) {6 return $e instanceof PodcastProcessingException &&7 $e->reason() === 'Subscription expired';8 });9})
在内部,Laravel 已经为你忽略了一些类型的错误,例如由 404 HTTP 错误、源不匹配导致的 403 HTTP 响应或无效 CSRF 令牌导致的 419 HTTP 响应产生的异常。如果你想指示 Laravel 停止忽略某种特定类型的异常,可以使用应用程序 bootstrap/app.php 文件中的 stopIgnoring 异常方法。
1use Symfony\Component\HttpKernel\Exception\HttpException;2 3->withExceptions(function (Exceptions $exceptions): void {4 $exceptions->stopIgnoring(HttpException::class);5})
渲染异常
默认情况下,Laravel 异常处理器会将异常转换为 HTTP 响应。不过,你可以自由地为特定类型的异常注册自定义渲染闭包。你可以通过在应用程序的 bootstrap/app.php 文件中使用 render 异常方法来实现。
传递给 render 方法的闭包应该返回一个 Illuminate\Http\Response 实例,该实例可以通过 response 辅助函数生成。Laravel 将通过检查闭包的类型提示来确定闭包要渲染的异常类型。
1use App\Exceptions\InvalidOrderException;2use Illuminate\Http\Request;3 4->withExceptions(function (Exceptions $exceptions): void {5 $exceptions->render(function (InvalidOrderException $e, Request $request) {6 return response()->view('errors.invalid-order', status: 500);7 });8})
你还可以使用 render 方法来覆盖内置 Laravel 或 Symfony 异常(如 NotFoundHttpException)的渲染行为。如果传递给 render 方法的闭包没有返回值,将使用 Laravel 的默认异常渲染。
1use Illuminate\Http\Request; 2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 3 4->withExceptions(function (Exceptions $exceptions): void { 5 $exceptions->render(function (NotFoundHttpException $e, Request $request) { 6 if ($request->is('api/*')) { 7 return response()->json([ 8 'message' => 'Record not found.' 9 ], 404);10 }11 });12})
将异常渲染为 JSON
在渲染异常时,Laravel 会根据请求的 Accept 头自动确定是否应将异常渲染为 HTML 或 JSON 响应。如果你想自定义 Laravel 判断渲染 HTML 还是 JSON 异常响应的方式,可以利用 shouldRenderJsonWhen 方法。
1use Illuminate\Http\Request; 2use Throwable; 3 4->withExceptions(function (Exceptions $exceptions): void { 5 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { 6 if ($request->is('admin/*')) { 7 return true; 8 } 9 10 return $request->expectsJson();11 });12})
自定义异常响应
在极少数情况下,你可能需要自定义 Laravel 异常处理器渲染的整个 HTTP 响应。为此,你可以使用 respond 方法注册一个响应自定义闭包。
1use Symfony\Component\HttpFoundation\Response; 2 3->withExceptions(function (Exceptions $exceptions): void { 4 $exceptions->respond(function (Response $response) { 5 if ($response->getStatusCode() === 419) { 6 return back()->with([ 7 'message' => 'The page expired, please try again.', 8 ]); 9 }10 11 return $response;12 });13})
可报告与可渲染的异常
除了在应用程序的 bootstrap/app.php 文件中定义自定义报告和渲染行为外,你也可以直接在应用程序的异常类中定义 report 和 render 方法。当这些方法存在时,它们将被框架自动调用。
1<?php 2 3namespace App\Exceptions; 4 5use Exception; 6use Illuminate\Http\Request; 7use Illuminate\Http\Response; 8 9class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }18 19 /**20 * Render the exception as an HTTP response.21 */22 public function render(Request $request): Response23 {24 return response(/* ... */);25 }26}
如果你的异常扩展了一个已经是可渲染的异常(例如内置的 Laravel 或 Symfony 异常),你可以从异常的 render 方法中返回 false,以渲染该异常的默认 HTTP 响应。
1/** 2 * Render the exception as an HTTP response. 3 */ 4public function render(Request $request): Response|bool 5{ 6 if (/** Determine if the exception needs custom rendering */) { 7 8 return response(/* ... */); 9 }10 11 return false;12}
如果你的异常包含仅在满足特定条件时才需要的自定义报告逻辑,你可能需要指示 Laravel 在某些情况下使用默认的异常处理配置来报告该异常。为此,你可以从异常的 report 方法中返回 false。
1/** 2 * Report the exception. 3 */ 4public function report(): bool 5{ 6 if (/** Determine if the exception needs custom reporting */) { 7 8 // ... 9 10 return true;11 }12 13 return false;14}
你可以对 report 方法所需的任何依赖项进行类型提示,它们将由 Laravel 的 服务容器 自动注入。
异常报告限流
如果你的应用程序报告了大量的异常,你可能希望对实际记录或发送到应用程序外部错误跟踪服务的异常数量进行限流。
要对异常进行随机采样,可以使用应用程序 bootstrap/app.php 文件中的 throttle 异常方法。throttle 方法接收一个闭包,该闭包应返回一个 Lottery 实例。
1use Illuminate\Support\Lottery;2use Throwable;3 4->withExceptions(function (Exceptions $exceptions): void {5 $exceptions->throttle(function (Throwable $e) {6 return Lottery::odds(1, 1000);7 });8})
也可以根据异常类型进行条件采样。如果你只想对特定的异常类实例进行采样,可以只为该类返回 Lottery 实例。
1use App\Exceptions\ApiMonitoringException; 2use Illuminate\Support\Lottery; 3use Throwable; 4 5->withExceptions(function (Exceptions $exceptions): void { 6 $exceptions->throttle(function (Throwable $e) { 7 if ($e instanceof ApiMonitoringException) { 8 return Lottery::odds(1, 1000); 9 }10 });11})
你还可以通过返回 Limit 实例而不是 Lottery 来对记录或发送到外部错误跟踪服务的异常进行速率限制。如果你想防止突发的异常浪涌淹没你的日志(例如,当应用程序使用的第三方服务宕机时),这非常有用。
1use Illuminate\Broadcasting\BroadcastException; 2use Illuminate\Cache\RateLimiting\Limit; 3use Throwable; 4 5->withExceptions(function (Exceptions $exceptions): void { 6 $exceptions->throttle(function (Throwable $e) { 7 if ($e instanceof BroadcastException) { 8 return Limit::perMinute(300); 9 }10 });11})
默认情况下,限制将使用异常的类作为速率限制键。你可以通过在 Limit 上使用 by 方法指定自己的键来自定义此行为。
1use Illuminate\Broadcasting\BroadcastException; 2use Illuminate\Cache\RateLimiting\Limit; 3use Throwable; 4 5->withExceptions(function (Exceptions $exceptions): void { 6 $exceptions->throttle(function (Throwable $e) { 7 if ($e instanceof BroadcastException) { 8 return Limit::perMinute(300)->by($e->getMessage()); 9 }10 });11})
当然,你可以为不同的异常返回 Lottery 和 Limit 实例的混合组合。
1use App\Exceptions\ApiMonitoringException; 2use Illuminate\Broadcasting\BroadcastException; 3use Illuminate\Cache\RateLimiting\Limit; 4use Illuminate\Support\Lottery; 5use Throwable; 6 7->withExceptions(function (Exceptions $exceptions): void { 8 $exceptions->throttle(function (Throwable $e) { 9 return match (true) {10 $e instanceof BroadcastException => Limit::perMinute(300),11 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),12 default => Limit::none(),13 };14 });15})
HTTP 异常
有些异常描述了来自服务器的 HTTP 错误代码。例如,“页面未找到”错误 (404)、“未经授权错误” (401),甚至是开发者生成的 500 错误。为了在应用程序的任何位置生成此类响应,你可以使用 abort 辅助函数。
1abort(404);
自定义 HTTP 错误页面
Laravel 可以轻松地为各种 HTTP 状态码显示自定义错误页面。例如,要自定义 404 HTTP 状态码的错误页面,请创建一个 resources/views/errors/404.blade.php 视图模板。此视图将为应用程序生成的所有 404 错误进行渲染。该目录中的视图应以对应的 HTTP 状态码命名。由 abort 函数引发的 Symfony\Component\HttpKernel\Exception\HttpException 实例将作为 $exception 变量传递给视图。
1<h2>{{ $exception->getMessage() }}</h2>
你可以使用 vendor:publish Artisan 命令发布 Laravel 的默认错误页面模板。一旦模板发布,你就可以根据自己的喜好对其进行自定义。
1php artisan vendor:publish --tag=laravel-errors
备用 HTTP 错误页面
你还可以为一系列特定的 HTTP 状态码定义“备用”错误页面。如果发生的特定 HTTP 状态码没有对应的页面,将渲染此页面。为此,请在应用程序的 resources/views/errors 目录中定义 4xx.blade.php 模板和 5xx.blade.php 模板。
在定义备用错误页面时,备用页面不会影响 404、500 和 503 错误响应,因为 Laravel 针对这些状态码有内部专用页面。要自定义这些状态码的渲染页面,你应该为它们中的每一个单独定义自定义错误页面。