编程

Xdebug: PHP 的超级调试工具

4 2026-06-11 02:05:00

在前面的文章中,我使用 dd()、Log 语句和 Ray 进行了调试。每个工具都有它们的功能。但有一个工具我还没有提到——一旦你学会了,它就会完全改变你对调试的看法。

Xdebug。

如果你听说过它,但从未设置过,那么你并不孤单。Xdebug 以配置痛苦而闻名。老实说,这种声誉并不完全不值得。但是一旦它运行起来,它就会给你提供其他工具无法提供的东西:在执行过程中暂停代码,逐行检查每个变量、每个方法调用、应用做出的每个决定。

这就像为你的代码配备了一台时光机。

Xdebug 实际上做了什么

让我解释一下是什么让 Xdebug 与众不同。

当你使用 dd() 时,你是在说:“停在这里,给我看看这个值。”请求会终止。你看到一个快照。

当你使用 Ray 时,你是在说:“在代码继续运行的同时向我发送这个值。”你看到了一个数据流,但你无法控制它。

当你使用 Xdebug 时,你是在说:“在这里暂停。让我四处看看。让我一次向前走一行。让我决定什么时候继续。”

你不再只是观察你的代码了。你在控制它。

设置安装 Xdebug

本文将不会介绍如何安装设置。可百度搜索查看网络上的相关教程。

选择一个符合你的进行设置的,花 15 - 20 分钟让它工作,然后回到这里。我等着。

你的第一次调试会话

好的,Xdebug 已经安装。你的 IDE 正在监听连接。现在怎么办?

假设我正在调试上一篇文章中的结账流程。订单总额有问题,我想确切地了解发生了什么。

首先,我设置了一个断点。在 PHPStorm(或 VS Code)中,我在要暂停执行的行号旁边的空白处单击:

public function store(CheckoutRequest $request)
{
    $cart = Cart::findOrFail($request->cart_id);
    
    // I click here to set a breakpoint
    $order = Order::create([  // <-- Red dot appears in the gutter
        'user_id' => auth()->id(),
        'total' => $cart->calculateTotal(),
    ]);
    
    event(new OrderCreated($order));
    
    return redirect()->route('orders.show', $order);
}

然后,我在 IDE 中启动调试监听器并发出请求——要么通过浏览器(启用了 Xdebug 浏览器扩展),要么通过运行测试。

执行到那一刻,一切都停止了。我的 IDE 亮起:

  • 当前行高亮显示——显示执行暂停的确切位置
  • 调用堆栈——显示我们是如何到达这里的(哪个控制器、哪个中间件、哪个路由)
  • 作用域中的所有变量——$request$cart 和此时可用的任何其他变量

这就是强大的地方。

