<p>There's a moment in every developer's day that deserves better than it usually gets: the moment you stop guessing what your code is doing and actually watch it. Set a breakpoint, reload the page, and step through line by line while the variables reveal themselves. It's one of the most powerful things you can do — and yet setting it up has a reputation for being fiddly enough that a lot of us just… sprinkle <code>dd()</code> everywhere and move on.</p><p>Grove 0.2.9 is about making that first move — turning step-debugging on — feel like flipping a light switch. It also comes with an honest story about a limitation we ran straight into, which I think is worth telling.</p><h2>Why: dd() is fine, until it isn't</h2><p><code>dd()</code> and <code>ray()</code> are great. But some bugs live in the space between lines — a loop that runs one too many times, a value that's null when you swore it couldn't be, a middleware that quietly rewrites a request. For those, nothing beats a real debugger: pause execution, inspect the whole call stack, hover over variables, step in and out.</p><p>The tool for that in PHP is Xdebug, and the friction has always been the setup: editing <code>php.ini</code>, restarting things, remembering the magic port, worrying about the performance hit of leaving it on. Grove's job is to make that setup disappear.</p><h2>How: one toggle, trigger mode, done</h2><p>Open <strong>Tools → Xdebug step-debugging</strong> and flip the switch:</p><pre><code class="language-text">  🛠  Tools
  ┌───────────────────────────────────────────────────────────────┐
  │  Xdebug step-debugging                                 (──● )  │  ← live toggle
  │  Load Xdebug into PHP-FPM on demand. A request opts in with    │
  │  the XDEBUG_TRIGGER cookie/param; editor listens on port 9003. │
  │                                                                 │
  │   php@8.4    ready (built into this PHP)                        │
  │   php@8.3    unavailable — needs a PHP with Xdebug              │
  │                                                                 │
  │  Needs a PHP that has Xdebug — grove php register.              │
  └───────────────────────────────────────────────────────────────┘
</code></pre><p>Prefer the terminal? Same thing:</p><pre><code class="language-text">  $ grove debug status
  Xdebug disabled (DBGp port 9003)
    php@8.4  ready (built into this PHP)
    php@8.3  unavailable — needs a PHP with Xdebug (grove php register)

  $ grove debug on
  Xdebug enabled (FPM pools reloaded, DBGp port 9003).
  Start a session with the XDEBUG_TRIGGER cookie/param, or `grove debug env` for CLI.
</code></pre><p>Two details make this pleasant to live with:</p><p><strong>It never touches your </strong><code>php.ini</code><strong>.</strong> Grove loads Xdebug per FPM pool via <code>-d</code> startup flags, so nothing global changes and toggling it off leaves no trace.</p><p><strong>It stays out of the way until you ask.</strong> Xdebug runs in <code>start_with_request=trigger</code> mode — the extension is resident but dormant. A normal request pays almost nothing; a request only starts a debug session when it carries the <code>XDEBUG_TRIGGER</code> cookie or query param. Install the "Xdebug helper" browser extension, click the little bug, reload — and now you're debugging. Everyone else hitting the site notices nothing.</p><h3>Debugging the browser side</h3><ol><li><p>In your editor, start a "Listen for Xdebug" / DBGp session on port 9003.</p></li><li><p>Toggle Xdebug on in Grove.</p></li><li><p>Flip the browser extension to Debug and reload your <code>*.test</code> page.</p></li><li><p>Your breakpoint hits. 🎯</p></li></ol><h3>Debugging the CLI side (artisan, tests)</h3><p>The browser trigger doesn't help a command-line process, so Grove prints the exact environment for it:</p><pre><code class="language-text">  $ eval "$(grove debug env)"
  $ php artisan queue:work        # now connects to your editor's listener
</code></pre><p>Under the hood that's just:</p><pre><code class="language-text">  $ grove debug env
  export XDEBUG_MODE=debug
  export XDEBUG_SESSION=1
  export XDEBUG_CONFIG="client_host=127.0.0.1 client_port=9003"
</code></pre><p>Grove speaks the runtime half of this — getting Xdebug loaded and pointed at the right port. Your editor's DAP/DBGp client is the other half, and it listens; Xdebug connects out to it.</p><h2>The honest part: static PHP can't be debugged</h2><p>Here's the limitation I promised. Grove's bundled PHP builds are fully static — a single self-contained binary with no external dependencies, which is exactly what makes "download Grove and go" work without Homebrew or a system PHP.</p><p>But that superpower has a cost: a fully-static PHP cannot load Xdebug. Xdebug is a Zend extension, and static PHP has no way to <code>dlopen</code> an external <code>.so</code> — and the tool we build our PHP with (<code>static-php-cli</code>) flatly refuses to compile Xdebug in, either:</p><pre><code class="language-text">  Extension [xdebug] does not support static build!
</code></pre><p>We explored shipping a parallel "debug build," a CI pipeline, a downloadable <code>xdebug.so</code> — and every path dead-ended on the same wall. So rather than ship a button that could never work, 0.2.9 does the honest thing: it tells you the truth.</p><p>If you want to step-debug, register a PHP that already has Xdebug:</p><pre><code class="language-text">  $ grove php register /opt/homebrew/opt/php/bin/php
  ✓ registered php@8.4 (with xdebug)

  $ grove debug status
  Xdebug disabled (DBGp port 9003)
    php@8.4  ready (built into this PHP)
</code></pre><p>Isolate a site to that build (<code>grove isolate myapp 8.4</code>), toggle Xdebug on, and you're debugging — with Grove handling all the wiring.</p><h2>Getting it</h2><p>Grove auto-updates. After it relaunches, restart the background service to pick up the new daemon — one click now:</p><pre><code class="language-text">  Tools → Restart daemon   ↻   (no password)
</code></pre><p>Then:</p><pre><code class="language-text">  $ grove status | head -1
  Grove 0.2.9
</code></pre><hr><p>Not every feature is about adding power — some are about removing the little rituals that stand between you and the work. Turning on a debugger shouldn't require a config-file pilgrimage. Flip a switch, click the bug, watch your code run.</p><p>And when a tidy story meets a stubborn technical wall, the kindest thing software can do is be honest about it. 🌳</p>