简介

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

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

配置

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

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

异常处理

报告异常

在Laravel中,异常报告用于记录异常或将其发送到外部服务 SentryFlare。 默认情况下,将根据您的 日志 配置记录异常。但是,您可以随心所欲地记录异常。

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

  1. ->withExceptions(function (Exceptions $exceptions) {
  2. $exceptions->report(function (InvalidOrderException $e) {
  3. // ...
  4. });
  5. })

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

  1. ->withExceptions(function (Exceptions $exceptions) {
  2. $exceptions->report(function (InvalidOrderException $e) {
  3. // ...
  4. })->stop();
  5. $exceptions->report(function (InvalidOrderException $e) {
  6. return false;
  7. });
  8. })


注意
要自定义给定异常的异常报告,你也可以使用 可报告异常.

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

  1. ->withExceptions(function (Exceptions $exceptions) {
  2. $exceptions->context(fn () => [
  3. 'foo' => 'bar',
  4. ]);
  5. })

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

  1. <?php
  2. namespace App\Exceptions;
  3. use Exception;
  4. class InvalidOrderException extends Exception
  5. {
  6. // ...
  7. /**
  8. * 获取异常上下文信息
  9. *
  10. * @return array<string, mixed>
  11. */
  12. public function context(): array
  13. {
  14. return ['order_id' => $this->orderId];
  15. }
  16. }

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

  1. public function isValid(string $value): bool
  2. {
  3. try {
  4. // 验证这个值...
  5. } catch (Throwable $e) {
  6. report($e);
  7. return false;
  8. }
  9. }

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

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

  1. ->withExceptions(function (Exceptions $exceptions) {
  2. $exceptions->dontReportDuplicates();
  3. })

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

  1. $original = new RuntimeException('Whoops!');
  2. report($original); // 报告了
  3. try {
  4. throw $original;
  5. } catch (Throwable $caught) {
  6. report($caught); // 忽略了
  7. }
  8. report($original); // 忽略了
  9. report($caught); // 忽略了

异常日志等级

当消息写入应用程序的日志时, 消息以指定的日志等级写入,这表示被记录的消息的严重性或重要性。

如上所述,即使你使用 php report 方法注册自定义异常报告回调,Laravel 仍将使用应用程序的默认日志配置记录异常;但是,由于日志等级有时会影响消息记录的管道,你可能希望配置某些异常在哪个日志等级下被记录。

为此,你可以使用应用程序的 php bootstrap/app.php 文件中的 php level 异常方法。此方法将异常类型作为其第一个参数,日志等级作为其第二个参数接收:

  1. use PDOException;
  2. use Psr\Log\LogLevel;
  3. ->withExceptions(function (Exceptions $exceptions) {
  4. $exceptions->level(PDOException::class, LogLevel::CRITICAL);
  5. })

按类型忽略异常

在构建应用程序时,有些类型的异常你永远不希望报告。要忽略这些异常,你可以在应用程序的 php bootstrap/app.php 文件中使用 php dontReport 异常方法。此方法提供的任何类都永远不会被报告;但是,它们仍然可以有自定义的渲染逻辑:

  1. use App\Exceptions\InvalidOrderException;
  2. ->withExceptions(function (Exceptions $exceptions) {
  3. $exceptions->dontReport([
  4. InvalidOrderException::class,
  5. ]);
  6. })

在内部,Laravel 已经为你忽略了某些类型的错误,例如由于 404 HTTP 错误导致的异常或由于无效 CSRF 令牌生成的 419 HTTP 响应。如果你想指示 Laravel 停止忽略给定类型的异常,你可以在应用程序的 php bootstrap/app.php 文件中使用 php stopIgnoring 异常方法:

  1. use Symfony\Component\HttpKernel\Exception\HttpException;
  2. ->withExceptions(function (Exceptions $exceptions) {
  3. $exceptions->stopIgnoring(HttpException::class);
  4. })

渲染异常

默认情况下,Laravel 异常处理器将异常转换为 HTTP 响应。但是,你可以为给定类型的异常注册自定义渲染闭包。你可以通过在应用程序的 php bootstrap/app.php 文件中使用 php render 异常方法来完成此操作。