检查变量(优于任何 dd()

在变量面板中,我可以看到 $cart 和其中的所有内容。但与 dd() 不同,我可以:

  • 展开嵌套对象--单击进入 $cart->items 并查看每个项目,然后单击进入每个项目并查看其属性
  • 求值表达式——在求值窗口中键入 $cart->calculateTotal(),查看它返回什么,而无需修改我的代码
  • 即时修改值--将 $cart->discount 更改为其他值,并继续执行以查看会发生什么

最后一点很重要。我可以实时测试假设,而不用更改代码、刷新和重试。

“如果折扣为零怎么办?那么总数是正确的吗?”

我只需更改调试器中的值,点击 continue,即可立即发现。

逐步遍历代码

这就是 Xdebug 真正厉害的地方。一旦我在断点处暂停,我有几个选择:

  • 单步执行(PHPStorm 中的 F8):执行当前行并移动到下一行。如果当前行调用了一个方法,它将完全运行该方法并在下一行停止。
  • 单步进入(F7):如果当前行调用了一个方法,请跳到该方法并在第一行暂停。这就是我如何准确跟踪 calculateTotal() 的作用。
  • 单步跳出(Shift+F8):如果我在一个方法内部,并且已经看够了,请跳回到调用此方法的任何地方。
  • 继续(F9):继续运行,直到下一个断点(或直到请求完成)。

让我向你展示如何使用这些来调试结账问题。

我在 Order::create() 行停顿了一下。我想知道 $cart->calculateTotal() 返回什么,更重要的是,它为什么返回那个值。

所以我进入(Step Into) calculateTotal() 方法:

// Now I'm inside Cart.php

public function calculateTotal(): float
{
    $subtotal = $this->items->sum(function ($item) {
        return $item->price * $item->quantity;
    });
    
    $discount = $this->calculateDiscount($subtotal);
    
    return $subtotal - $discount;
}

我能看到 $subtotal 的计算过程。如果怀疑问题出在 calculateDiscount() 函数,我可以单步进入该函数。我还可以逐行单步执行,观察每个变量的变化情况。

这就像看着你的代码以慢动作执行。

条件断点

有时,错误仅在特定条件下才会发生。例如,结账功能对大多数订单运行正常,但在应用折扣码时会出现问题。

我不想在每次结账时都暂停——只有在有折扣码时才暂停。

右键点击断点并添加条件:

$cart->discount_code !== null

现在,Xdebug 只会在该条件为真时暂停。我可以进行 10 次测试购买,调试器只会对有关的购买激活。

这对于在循环中调试问题也非常有用。而不是在每次迭代时暂停:

foreach ($items as $item) {
    $this->processItem($item);  // Breakpoint here with condition: $item->sku === 'PROBLEM-SKU'
}

此处,我只暂停特定的项目。

调试请求生命周期

还记得其他文章中提到的请求生命周期吗?Xdebug 让你可以确实看到这个过程。

在某个中间件中设置断点:

// app/Http/Middleware/LogApiRequests.php

public function handle(Request $request, Closure $next)
{
    // Breakpoint here
    $response = $next($request);
    
    return $response;
}

现在,当你发起请求时,你可以:

  1. 在中间件中暂停,在请求到达控制器之前监测请求
  2. 单步进入 $next($request) 调用,以在管道中深度跟进请求。
  3. 观察其在每个中间件中的移动,然后进入路由,然后进入控制器。
  4. 查看返回时的响应对象

这样你就真正了解 Laravel 是如何工作的。不是通过阅读它,而是通过观察它的发生。

调试 Eloquent 查询

我最喜欢的 Xdebug 用途之一是了解 Eloquent 实际上在做什么。

在查询范围或关联方法内设置断点:

// app/Models/User.php

public function scopeActive($query)
{
    // Breakpoint here
    return $query->where('status', 'active')
                 ->whereNotNull('email_verified_at');
}

当执行暂停时,我可以在调试器中计算 $query->toSql(),以查看正在构建的 SQL。我可以逐步观察查询构建器调用链。

对于复杂的查询,这比每次都进行日志记录要好。

调试 Job 和命令

与其他工具相比,Xdebug 在这方面节省了最多的时间。

当作业在队列中运行时,没有浏览器。dd() 只会打印 worker 进程输出(如果你正在看的话)。Ray 可以使用,但你仍然只是在观察。

而使用 Xdebug,可以在作业中设置断点:

// app/Jobs/ProcessOrder.php

public function handle()
{
    // Breakpoint here
    $this->order->items->each(function ($item) {
        $this->updateInventory($item);
    });
    
    $this->sendConfirmationEmail();
}

然后我运行队列 worker。当作业执行时,调试器会暂停,我可以逐步完成整个作业执行过程——检查订单、查看库存更新,看看到底发生了什么。

对于 Artisan 命令,同样的事情:

// app/Console/Commands/ImportUsers.php

public function handle()
{
    // Breakpoint here
    $rows = $this->parseCSV();
    
    foreach ($rows as $row) {
        User::create($row);
    }
}

运行 php artisan import:users,调试器会捕获它。

调试测试

这就是 Xdebug 成为我日常工具的地方。

我写了一个失败的测试。我没有添加 dd() 调用并反复运行它,而是设置了一个断点并在调试模式下运行测试。

public function test_order_total_includes_discount()
{
    $cart = Cart::factory()
        ->has(CartItem::factory()->count(3))
        ->create(['discount_code' => 'SAVE20']);
    
    // Breakpoint here
    $response = $this->post('/checkout', [
        'cart_id' => $cart->id,
    ]);
    
    $response->assertStatus(200);
    
    $order = Order::first();
    $this->assertEquals(80.00, $order->total); // This is failing — why?
}

当我在调试模式下运行此测试时,我可以进入结账过程,观察计算的总数,并确切地看到预期的 80.00 变成了其他值。

无需猜测。我可以确实知晓。

调试心态的转变

以下是你熟悉 Xdebug 后会发生的变化:

之前:“让我添加一些 dd() 调用,看看发生了什么。”

之后:“让我设置一个断点,看看发生了什么。”

区别在于控制。使用 dd() 和 Ray,你可以在事后收集证据。使用 Xdebug,你可以在现场观看一切展开。

这需要一些时间来适应。前几次,遍历代码感觉很慢。但是,一旦你熟悉了键盘快捷键,并对在哪里设置断点有了直觉,它就成为理解复杂错误的最快方法。

何时使用何种方式

这是我的看法:

情形工具
快速确认值dd()
查看请求的数据流Ray
理解为什么出现某个情形Xdebug
调试多分支复杂逻辑Xdebug
调试作业、命令或者测试Xdebug
生产环境调试Log 语句

Xdebug 不是其他工具的替代品——当仅仅是观察数据不够用,需要控制时候可以使用的工具。

开始尝试

如果你从没用过 Xdebug,不妨花费 30 分钟设置一些。

下次出现 bug 是,如果需要 5-6 个 dd() 调用才能跟踪到,不妨试试该调试器。

设置断点。切入到代码中,观察变量的变化过程。

 

PHP