Trying to debug PHP using its default current-line-only error messages is horrible. How can I get PHP to produce a backtrace (stack trace) when errors are produced?
Trying to debug PHP using its default current-line-only error messages is horrible. How can I get PHP to produce a backtrace (stack trace) when errors are produced?
My script for installing an error handler that produces a backtrace:
<?php
function process_error_backtrace($errno, $errstr, $errfile, $errline, $errcontext) {
if(!(error_reporting() & $errno))
return;
switch($errno) {
case E_WARNING :
case E_USER_WARNING :
case E_STRICT :
case E_NOTICE :
case E_USER_NOTICE :
$type = 'warning';
$fatal = false;
break;
default :
$type = 'fatal error';
$fatal = true;
break;
}
$trace = array_reverse(debug_backtrace());
array_pop($trace);
if(php_sapi_name() == 'cli') {
echo 'Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ':' . "\n";
foreach($trace as $item)
echo ' ' . (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()' . "\n";
} else {
echo '<p class="error_backtrace">' . "\n";
echo ' Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ':' . "\n";
echo ' <ol>' . "\n";
foreach($trace as $item)
echo ' <li>' . (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()</li>' . "\n";
echo ' </ol>' . "\n";
echo '</p>' . "\n";
}
if(ini_get('log_errors')) {
$items = array();
foreach($trace as $item)
$items[] = (isset($item['file']) ? $item['file'] : '<unknown file>') . ' ' . (isset($item['line']) ? $item['line'] : '<unknown line>') . ' calling ' . $item['function'] . '()';
$message = 'Backtrace from ' . $type . ' \'' . $errstr . '\' at ' . $errfile . ' ' . $errline . ': ' . join(' | ', $items);
error_log($message);
}
if($fatal)
exit(1);
}
set_error_handler('process_error_backtrace');
?>
Caveat: it is powerless to affect various 'PHP Fatal Errors', since Zend in their wisdom decided that these would ignore set_error_handler(). So you still get useless final-location-only errors with those.
Xdebug prints a backtrace table on errors, and you don't have to write any PHP code to implement it.
Downside is you have to install it as a PHP extension.
As php debug extensions, there is Xdebug and PHP DBG. Each one has its advantages and disadvantages.
If you can't install a debugger then use this function sorrounding the fatal error to get the "fatal stack". Check the code and example below that explains better how to use it:
// Give an extra parameter to the filename
// to save multiple log files
function _fatalog_($extra = false)
{
static $last_extra;
// CHANGE THIS TO: A writeable filepath in your system...
$filepath = '/var/www/html/sites/default/files/fatal-'.($extra === false ? $last_extra : $extra).'.log';
if ($extra===false) {
unlink($filepath);
} else {
// we write a log file with the debug info
file_put_contents($filepath, json_encode(debug_backtrace()));
// saving last extra parameter for future unlink... if possible...
$last_extra = $extra;
}
}
Here's an example of how to use it:
// A function which will produce a fatal error
function fatal_example()
{
_fatalog_(time()); // writing the log
$some_fatal_code = array()/3; // fatality!
_fatalog_(); // if we get here then delete last file log
}
Finally to read the contents of the log...
var_dump(json_decode(file_get_contents('/path/to-the-fatal.log')));
Hope that helps!
You can use (new \Exception())->getTraceAsString() instead of debug_backtrace():
set_error_handler(function(int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false;
}
$severities = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE_ERROR',
E_CORE_WARNING => 'CORE_WARNING',
E_COMPILE_ERROR => 'COMPILE_ERROR',
E_COMPILE_WARNING => 'COMPILE_WARNING',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_STRICT => 'STRICT',
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER_DEPRECATED',
];
$e = new \Exception;
// Just remove the current point from the trace
$reflection = new \ReflectionProperty(get_class($e), 'trace');
$reflection->setAccessible(true);
$trace = $reflection->getValue($e);
array_shift($trace);
$reflection->setValue($e, $trace);
$text = '';
$text .= ($severities[$severity] ?? $severity) . ': ';
$text .= "$message in $file($line)\n";
$text .= "Stack trace:\n";
$text .= $e->getTraceAsString();
error_log($text);
if (in_array($severity, [
E_CORE_ERROR,
E_ERROR,
E_RECOVERABLE_ERROR,
E_PARSE,
E_COMPILE_ERROR,
E_USER_ERROR,
], true)) {
http_response_code(500);
exit(255);
}
return true;
});
Output:
[16-Feb-2022 00:01:09 UTC] DEPRECATED: trim(): Passing null to parameter #1 ($string) of type string is deprecated in /home/user/project/doSomething.php(3)
Stack trace:
#0 /home/user/project/doSomething.php(3): trim(NULL)
#1 /home/user/project/index.php(4): doSomething()
#2 {main}