传递给 php render 方法的闭包应返回 php Illuminate\Http\Response 的实例,该实例可通过 php response 辅助函数生成。Laravel 将通过检查闭包的类型提示来确定闭包渲染的异常类型:

  1. use App\Exceptions\InvalidOrderException;
  2. use Illuminate\Http\Request;
  3. ->withExceptions(function (Exceptions $exceptions) {
  4. $exceptions->render(function (InvalidOrderException $e, Request $request) {
  5. return response()->view('errors.invalid-order', [], 500);
  6. });
  7. })

你还可以使用 php render 方法覆盖 Laravel 或 Symfony 内置异常的渲染行为,如 php NotFoundHttpException。如果传递给 php render 方法的闭包不返回值,将使用 Laravel 的默认异常渲染:

  1. use Illuminate\Http\Request;
  2. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  3. ->withExceptions(function (Exceptions $exceptions) {
  4. $exceptions->render(function (NotFoundHttpException $e, Request $request) {
  5. if ($request->is('api/*')) {
  6. return response()->json([
  7. 'message' => 'Record not found.'
  8. ], 404);
  9. }
  10. });
  11. })

将异常渲染为 JSON
在渲染异常时,Laravel 会根据请求的 php Accept 头自动确定异常应渲染为 HTML 还是 JSON 响应。如果你希望自定义 Laravel 确定是否渲染 HTML 或 JSON 异常响应的方式,你可以使用 php shouldRenderJsonWhen 方法:

  1. use Illuminate\Http\Request;
  2. use Throwable;
  3. ->withExceptions(function (Exceptions $exceptions) {
  4. $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
  5. if ($request->is('admin/*')) {
  6. return true;
  7. }
  8. return $request->expectsJson();
  9. });
  10. })

自定义异常响应
在极少数情况下,你可能需要自定义 Laravel 异常处理器渲染的整个 HTTP 响应。为此,你可以使用 php respond 方法注册一个响应自定义闭包:

  1. use Symfony\Component\HttpFoundation\Response;
  2. ->withExceptions(function (Exceptions $exceptions) {
  3. $exceptions->respond(function (Response $response) {
  4. if ($response->getStatusCode() === 419) {
  5. return back()->with([
  6. 'message' => 'The page expired, please try again.',
  7. ]);
  8. }
  9. return $response;
  10. });
  11. })

可报告和可渲染的异常

而不是在应用程序的 php bootstrap/app.php 文件中定义自定义报告和渲染行为,你可以直接在应用程序的异常上定义 php reportphp render 方法。这些方法存在时,它们将自动被框架调用:

  1. <?php
  2. namespace App\Exceptions;
  3. use Exception;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Http\Response;
  6. class InvalidOrderException extends Exception
  7. {
  8. /**
  9. * 报告异常。
  10. */
  11. public function report(): void
  12. {
  13. // ...
  14. }
  15. /**
  16. * 将异常渲染成 HTTP 响应。
  17. */
  18. public function render(Request $request): Response
  19. {
  20. return response(/* ... */);
  21. }
  22. }

如果你的异常扩展了已经可渲染的异常,比如内置的 Laravel 或 Symfony 异常,你可以从异常的 php render 方法返回 php false 来渲染异常的默认 HTTP 响应:

  1. /**
  2. * 将异常渲染成 HTTP 响应。
  3. */
  4. public function render(Request $request): Response|bool
  5. {
  6. if (/** 确定异常是否需要自定义渲染 */) {
  7. return response(/* ... */);
  8. }
  9. return false;
  10. }

如果你的异常包含自定义报告逻辑,这些逻辑只在某些条件满足时才需要,你可能需要指定 Laravel 有时使用默认的异常处理配置来报告异常。为此,你可以从异常的 php report 方法返回 php false

  1. /**
  2. * 报告异常。
  3. */
  4. public function report(): bool
  5. {
  6. if (/** 确定异常是否需要自定义报告 */) {
  7. // ...
  8. return true;
  9. }
  10. return false;
  11. }


注意
你可以为 php report 方法的任何所需依赖类型提示,并且 Laravel 服务容器将自动将它们注入到方法中。

