phpdbg for fun and profit
Adam Harvey
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.
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.
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…
<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!
phpdbg for fun and profit
Adam Harvey
@LGnome