phpdbg for fun and profit – Adam Harvey – @LGnome



phpdbg for fun and profit – Adam Harvey – @LGnome

0 1


phpdbg-for-fun-and-profit

Slides for my "phpdbg for fun and profit" talk.

On Github LawnGnome / phpdbg-for-fun-and-profit

phpdbg for fun and profit

Adam Harvey

@LGnome

So let's start with the basics. What is phpdbg?

Command line debugger

It's a command line debugger, like gdb or lldb. It was originally written by Joe Watkins and Felipe Pena, and has also seen significant input from Bob Weinand.
aharvey@aharvey-mbp:/tmp$ phpdbg code/fizzbuzz.php [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /tmp/code/fizzbuzz.php] prompt> break fizzbuzz [Breakpoint #0 added at fizzbuzz] prompt> run 15 [Breakpoint #0 at /tmp/code/fizzbuzz.php:3, hits: 1] >00003: if (0 == ($n % 3)) { 00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { prompt> Practically, that means that you control it via commands in the terminal, like so. This is an example that we'll walk through in a few minutes.
This does mean that your script is not running in a Web server like XDebug, and your output isn't going to go to a browser. We'll talk about that later.
Let's talk about getting phpdbg. Unlike XDebug and most other debugging and profiling tools for PHP, it's not an extension, but rather a different SAPI. (Explain SAPIs.)

Debian and Ubuntu

apt-get install php5-phpdbg
apt-get install php7.0-phpdbg
apt-get install php7.1-phpdbg
On Debian and Ubuntu, there are packages available in recent versions: in practice, that means Vivid, Wily and Xenial for Ubuntu and Jessie for Debian. Additionally, Ondřej Surý packages phpdbg for all versions back to Precise.

Red Hat and CentOS

yum install php56-php-dbg
yum install php70-php-dbg
yum install php71-php-dbg
dnf install php-dbg
On RHEL 5-7, Remi's repo includes phpdbg for PHP 5.6 or 7.0. Recent versions of Fedora include phpdbg as well. Note that you can't search for phpdbg.

Mac OS X

brew tap homebrew/php
brew install php70 --with-phpdbg
The Homebrew PHP tap includes phpdbg packages for PHP 5.4 and later. MAMP… well, don't use MAMP.

From source

./configure --enable-phpdbg
Finally, if you're building from source (good for you!), it's bundled from PHP 5.6. You just need to add one switch to configure on PHP 5.6. On PHP 7.0, you'll get phpdbg by default.
Let's look at how to use phpdbg, starting with how to start it.
aharvey@aharvey-mbp:/tmp$ phpdbg code/fizzbuzz.php [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /tmp/code/fizzbuzz.php] prompt> We saw this in screenshot form briefly earlier. Like PHP CLI, we provide a file name. (You don't have to, but I like to.) However, instead of starting the script immediately, phpdbg just starts the Zend Engine and gives us a prompt. Help will give us…
prompt> help phpdbg is a lightweight, powerful and easy to use debugging platform for PHP5.4+ It supports the following commands: Information list list PHP source info displays information on the debug session print show opcodes frame select a stack frame and print a stack frame summary back shows the current backtrace help provide help on a topic Starting and Stopping Execution exec set execution context run attempt execution step continue execution until other line is reached continue continue execution until continue execution up to the given location next continue execution up to the given location and halt on the first line after it finish continue up to end of the current execution frame leave continue up to end of the current execution frame and halt after the calling instruction break set a breakpoint at the specified target watch set a watchpoint on $variable clear clear one or all breakpoints clean clean the execution environment Miscellaneous set set the phpdbg configuration source execute a phpdbginit script register register a phpdbginit function as a command alias sh shell a command ev evaluate some code quit exit phpdbg Type help <command> or (help alias) to get detailed help on any of the above commands, for example help list or h l Note that help will also match partial commands if unique (and list out options if not unique), so help clea will give help on the clean command, but help cl will list the summary for clean and clear. Type help aliases to show a full alias list, including any registered phpdginit functions Type help syntax for a general introduction to the command syntax. Type help options for a list of phpdbg command line options. Type help phpdbginit to show how to customise the debugger environment. Ack! Don't worry. You don't need to read it now, and it's not that bad. I'm not going to cover everything, but I'm going to cover the immediately useful stuff through a couple of examples.
prompt> help run Command: run Alias: r attempt execution Enter the vm, starting execution. Execution will then continue until the next breakpoint or completion of the script. Add parameters you want to use as $argv Examples prompt> run prompt> r Will cause execution of the context, if it is set prompt> r test Will execute with $argv[1] == "test" Note that the execution context must be set. If not previously compiled, then the script will be compiled before execution. Note that attempting to run a script that is already executing will result in an "execution in progress" error. prompt> To start with: let's look at run. Again, there's a bit too much for a slide, but what I want to point out is the overall structure: each command has online help that includes its aliases, what it does, and examples. Let's look at each part in turn. (I won't do this for all the commands, but it's important to know how to read the help.)
prompt> help run Command: run Alias: r attempt execution Enter the vm, starting execution. Execution will then continue until the next breakpoint or completion of the script. Add parameters you want to use as $argv This tells us that the command can also be called with a straight "r", or "attempt execution", and then describes what it does.
Examples prompt> run prompt> r Will cause execution of the context, if it is set prompt> r test Will execute with $argv[1] == "test" Here are the examples. Note that this command takes optional arguments: in this case, "test" is the same as running "php fizzbuzz.php test".
Note that the execution context must be set. If not previously compiled, then the script will be compiled before execution. Note that attempting to run a script that is already executing will result in an "execution in progress" error. Finally, we have some general notes. The execution context is the file that's the entry point. And we can't run a script if another script is executing, which is pretty common sense.
prompt> run 15 The script that I loaded is a FizzBuzz implementation, and it takes the maximum number as an argument. So let's run it. (FizzBuzz: divisible by 3 is Fizz; by 5 is Buzz; by both is FizzBuzz.)
prompt> run 15 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 Fizz [Script ended normally] prompt> Two things. Firstly, you can see that we got output, and then it ended. Secondly, I've committed one of the classic blunders, and I'm not talking about starting a land war in Asia. See that last "Fizz"?
function fizzbuzz(int $n): string {
  if (0 == ($n % 3)) {
    return 'Fizz';
  } elseif (0 == ($n % 5)) {
    return 'Buzz';
  }
  return (string) $n;
}

foreach (range(1, (int) $_SERVER['argv'][1]) as $n) {
  echo fizzbuzz($n).' ';
}
echo "\n";
Here's my script. The bug is very obvious, but let's see how we'd attack this with a debugger. We know that we call fizzbuzz() for each number, and we know that the output for 15 seems wrong.
prompt> help break Command: break Alias: b set breakpoint Breakpoints can be set at a range of targets within the execution environment. Execution will be paused if the program flow hits a breakpoint. As those of you who've used debuggers before know, the biggest single tool you have is breakpoints. Effectively, you're going to tell the runtime to stop when a certain condition is met. In traditional debuggers, that's usually when execution hits a particular line, or a particular function. phpdbg has a break command that sets a breakpoint.
prompt> break 3 [Breakpoint #0 added at /tmp/code/fizzbuzz.php:3] prompt> break fizzbuzz.php:3 [Breakpoint #0 added at /tmp/code/fizzbuzz.php:3] prompt> break fizzbuzz [Breakpoint #1 added at fizzbuzz] The basics are well, basic. You can set a breakpoint on a line alone (for the current file), a file and line (phpdbg is smart enough in this case to know it's the same one), or a function. "help break" covers the full range of syntax options: methods are also included.
prompt> run 15 [Breakpoint #0 at /tmp/code/fizzbuzz.php:3, hits: 1] >00003: if (0 == ($n % 3)) { 00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { prompt> With our newly set breakpoints, let's run the script! And, of course, it breaks right away, since we enter fizzbuzz() each time we have a value. We get this helpful source display, and we presumably want to know what $n is.
prompt> help ev Command: ev Alias: The ev command takes a string expression which it evaluates and then displays. It evaluates in the context of the lowest (that is the executing) frame, unless this has first been explicitly changed by issuing a frame command. phpdbg lets us evaluate code, which can be as simple as evaluating a variable. The nifty thing is that it evaluates it in the current context, so we can evaluate $n, even though it's a local variable.
prompt> ev $n 1 prompt> OK, $n is 1. We're pretty sure that works. We can tell the script to continue.
prompt> continue 1 [Breakpoint #0 at /tmp/code/fizzbuzz.php:3, hits: 2] >00003: if (0 == ($n % 3)) { 00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { prompt> ev $n 2 prompt> We continue. (The continue command resumes execution.) We see a "1", which is output from the script, then we break again. This time $n is 2. We're pretty sure that's OK too. Hmmm. Let's go back to the breakpoint help.
Examples prompt> break at phpdbg::isGreat if $opt == 'S' prompt> break @ phpdbg::isGreat if $opt == 'S' Break at any opcode in phpdbg::isGreat when the condition ($opt == 'S') is true There are many, many examples, but this one catches my eye. We know we're interested in fizzbuzz(), and we know we're interested only really in a special case: when $n is 15.
prompt> break del 0 [Deleted breakpoint #0] prompt> break at fizzbuzz if $n == 15 [Conditional breakpoint #1 added $n == 15/0x10a668300] prompt> Let's change it up a little. We'll delete our original breakpoint, and set a new, conditional breakpoint that only triggers when we enter fizzbuzz() and $n is 15. (That can be any valid PHP expression.)
prompt> continue 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 [Conditional breakpoint #1: at fizzbuzz if $n == 15 at /tmp/code/fizzbuzz.php:3, hits: 1] >00003: if (0 == ($n % 3)) { 00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { prompt> ev $n 15 prompt> And here we are! This is the case we care about. From here, we don't want to just continue, but we want to step through the code.
prompt> help step Command: step Alias: s step through execution Execute opcodes until next line Examples prompt> s Will continue and break again in the next encountered line Again, like pretty much every debugger ever, phpdbg has a step command. It just continues execution until the next line.
prompt> step [L3 0x10a67a040 IS_EQUAL 0 ~0 ~1 /tmp/code/fizzbuzz.php] [Conditional breakpoint #1: at fizzbuzz if $n == 15 at /tmp/code/fizzbuzz.php:3, hits: 2] >00003: if (0 == ($n % 3)) { 00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { prompt> Now we're actually executing the line we broke on. There's the opcode that's being executed last on that line (there's also a MOD, but that's happened by this point) and where we are.
prompt> step [L4 0x10a67a080 RETURN "Fizz" /tmp/code/fizzbuzz.php] >00004: return 'Fizz'; 00005: } elseif (0 == ($n % 5)) { 00006: return 'Buzz'; prompt> Step again, and we see the problem. We're about to return, and we're just returning "Fizz", not "FizzBuzz". Well, shit.
prompt> step [L12 0x10a6801c0 CONCAT @6 " " ~7 /tmp/code/fizzbuzz.php] >00012: echo fizzbuzz($n).' '; 00013: } 00014: echo "\n"; prompt> If we step again, we see we're indeed back out of the function, and we returned the wrong thing. Guess we should add a mod 15 case, eh?
Before we move onto a slightly more involved example, here's another case where phpdbg will stop execution: exceptions.
class TableFlipException extends Exception {}

function (╯°□°)╯︵ ┻━┻() {
  throw new TableFlipException('ruh roh');
}

(╯°□°)╯︵ ┻━┻();
Here's our sample code, which Sara kindly turned into a Packagist package earlier this year. We'll throw an exception, and there's no try-catch or exception handler.
aharvey@aharvey-mbp:/tmp$ phpdbg code/exception.php [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /tmp/code/exception.php] prompt> r [Uncaught TableFlipException in /tmp/code/exception.php on line 5: ruh roh] >00005: throw new TableFlipException('ruh roh'); 00006: } 00007: prompt> The nice thing here is that phpdbg goes back to the point where the exception is thrown.
class TableFlipException extends Exception {}

function (╯°□°)╯︵ ┻━┻() {
  throw new TableFlipException('ruh roh');
}

set_exception_handler(function ($e) { echo $e; });

(╯°□°)╯︵ ┻━┻();
The really useful (and interesting) part is that this still works even if you have an exception handler installed. It won't stop if you have a catch block, but if it makes it all the way to the exception handler, phpdbg will stop. That's fine, as long as you don't use exceptions for flow control, right?
aharvey@aharvey-mbp:/tmp$ phpdbg code/exception.php [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /tmp/code/exception.php] prompt> r [Uncaught TableFlipException in /tmp/code/exception.php on line 5: ruh roh] >00005: throw new TableFlipException('ruh roh'); 00006: } 00007: prompt> Here's the proof. Execution still stops at the same point, even though there's an exception handler that will eventually take care of this.
function (╯°□°)╯︵ ┻━┻() {
  throw new TableFlipException('ruh roh');
}
prompt> print [Stack in (╯°□°)╯︵ ┻━┻() (5 ops)] L4-6 (╯°□°)╯︵ ┻━┻() /Users/aharvey/Trees/phpdbg-for-fun-and-profit/code/exception.php - 0x11206e180 + 5 ops L5 #0 NEW "TableFlipException" @0 L5 #1 SEND_VAL_EX "ruh roh" 1 L5 #2 DO_FCALL L5 #3 THROW @0 L6 #4 RETURN null prompt> On a tangent, you can also get phpdbg to show you the actual opcodes for the context you're currently in. The table flipping function is a good one to look at, because you can see how this maps. Why is this important? Firstly, you can have phpdbg work by opcode instead of line. Secondly… well, you'll see.
Let's look at a more real example.

LawnGnome/confoo-tweeps

Here is a project called confoo-tweeps. It's on GitHub! It's written in Laravel! It uses façades! It must be good, right? Basically, this allows me to scrape the accounts and user names of people who tweet about #confoo because REASONS.
root@57d38b20b3cb:/src# php artisan tweeps:update Setting the name of LGnome to 'Adam Harvey'... Setting the name of adamculp to Adam Culp... Setting the name of afilina to Anna Filina... Setting the name of serialseb to SerialSeb 🇪🇺🏳️‍🌈... Setting the name of hannelita to Hanneli Tavante... Setting the name of confooca to ConFoo Conference... Setting the name of EliW to EliW... Setting the name of beausimensen to Beau D. Simensen... Setting the name of pjf to Paul Fenwick... Muahahahahaha! Obviously, this needs to monitor the search, so I've written an Artisan command that does just that. It looks like this. This is actual output, folks. Be afraid.
root@57d38b20b3cb:/src# php artisan tweeps:update Setting the name of LGnome to 'Adam Harvey'...
[Illuminate\Database\QueryException] SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for th e right syntax to use near '; --')' at line 1 (SQL: REP LACE INTO tweeps (account, name) VALUES ('LGnome', ''Ad am Harvey''))
However, then it started breaking. That's not good. Note that, while artisan actually outputs two exceptions here (I've omitted the second one for brevity), neither of them actually tell you where the exception occurred.
root@57d38b20b3cb:/src# phpdbg artisan [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /src/artisan] prompt> run tweeps:update Setting the name of LGnome to 'Adam Harvey'...
[Illuminate\Database\QueryException] ...
[Script ended normally] prompt> phpdbg to the rescue? Let's see. Well, that wasn't helpful. The problem is that the exception is being caught, so we still have no idea how to figure out where it's coming from. Aargh!
prompt> break ZEND_ADD prompt> b ZEND_ADD Break on any occurrence of the opcode ZEND_ADD Let's delve back into the breakpoint help. Conditional breakpoints were cool. Maybe there's something else. And lo and behold, we find this! Adds aren't cool, but you know what is cool?
#define ZEND_THROW                           108
(Sorry, I am contractually required to have at least one slide of C in each talk.) Remember those opcodes we saw a few slides back? There's a THROW opcode! ZEND_THROW is the opcode used when an exception is being thrown. So let's try breaking on that.
prompt> break ZEND_THROW [Breakpoint #0 added at ZEND_THROW] prompt> run tweeps:update Setting the name of LGnome to 'Adam Harvey'... [Breakpoint #0 in ZEND_THROW at /src/vendor/laravel/framework/src/Illuminate/Database/Connection.php:714, hits: 1] 00713: throw new QueryException( >00714: $query, $this->prepareBindings($bindings), $e 00715: ); 00716: } prompt> Success! We're now at the point the exception is thrown. (I've included the previous line just for context.) Unfortunately, it's pretty deep in Laravel. So let's now look at the last piece of the debugger puzzle: the stack trace.
prompt> help back Command: back Alias: t show trace Provide a formatted backtrace using the standard debug_backtrace() functionality. An optional unsigned integer argument specifying the maximum number of frames to be traced; if omitted then a complete backtrace is given. Here's the help for the back command, which prints the current stack trace.
prompt> back ... frame #5: App\Console\Commands\UpdateTweeps->updateTweep(account="LGnome", name="'Adam Harvey'") at /src/app/Console/Commands/UpdateTweeps.php:62 ... prompt> The stack trace is 16 frames deep at this point, but here's the first one that's not a function in a vendor directory. Now, we could just look at the code, but we can actually switch to that frame and examine its local variables with the frame command.
prompt> help frame Command: frame Alias: f switch to a frame The frame takes an optional integer argument. If omitted, then the current frame is displayed If specified then the current scope is set to the corresponding frame listed in a back trace. This can be used to allowing access to the variables in a higher stack frame than that currently being executed. Here's the frame command. In our case, we know that it's frame 5, so let's do that.
prompt> frame 5 [Switched to frame #5] >00062: DB::statement("REPLACE INTO tweeps (account, name) VALUES ('$account', '$name')"); 00063: } 00064: } prompt> Here's the updateTweep() method. Ah. That doesn't look like best practice.
prompt> ev $name 'Adam Harvey' prompt> ev $_ = "REPLACE INTO tweeps (account, name) VALUES ('$account', '$name')" REPLACE INTO tweeps (account, name) VALUES ('LGnome', ''Adam Harvey'') prompt> Let's look at the local $name and evaluate the string just to confirm our suspicions. (The awkward $_ is because of a limitation in phpdbg's command parser: a double quote straight after a command is assumed to be a parameter delimiter, and isn't passed to ev.) Little Bobby Tables teaches us once again that parameterised queries are a good thing.
All that's good, but most of us spend more time on web sites than CLI scripts. How can we deal with that?
$_SERVER = [
  'HTTP_HOST' => 'localhost',
  'HTTP_ACCEPT' => '...',
  ...
];
$_GET = [];
$_REQUEST = [];
$_POST = [];
$_COOKIE = [];
$_FILES = [];
chdir('public'); include 'index.php';
Source: http://phpdbg.com/docs/mocking-webserver The only viable approach today, and the one recommended by the phpdbg documentation is to fake the superglobals to look like a Web request.

LawnGnome/phpdbg-fake-request lawngnome/phpdbg-fake-request

For convenience's sake, I've bundled this approach into a script and accompanying library that you can install from Packagist. It's rough, and PHP 7 only, but it works.
Let's look at the web interface for the confoo tweeps application. It can't be as bad as the artisan command, right? Page one looks good. Lots of cool people there. Let's go to page two…
Hmmm. Let's zoom in…
Ah.
<tr>
<td>nefarioushax0r</td>
<td><script>alert("I PWNED U NOOB LOLX0RZ")</script></td>
</tr>
Here's an excerpt from the HTML. Well, there's your problem. But how's the script tag ending up in the HTML in the first place? phpdbg to the rescue!

http://127.0.0.1:9000/?page=2

Since this is a web page, we need to attack this differently. First, here's the URL in question. We can't just feed this to phpdbg without a wrapper, since this behaviour only occurs on the second page, which we can only get to with a get variable.
root@57d38b20b3cb:/src# phpdbg ./vendor/bin/fake-request GET / public/index.php -g page=2 [Welcome to phpdbg, the interactive PHP debugger, v0.5.0] To get help using phpdbg type "help" and press enter [Please report bugs to <http://bugs.php.net/report.php>] [Successful compilation of /src/vendor/lawngnome/phpdbg-fake-request/bin/fake-request] prompt> We'll start up the fake-request script. (The phpdbg is optional, since the shebang also includes phpdbg.) So far, so normal, but note that we've provided the request method, action, entry point and get variable on the command line.
prompt> break routes.php:15 [Pending breakpoint #0 added at routes.php:15] prompt> The question is where to stop execution. In this case, I've added a break point at the first line of the anonymous function that's the controller. Seems reasonable, since we know it's not the framework. It's pending because that file hasn't been loaded yet, but that's OK. phpdbg is smart enough to wait for it to be included.
prompt> run [Breakpoint #0 at /src/app/Http/routes.php:15, hits: 1] >00015: $tweeps = DB::table('tweeps')->paginate(15); 00016: ob_start(); 00017: prompt> OK, we're in. We can use the list command to examine the function, but I'm going to just show you the highlights on the next slide. (List is also probably the most awkward command in phpdbg. In practice, I'd probably just open it up in an editor tiled alongside.)
Route::get('/', function () {
    $tweeps = DB::table('tweeps')->paginate(15);
    ob_start();
?>
...
<?php foreach ($tweeps as $tweep): ?><tr>
	<td><?= $tweep->account ?></td>
	<td><?= $tweep->name ?></td>
</tr><?php endforeach ?>
...
<?php
    return ob_get_clean();
});
Here's a cut down version of the controller. (Mostly to keep it on one screen.) I can see that the only place we generate table rows is based on a loop variable called $tweep, and the second cell is always from $tweep->name. So let's add a conditional breakpoint looking for part of what was in the HTML.
prompt> break if strpos($tweep->name, 'alert') [Conditional breakpoint #1 added strpos($tweep->name, 'alert')/0x7f46430a8460] prompt> continue [Conditional breakpoint #1: on strpos($tweep->name, 'alert') == true at /src/app/Http/routes.php:46, hits: 1] >00046: <td><?= $tweep->account ?></td> 00047: <td><?= $tweep->name ?></td> 00048: </tr> prompt> You may remember that I said earlier that conditional breakpoints would accept any PHP expression. Let's use that here with strpos. Add the breakpoint, continue execution, and… well, that seems pretty damning. We're just echoing the values straight out.
prompt> ev $tweep stdClass Object ( [account] => nefarioushax0r [name] => <script>alert("I PWNED U NOOB LOLX0RZ")</script> ) prompt> Just to be sure, here's the $tweep variable. That name is definitely going to cause problems. I guess we should use a templating engine. Or at least call htmlspecialchars(), since PHP is a templating engine. Now, there are limits to this approach. You can't inject arbitrary, non-form post data, as you can't override the php://input stream. You'd have to modify your front controller to accept post data from somewhere else. And you're reliant on how good the emulation is. But that's why XDebug exists.
You can also use phpdbg as a driver for PHPUnit's code coverage. This has the twin advantages that you can get code coverage on new versions of PHP if XDebug doesn't yet support them, and also that phpdbg is considerably quicker.
root@de345107bc84:/src# phpdbg -qrr vendor/bin/phpunit --color --coverage-html coverage PHPUnit 5.3.4 by Sebastian Bergmann and contributors. ....................... 23 / 23 (100%) Time: 1.45 seconds, Memory: 6.00MB OK (23 tests, 29 assertions) Generating code coverage report in HTML format ... done Assuming you have a normal Composer-based setup, you can get code coverage by simply running phpunit under phpdbg with the right switches or configuration. -qrr hides the banner, runs immediately, and then quits. Here I'm running the test suite for my phpdbg request faker.
And there are the results. Good job! Particularly on excluding the command directory which doesn't have integration tests yet…
When I wrote the abstract for this talk, I mentioned IDE support, having heard of plans to implement phpdbg support in various IDEs. Unfortunately, as of today, there is no IDE I know of that implements support for phpdbg's remote protocol. PHPStorm has it on their roadmap, but without a version. NetBeans has an untriaged feature request. (Conclude on CLI, still useful, etc.)
Of course, phpdbg also isn't the only game in town. There's XDebug. And if you have complex Web needs beyond what you can fake, it's still the best choice. But phpdbg is lightweight, easy if you're comfortable with the command line (and you should be), and much easier to set up. Consider it for those simple tasks!

Thank you!

Questions?

@LGnome

Slides: https://lawngnome.github.io/phpdbg-for-fun-and-profit/

Image credits

phpdbg for fun and profit Adam Harvey @LGnome