节流报告的异常

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

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

  1. use Illuminate\Support\Lottery;
  2. use Throwable;
  3. ->withExceptions(function (Exceptions $exceptions) {
  4. $exceptions->throttle(function (Throwable $e) {
  5. return Lottery::odds(1, 1000);
  6. });
  7. })

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

  1. use App\Exceptions\ApiMonitoringException;
  2. use Illuminate\Support\Lottery;
  3. use Throwable;
  4. ->withExceptions(function (Exceptions $exceptions) {
  5. $exceptions->throttle(function (Throwable $e) {
  6. if ($e instanceof ApiMonitoringException) {
  7. return Lottery::odds(1, 1000);
  8. }
  9. });
  10. })

你还可以通过返回 php Limit 实例而不是 php Lottery 来限制记录或发送到外部错误跟踪服务的异常的频率。如果你想要防止突然出现的异常高峰淹没你的日志,例如,当你的应用程序使用的第三方服务宕机时,这会很有用:

  1. use Illuminate\Broadcasting\BroadcastException;
  2. use Illuminate\Cache\RateLimiting\Limit;
  3. use Throwable;
  4. ->withExceptions(function (Exceptions $exceptions) {
  5. $exceptions->throttle(function (Throwable $e) {
  6. if ($e instanceof BroadcastException) {
  7. return Limit::perMinute(300);
  8. }
  9. });
  10. })

默认情况下,限制将使用异常的类作为频率限制的关键。你可以通过在 php Limit 上指定自己的键来自定义它,使用 php by 方法:

  1. use Illuminate\Broadcasting\BroadcastException;
  2. use Illuminate\Cache\RateLimiting\Limit;
  3. use Throwable;
  4. ->withExceptions(function (Exceptions $exceptions) {
  5. $exceptions->throttle(function (Throwable $e) {
  6. if ($e instanceof BroadcastException) {
  7. return Limit::perMinute(300)->by($e->getMessage());
  8. }
  9. });
  10. })

当然,你可以为不同的异常返回 php Lotteryphp Limit 的混合:

  1. use App\Exceptions\ApiMonitoringException;
  2. use Illuminate\Broadcasting\BroadcastException;
  3. use Illuminate\Cache\RateLimiting\Limit;
  4. use Illuminate\Support\Lottery;
  5. use Throwable;
  6. ->withExceptions(function (Exceptions $exceptions) {
  7. $exceptions->throttle(function (Throwable $e) {
  8. return match (true) {
  9. $e instanceof BroadcastException => Limit::perMinute(300),
  10. $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
  11. default => Limit::none(),
  12. };
  13. });
  14. })

HTTP 异常

有些异常描述了来自服务器的 HTTP 错误代码。例如,这可能是一个 “页面未找到” 错误(404),一个 “未授权错误” (401),甚至是开发者生成的 500 错误。为了在应用程序中的任何位置生成这样的响应,你可以使用 php abort 辅助函数:

  1. abort(404);

自定义 HTTP 错误页面

Laravel 让显示各种 HTTP 状态码的自定义错误页面变得简单。例如,要自定义 404 HTTP 状态码的错误页面,请创建一个 php resources/views/errors/404.blade.php 视图模板。该视图将被渲染为应用程序生成的所有 404 错误。这个目录中的视图应以它们对应的 HTTP 状态码命名。由 php abort 函数引发的 php Symfony\Component\HttpKernel\Exception\HttpException 实例将作为一个 php $exception 变量传递到视图中:

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

你可以使用 php vendor:publish Artisan 命令发布 Laravel 的默认错误页面模板。一旦这些模板被发布,你可以根据自己的喜好进行自定义:

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

回退 HTTP 错误页面
你也可以为一系列 HTTP 状态码定义一个 “回退” 错误页面。如果不存在对应特定 HTTP 状态码的页面,则会渲染此页面。要完成此操作,请在应用程序的 php resources/views/errors 目录中定义一个 php 4xx.blade.php 模板和一个 php 5xx.blade.php 模板。

当定义回退错误页面时, 回退页面不会影响 php 404php 500php 503 错误响应, 因为 Laravel 内部有专门的页面来处理这些状态码。 要自定义这些状态码的页面,你应该为每个状态码分别定义一个自定义错误页面。