How The PHP Stack Frame Works
Introduction
The PHP stack frame isn't as straight-forward as you might think. This caught us out when building FilterBacktrace.
What's In A Stack Frame
Here's a stack dump generated by calling debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS):
array(11) {
[0] =>
array(5) {
'file' =>
string(98) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/tests/V1/Exceptions/BadRequirementDataTest.php"
'line' =>
int(142)
'function' =>
string(22) "newFromRequirementData"
'class' =>
string(57) "GanbaroDigital\Defensive\V1\Exceptions\BadRequirementData"
'type' =>
string(2) "::"
}
[1] =>
array(3) {
'function' =>
string(35) "testCanCreateFromBadRequirementData"
'class' =>
string(65) "GanbaroDigitalTest\Defensive\V1\Exceptions\BadRequirementDataTest"
'type' =>
string(2) "->"
}
[2] =>
array(5) {
'file' =>
string(101) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/Framework/TestCase.php"
'line' =>
int(909)
'function' =>
string(10) "invokeArgs"
'class' =>
string(16) "ReflectionMethod"
'type' =>
string(2) "->"
}
[3] =>
array(5) {
'file' =>
string(101) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/Framework/TestCase.php"
'line' =>
int(768)
'function' =>
string(7) "runTest"
'class' =>
string(26) "PHPUnit_Framework_TestCase"
'type' =>
string(2) "->"
}
[4] =>
array(5) {
'file' =>
string(103) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/Framework/TestResult.php"
'line' =>
int(612)
'function' =>
string(7) "runBare"
'class' =>
string(26) "PHPUnit_Framework_TestCase"
'type' =>
string(2) "->"
}
[5] =>
array(5) {
'file' =>
string(101) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/Framework/TestCase.php"
'line' =>
int(724)
'function' =>
string(3) "run"
'class' =>
string(28) "PHPUnit_Framework_TestResult"
'type' =>
string(2) "->"
}
[6] =>
array(5) {
'file' =>
string(102) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/Framework/TestSuite.php"
'line' =>
int(747)
'function' =>
string(3) "run"
'class' =>
string(26) "PHPUnit_Framework_TestCase"
'type' =>
string(2) "->"
}
[7] =>
array(5) {
'file' =>
string(100) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/TestRunner.php"
'line' =>
int(440)
'function' =>
string(3) "run"
'class' =>
string(27) "PHPUnit_Framework_TestSuite"
'type' =>
string(2) "->"
}
[8] =>
array(5) {
'file' =>
string(97) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/Command.php"
'line' =>
int(149)
'function' =>
string(5) "doRun"
'class' =>
string(25) "PHPUnit_TextUI_TestRunner"
'type' =>
string(2) "->"
}
[9] =>
array(5) {
'file' =>
string(97) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/Command.php"
'line' =>
int(100)
'function' =>
string(3) "run"
'class' =>
string(22) "PHPUnit_TextUI_Command"
'type' =>
string(2) "->"
}
[10] =>
array(5) {
'file' =>
string(82) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/phpunit"
'line' =>
int(47)
'function' =>
string(4) "main"
'class' =>
string(22) "PHPUnit_TextUI_Command"
'type' =>
string(2) "::"
}
}
Each stack frame can contain any of these details:
| Detail | Description |
|---|---|
| file | A PHP file that has been executed |
| line | The line number in 'file' that is being executed |
| class | A class that is being called |
| function | The method on 'class' that is being called (if 'class' has a value) |
| function | A global function that is being called (if 'class' has no value or is not set) |
| type | How 'function' was called (:: for a static call, -> for a call on an object, not set if 'class' is not set) |
At first glance, that looks very straight-forward. Let's isolate a single stack frame:
| Detail | Value |
|---|---|
| file | /Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/TestRunner.php |
| line | 440 |
| class | PHPUnit_Framework_TestSuite |
| function | run |
Hang on a moment. According to that, line 440 of TestRunner.php is part of the run() method of PHPUnit_Framework_TestSuite. Surely that's a typo?
No, it isn't. Here's the stack frame that I built that table from:
[7] =>
array(5) {
'file' =>
string(100) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/TestRunner.php"
'line' =>
int(440)
'function' =>
string(3) "run"
'class' =>
string(27) "PHPUnit_Framework_TestSuite"
'type' =>
string(2) "->"
}
What's going on?
Stack Frames Contain Staggered Data
In any PHP stack frame, file and line correctly refer to the code that is executing. class, function and type (if present) refer to the code that file and line is calling - and not the code in file and line line as you might be expecting.
You can clearly see that in the following two stack frames:
[7] =>
array(5) {
'file' =>
string(100) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/TestRunner.php"
'line' =>
int(440)
'function' =>
string(3) "run"
'class' =>
string(27) "PHPUnit_Framework_TestSuite"
'type' =>
string(2) "->"
}
[8] =>
array(5) {
'file' =>
string(97) "/Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/Command.php"
'line' =>
int(149)
'function' =>
string(5) "doRun"
'class' =>
string(25) "PHPUnit_TextUI_TestRunner"
'type' =>
string(2) "->"
}
How To Work Out Actual Caller Details
To work out a full set of caller details, do the following:
- take the
class,functionandtypevalues from a stack frame - take the
fileandlinevalues from the preceeding stack frame
In our example above, if you combine class, function and type from stack frame 8 with file and line from stack frame 7, you get this:
| Detail | Value |
|---|---|
| file | /Users/stuart/Devel/ganbarodigital/php-mv-defensive/vendor/phpunit/phpunit/src/TextUI/TestRunner.php |
| line | 440 |
| class | PHPUnit_TextUI_TestRunner |
| function | doRun |
| type | -> |
... which looks far more credible :)
Automatically Handled For You
Our FilterBacktrace class automatically handles this for you. All of our other classes in the TraceInspectors namespace use FilterBacktrace to understand the PHP stack.
As long as you use our classes, you won't have to worry about this PHP behaviour in your own code.