Skip to content

错误处理

简介

当你启动一个新的 Laravel 项目时,错误和异常处理已经为你配置好了;然而,在任何时候,你都可以使用应用程序的 bootstrap/app.php 文件中的 withExceptions 方法来管理应用程序如何报告和渲染异常。

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

配置

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

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

异常处理

报告异常

在 Laravel 中,异常报告用于记录异常或将其发送到外部服务,如 SentryFlare。默认情况下,异常将根据你的日志配置进行记录。但是,你可以自由地以任何你想要的方式记录异常。

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

php
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->report(function (InvalidOrderException $e) {
            // ...
        });
    })

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

php
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->report(function (InvalidOrderException $e) {
            // ...
        })->stop();

        $exceptions->report(function (InvalidOrderException $e) {
            return false;
        });
    })

NOTE

要为给定的异常自定义异常报告,你也可以利用可报告的异常

全局日志上下文

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

php
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->context(fn () => [
            'foo' => 'bar',
        ]);
    })

异常日志上下文

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

php
    <?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 辅助函数允许你快速报告异常而不向用户渲染错误页面:

php
    public function isValid(string $value): bool
    {
        try {
            // Validate the value...
        } catch (Throwable $e) {
            report($e);

            return false;
        }
    }

去重报告的异常

如果你在应用程序中使用 report 函数,你可能会偶尔报告同一异常多次,在日志中创建重复条目。

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

php
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->dontReportDuplicates();
    })

现在,当使用 report 辅助函数报告同一异常实例时,只有第一次调用会被报告:

php
$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 异常方法。此方法接收异常类型作为其第一个参数和日志级别作为其第二个参数:

php
    use PDOException;
    use Psr\Log\LogLevel;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->level(PDOException::class, LogLevel::CRITICAL);
    })

按类型忽略异常

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

php
    use App\Exceptions\InvalidOrderException;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->dontReport([
            InvalidOrderException::class,
        ]);
    })

或者,你可以简单地在异常类上"标记" Illuminate\Contracts\Debug\ShouldntReport 接口。当异常被标记为此接口时,它将永远不会被 Laravel 的异常处理程序报告:

php
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

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

php
    use Symfony\Component\HttpKernel\Exception\HttpException;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->stopIgnoring(HttpException::class);
    })

渲染异常

默认情况下,Laravel 异常处理程序会为你将异常转换为 HTTP 响应。但是,你可以为给定类型的异常注册自定义渲染闭包。你可以在应用程序的 bootstrap/app.php 文件中使用 render 异常方法来实现这一点。

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

php
    use App\Exceptions\InvalidOrderException;
    use Illuminate\Http\Request;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (InvalidOrderException $e, Request $request) {
            return response()->view('errors.invalid-order', status: 500);
        });
    })

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

php
    use Illuminate\Http\Request;
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

    ->withExceptions(function (Exceptions $exceptions) {
        $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 方法:

php
    use Illuminate\Http\Request;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
            if ($request->is('admin/*')) {
                return true;
            }

            return $request->expectsJson();
        });
    })

自定义异常响应

很少,你可能需要自定义 Laravel 异常处理程序渲染的整个 HTTP 响应。要实现这一点,你可以使用 respond 方法注册响应自定义闭包:

php
    use Symfony\Component\HttpFoundation\Response;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->respond(function (Response $response) {
            if ($response->getStatusCode() === 419) {
                return back()->with([
                    'message' => 'The page expired, please try again.',
                ]);
            }

            return $response;
        });
    })

可报告和可渲染的异常

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

php
    <?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 into an HTTP response.
         */
        public function render(Request $request): Response
        {
            return response(/* ... */);
        }
    }

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

php
    /**
     * Render the exception into 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:

php
    /**
     * Report the exception.
     */
    public function report(): bool
    {
        if (/** Determine if the exception needs custom reporting */) {

            // ...

            return true;
        }

        return false;
    }

NOTE

你可以在异常的 report 方法中对所需的任何依赖项进行类型提示,它们将由 Laravel 的服务容器自动注入到方法中。

限制报告的异常

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

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

php
    use Illuminate\Support\Lottery;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->throttle(function (Throwable $e) {
            return Lottery::odds(1, 1000);
        });
    })

还可以根据异常类型进行条件抽样。如果你只想对特定异常类的实例进行抽样,你可以仅为该类返回 Lottery 实例:

php
    use App\Exceptions\ApiMonitoringException;
    use Illuminate\Support\Lottery;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->throttle(function (Throwable $e) {
            if ($e instanceof ApiMonitoringException) {
                return Lottery::odds(1, 1000);
            }
        });
    })

你还可以通过返回 Limit 实例而不是 Lottery 实例来限制记录或发送到外部错误跟踪服务的异常。这在你想要保护免受突然涌入的异常淹没日志时非常有用,例如,当应用程序使用的第三方服务不可用时:

php
    use Illuminate\Broadcasting\BroadcastException;
    use Illuminate\Cache\RateLimiting\Limit;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->throttle(function (Throwable $e) {
            if ($e instanceof BroadcastException) {
                return Limit::perMinute(300);
            }
        });
    })

默认情况下,限制将使用异常的类作为速率限制键。你可以使用 Limit 上的 by 方法指定自己的键:

php
    use Illuminate\Broadcasting\BroadcastException;
    use Illuminate\Cache\RateLimiting\Limit;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->throttle(function (Throwable $e) {
            if ($e instanceof BroadcastException) {
                return Limit::perMinute(300)->by($e->getMessage());
            }
        });
    })

当然,你可以为不同的异常返回 LotteryLimit 实例的混合:

php
    use App\Exceptions\ApiMonitoringException;
    use Illuminate\Broadcasting\BroadcastException;
    use Illuminate\Cache\RateLimiting\Limit;
    use Illuminate\Support\Lottery;
    use Throwable;

    ->withExceptions(function (Exceptions $exceptions) {
        $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 辅助函数:

php
    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 的默认错误页面模板。一旦模板被发布,你可以根据自己的喜好自定义它们:

shell
php artisan vendor:publish --tag=laravel-errors

回退 HTTP 错误页面

你还可以为一系列 HTTP 状态码定义 "回退" 错误页面。如果没有与特定发生的 HTTP 状态码对应的页面,将呈现此页面。要实现这一点,在应用程序的 resources/views/errors 目录中定义一个 4xx.blade.php 模板和一个 5xx.blade.php 模板。

在定义回退错误页面时,回退页面不会影响 404, 500503 错误响应,因为 Laravel 为这些状态码有内部专用页面。要自定义这些状态码呈现的页面,你应该为每个状态码单独定义自定义错误页面。