The Origin
I've been working with PHP and Laravel for years. And like every PHP developer, I've been through that classic cycle: install Xdebug, configure xdebug.mode=debug, map paths in PHPStorm, pray that Docker doesn't change the internal network IP, and when it finally works... it's slow. Every request takes seconds longer.
But the real problem was never performance. It was setup. I was working on projects running in Docker via Laravel Sail, others on WSL, some with SSH to staging. Each environment meant a different Xdebug configuration. Network changed? Redo it. Spun up a new container? Remap paths. Switched machines? Start from zero.
The turning point was on a Sunday. I had a production bug in a Laravel project, needed to debug fast, and spent 40 minutes just trying to get Xdebug to connect through Docker. When it finally did, the breakpoint landed on the wrong line because the path mapping was off by one level. At that moment I thought: "This can't be this hard. What if the debugger didn't need any extension at all? What if it was part of the code itself?"
That's how DDLess was born.
Iteration 1: phpdbg
Before instrumenting code, my first idea was to use what already existed. PHP has phpdbg, a built-in interactive debugger. It uses Zend Engine opcodes, it's fast, and it doesn't need an extension. Seemed perfect.
The problem is that phpdbg was designed to be interactive in the terminal. It has no clean communication protocol for integrating with an external UI. To make it work with a graphical interface, I would have to parse its text output, which changes between PHP versions. On top of that, it doesn't work well inside contexts like Laravel Sail or Docker because it needs to be the process entrypoint, and that doesn't work when you have a web server in front.
Within a few hours I realized this path would lead me down a compatibility rabbit hole. I scrapped it.
Iteration 2: Regex and Tokenizer
The next idea was: what if I injected debug calls directly into the PHP code before execution? Something like a ddless_step_check(__FILE__, __LINE__) before each executable line. If PHP ran this modified code, I'd have full control over the execution flow without needing any extension.
The first step was figuring out which lines are "instrumentable," meaning where it makes sense to place a breakpoint. There's no point placing one on a comment, a class declaration, or in the middle of a multiline string.
My first implementation used PHP's token_get_all() to tokenize the code and a state machine with regex to decide where to inject. It worked for simple cases:
$x = 1; // instrumentable
echo $x; // instrumentable
if ($x > 0) { // instrumentable
But the edge cases were endless. Chained method calls across multiple lines:
$query->where('status', 'active')
->orderBy('created_at') // continuation, not a new statement
->get();
Heredoc/nowdoc strings with PHP code inside. Multiline arrays where the closing ] was on the next line. elseif that needed special handling because you can't inject a line before an elseif, as it would break the if chain. Same for else, catch, and finally.
It was almost 600 lines of code between ddless_analyze_code_tokens(), ddless_is_safe_to_inject_before(), and several helper functions. And every week a new case would break things. A developer would use a different coding pattern and the instrumentation would generate a syntax error.
I knew I was building on sand. But it worked for a while, and it allowed me to validate that the core idea — code instrumentation combined with file-based IPC — was viable.
See more: https://ddless.com/blog/technical-journey-building-php-debugger
Top comments (0)