Skip to content

第 2.7 章:错误处理

学习时间: 40 分钟


1. 为什么标准错误很糟糕?

如果您的 Laravel 应用程序发生错误(例如,在数据库中找不到记录),而您没有进行任何处理,用户将会看到一个包含调试信息的巨大 HTML 页面,或者一条不具信息量的“服务器错误”消息。

对于 API 来说,这是一场灾难。您的前端应用程序期望接收 JSON,而不是 HTML。我们的任务是拦截任何错误并将其转换为结构化的 JSON 响应。


2. 错误中央调度器:bootstrap/app.php

在旧版 Laravel 中,有一个笨重的 App\Exceptions\Handler.php 文件。在 Laravel 11/12 中,一切都变得更加简单和优雅。错误管理中心现在直接位于您的应用程序配置文件中 — bootstrap/app.php

打开 bootstrap/app.php。在最底部,您会看到一个 .withExceptions(...) 块。这就是我们的“中央调度器”。

<?php
// bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // ...
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // <-- 我们将在这里工作
    })->create();

3. 处理最常见的错误:“未找到” (404)

API 中最常见的错误是用户请求不存在的资源(例如,GET /api/planets/999)。在这种情况下,Laravel 会生成 ModelNotFoundExceptionNotFoundHttpException 异常。让我们来拦截它们。

将以下代码添加到 .withExceptions(...) 内部:

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // 当在数据库中找不到模型时,拦截异常
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        // 检查请求是否来自我们的 API
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '在我们的银河系中未找到所请求的资源。'
            ], 404);
        }
    });

    // 当路由本身未找到时,拦截异常
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '不存在这样的宇宙路线。'
            ], 404);
        }
    });

})->create();
我们做了什么?

  1. $exceptions->render(...) — 我们注册了一个“处理程序”。它表示:“如果发生 ModelNotFoundException 类型的异常,则执行此代码。”
  2. if ($request->is('api/*')) — 这是一个重要的检查。它确保我们漂亮的 JSON 响应仅发送给 API 请求,而不影响普通的网页。
  3. return response()->json(...) — 我们创建并返回带有 404 代码的标准化 JSON 响应。

现在,如果您请求一个不存在的星球,您将获得一个整洁的 JSON,而不是丑陋的 HTML 页面。


4. 自定义异常:创建我们自己的“警报信号”

有时标准异常是不够的。假设我们有一个业务规则:“不能删除‘地球’这个星球”。如果有人试图这样做,我们必须返回一个有意义的错误。

步骤 1:创建我们自己的异常类 在终端中执行:

php artisan make:exception CannotDeleteEarthException

步骤 2:在控制器中使用它 打开 PlanetController.php 并修改 destroy 方法:

<?php
// app/Http/Controllers/PlanetController.php
use App\Exceptions\CannotDeleteEarthException; // <-- 导入我们的异常
use App\Models\Planet;

public function destroy(Planet $planet)
{
    // 我们的新业务规则
    if (strtolower($planet->name) === 'земля') {
        throw new CannotDeleteEarthException('根据银河法典,禁止删除地球。');
    }

    $planet->delete();
    return response()->json(null, 204);
}
现在,如果有人试图执行 DELETE /api/planets/1(其中 1 是地球的 ID),我们的代码将抛出 CannotDeleteEarthException 异常。

步骤 3:教 Laravel 优雅地处理我们的“警报” 回到 bootstrap/app.php 并为我们的异常添加一个新的处理程序。

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // 我们的新处理程序
    $exceptions->render(function (CannotDeleteEarthException $e, Request $request) {
        return response()->json([
            'message' => '操作被禁止。',
            'details' => $e->getMessage() // 获取我们通过 throw 传递的消息
        ], 403); // 403 Forbidden - “禁止访问”
    });

    // ... (404 的其他处理程序)

})->create();
完成!我们创建了自己命名的异常,这使得控制器代码更清晰,并教会 Laravel 将其转换为具有正确 HTTP 状态的漂亮、有意义的 JSON 响应。


5. 处理所有其他故障 (500 Internal Server Error)

如何处理所有其他意外错误?例如,如果数据库崩溃或代码中存在语法错误。为此,我们可以为最常见的错误类型 — Throwable — 注册一个“通用”处理程序。

重要提示: 这个处理程序必须是最后一个,以免拦截我们上面定义的更具体的异常。

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // ... (CannotDeleteEarthException 和 404 的处理程序)

    // 通用处理程序(在最末尾)
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->is('api/*')) {
            // 在调试模式下,可以显示真实的错误消息
            $message = config('app.debug')
                ? '发生错误:' . $e->getMessage()
                : '船上发生了意外错误。工程师已被召集。';

            return response()->json(['message' => $message], 500);
        }
    });

})->create();

现在,任何“未知”异常都将被巧妙地拦截并转换为带有 500 代码的 JSON,而不会破坏您的 API 或向用户显示多余的信息。


6. 错误日志记录:宇宙飞船的黑匣子

config/logging.php 中的日志配置:

<?php
'channels' => [
    'space_api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/space_api.log'),
        'level' => 'error',
        'days' => 14,
    ],
],

添加日志记录:

<?php
try {
    // 有错误风险的代码
} catch (Exception $e) {
    Log::channel('space_api')->error('访问星球时出错', [
        'exception' => $e,
        'request' => request()->all(),
        'user_id' => auth()->id()
    ]);
    throw $e;
}


巩固测验

1. “星球未找到”的 HTTP 状态码:

2. 用于全局错误处理的类:

3. 创建自定义异常的方法:

4. 用于 API 错误单独日志记录的通道:

5. 创建自定义异常的主要优势:


🚀 本章总结:

您为您的 API 配备了可靠的救援系统: - 🛟 全局捕获标准错误 - 🪐 带有可理解代码的自定义异常 - 📝 所有错误统一使用 JSON 格式 - 🔍 记录事件详情日志 - 📡 与监控系统集成

宇宙飞船已准备好应对紧急情况! 在本章的最后一节,我们将测试所有系统。

📌 检查:

  1. 创建 PlanetNotFoundException 异常
  2. ->withExceptions 中添加 404 错误处理
  3. 测试对不存在行星的请求

⚠️ 如果错误未被捕获:

  • 确保 is('api/*') 与您的路由匹配
  • 检查 register() 中的处理程序顺序
  • 对于自定义异常,请使用 throw new