laravel
4. 基础知识
错误处理

介绍

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

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

配置

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

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

处理异常

报告异常

在 Laravel 中,异常报告用于记录异常或将其发送到外部服务 Sentry (opens in a new tab)Flare (opens in a new tab)。默认情况下,异常将根据您的 日志记录 配置进行记录。但是,您可以按照自己的意愿记录异常。

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

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

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

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

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

[!注意]
要为给定的异常自定义异常报告,您还可以利用 可报告的异常

全局日志上下文

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->context(fn () => [
        '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 函数,您可能偶尔会多次报告相同的异常,从而在您的日志中创建重复的条目。

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

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

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

$original = new RuntimeException('Whoops!');
 
report($original); // 被报告
 
try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // 被忽略
}
 
report($original); // 被忽略
report($caught); // 被忽略

异常日志级别

当消息被写入到应用程序的日志中时,这些消息会以指定的日志级别进行写入,该级别表示正在记录的消息的严重性或重要性。

如上所述,即使您使用report方法注册了自定义异常报告回调,Laravel 仍会使用应用程序的默认日志配置来记录异常;然而,由于日志级别有时会影响消息被记录的通道,您可能希望配置某些异常被记录的日志级别。

要实现此目的,您可以在应用程序的bootstrap/app.php文件中使用level异常方法。此方法将异常类型作为其第一个参数,将日志级别作为其第二个参数:

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

按类型忽略异常

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

use App\Exceptions\InvalidOrderException;
 
->withExceptions(function (Exceptions $exceptions) {
    $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
{
    //
}

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

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 将通过检查闭包的类型提示来确定闭包呈现的异常类型:

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)的呈现行为。如果提供给render方法的闭包未返回值,则将使用 Laravel 的默认异常呈现:

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方法:

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方法注册一个响应自定义闭包:

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;
    });
})

可报告和可渲染的异常

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

<?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;
}

[!NOTE]
您可以为 report 方法的任何所需依赖项进行类型提示,Laravel 的服务容器将自动将它们注入到该方法中。

限制报告的异常

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

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

use Illuminate\Support\Lottery;
use Throwable;

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

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

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(中止)助手:

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 模板。

在定义备用错误页面时,备用页面不会影响 404500503 错误响应,因为 Laravel 为这些状态代码有内部专用页面。要为这些状态代码自定义渲染的页面,您应该为它们各自定义一个自定义错误页面。