Xdebug: PHP 的超级调试工具
在前面的文章中,我使用 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;
}现在,当你发起请求时,你可以:
- 在中间件中暂停,在请求到达控制器之前监测请求
- 单步进入
$next($request)调用,以在管道中深度跟进请求。 - 观察其在每个中间件中的移动,然后进入路由,然后进入控制器。
- 查看返回时的响应对象
这样你就真正了解 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() 调用才能跟踪到,不妨试试该调试器。
设置断点。切入到代码中,观察变量的变化过程。