-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
312 additions
and
323 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
* @package Backtrace | ||
* @author Brad Kent <[email protected]> | ||
* @license http://opensource.org/licenses/MIT MIT | ||
* @copyright 2020-2022 Brad Kent | ||
* @copyright 2020-2023 Brad Kent | ||
* @version v2.1 | ||
* @link http://www.github.com/bkdotcom/Backtrace | ||
*/ | ||
|
@@ -13,6 +13,8 @@ | |
|
||
use bdk\Backtrace\Normalizer; | ||
use bdk\Backtrace\SkipInternal; | ||
use Exception; | ||
use ParseError; | ||
|
||
/** | ||
* Utility for getting backtrace | ||
|
@@ -28,7 +30,7 @@ class Backtrace | |
const INCL_OBJECT = 2; | ||
|
||
/** | ||
* add a new namespace or classname to be used to determine when to | ||
* Add a new namespace or classname to be used to determine when to | ||
* stop iterrating over the backtrace when determining calling info | ||
* | ||
* @param array|string $classes classname(s) | ||
|
@@ -45,8 +47,9 @@ public static function addInternalClass($classes, $level = 0) | |
/** | ||
* Helper method to get backtrace | ||
* | ||
* Uses passed exception, debug_backtrace, or xdebug_get_function_stack | ||
* | ||
* Utilizes `xdebug_get_function_stack()` (if available) to get backtrace in shutdown phase | ||
* When called internally, internal frames are removed | ||
* | ||
* @param int|null $options bitmask of options | ||
* @param int $limit limit the number of stack frames returned. | ||
|
@@ -56,16 +59,24 @@ public static function addInternalClass($classes, $level = 0) | |
*/ | ||
public static function get($options = 0, $limit = 0, $exception = null) | ||
{ | ||
$options = $options ?: 0; | ||
$backtrace = static::getBacktrace($options, $limit, $exception); | ||
if (empty($backtrace)) { | ||
return array(); | ||
$limit = $limit ?: null; | ||
if ($exception) { | ||
$backtrace = self::getExceptionTrace($exception); | ||
return \array_slice($backtrace, 0, $limit); | ||
} | ||
// don't incl args passed to trace() | ||
$backtrace[0]['args'] = array(); | ||
$options = self::translateOptions($options); | ||
$backtrace = \debug_backtrace($options, $limit ? $limit + 2 : 0); | ||
if (\array_key_exists('file', \end($backtrace)) === false) { | ||
// We're in shutdown | ||
$backtrace = static::xdebugGetFunctionStack() ?: array(); | ||
$backtrace = \array_reverse($backtrace); | ||
} | ||
$backtrace = Normalizer::normalize($backtrace); | ||
$backtrace = SkipInternal::removeInternalFrames($backtrace); | ||
// keep the calling file & line, but toss the called function (what initiated trace) | ||
unset($backtrace[0]['function']); | ||
return $backtrace; | ||
unset($backtrace[\count($backtrace) - 1]['function']); // remove "{main}" | ||
return \array_slice($backtrace, 0, $limit); | ||
} | ||
|
||
/** | ||
|
@@ -95,7 +106,9 @@ public static function getCallerInfo($offset = 0, $options = 0) | |
*/ | ||
$phpOptions = static::translateOptions($options | self::INCL_OBJECT); | ||
$backtrace = \debug_backtrace($phpOptions, 28); | ||
$backtrace = Normalizer::normalize($backtrace); | ||
$index = SkipInternal::getFirstIndex($backtrace, $offset); | ||
$index = \max($index, 1); // insure we're >= 1 | ||
$return = static::callerInfoBuild(\array_slice($backtrace, $index)); | ||
if (!($options & self::INCL_OBJECT)) { | ||
unset($return['object']); | ||
|
@@ -104,9 +117,8 @@ public static function getCallerInfo($offset = 0, $options = 0) | |
} | ||
|
||
/** | ||
* Add lines surrounding frame line to each frame | ||
* Add context (code snippet) to each frame | ||
* | ||
* Adds a `context` value to each backtrace frame | ||
* context is an array of `lineNumber => line` | ||
* | ||
* @param array $backtrace backtrace frames | ||
|
@@ -136,7 +148,7 @@ public static function addContext($backtrace, $length = 19) | |
* Returns array of lineNumber => line | ||
* | ||
* @param string $file filepath | ||
* @param int $start line to start on (1-indexed; 1 = line; 1 = first line) | ||
* @param int $start line to start on (1-indexed; 1 = first line) | ||
* 0 also = first line | ||
* @param int $length number of lines to return | ||
* | ||
|
@@ -150,7 +162,7 @@ public static function getFileLines($file, $start = 1, $length = null) | |
return false; | ||
} | ||
$lines = \array_merge(array(null), \file($file)); | ||
if ($start === 0) { | ||
if ($start <= 0) { | ||
$start = 1; | ||
} | ||
if ($start > 1 || $length) { | ||
|
@@ -180,6 +192,46 @@ public static function isXdebugFuncStackAvail() | |
return true; | ||
} | ||
|
||
/** | ||
* Wrapper for xdebug_get_function_stack | ||
* accounts for bug 1529 (may report incorrect file) | ||
* | ||
* xdebug.collect_params ini must be set prior to running code to be backtraced for params (args) to be collected | ||
* | ||
* @return array|false | ||
* | ||
* @see https://bugs.xdebug.org/view.php?id=695 | ||
* @see https://bugs.xdebug.org/view.php?id=1529 | ||
* @see https://xdebug.org/docs/all_settings#xdebug.collect_params | ||
*/ | ||
public static function xdebugGetFunctionStack() | ||
{ | ||
if (static::isXdebugFuncStackAvail() === false) { | ||
return false; | ||
} | ||
$stack = \xdebug_get_function_stack(); | ||
$xdebugVer = \phpversion('xdebug'); | ||
if (\version_compare($xdebugVer, '2.6.0', '<')) { | ||
$stack = static::xdebugFix($stack); | ||
} | ||
$error = \error_get_last(); | ||
if ($error !== null && $error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR)) { | ||
// xdebug_get_function_stack doesn't include the frame that triggered the error! | ||
$errorFileLine = array( | ||
'file' => $error['file'], | ||
'line' => $error['line'], | ||
); | ||
$lastFrame = \end($stack); | ||
if (\array_intersect_assoc($errorFileLine, $lastFrame) !== $errorFileLine) { | ||
\array_push($stack, $errorFileLine); | ||
} | ||
} | ||
return \array_map(static function ($frame) { | ||
\ksort($frame); | ||
return $frame; | ||
}, $stack); | ||
} | ||
|
||
/** | ||
* Build callerInfo array from given backtrace segment | ||
* | ||
|
@@ -190,31 +242,39 @@ public static function isXdebugFuncStackAvail() | |
private static function callerInfoBuild($backtrace) | ||
{ | ||
$return = array( | ||
'args' => array(), | ||
'class' => null, // where the method is defined | ||
'classCalled' => null, // parent::method()... this will be the parent class | ||
'classContext' => null, // child->method() | ||
'evalLine' => null, | ||
'file' => null, | ||
'function' => null, | ||
'line' => null, | ||
'type' => null, | ||
); | ||
$numFrames = \count($backtrace); | ||
$iFileLine = 0; | ||
$iFunc = 1; | ||
if (isset($backtrace[$iFunc])) { | ||
$return = \array_merge($return, $backtrace[$iFunc]); | ||
$return = \array_merge( | ||
$return, | ||
$backtrace[$iFunc], | ||
\preg_match('/^(?<class>\S+)(?<type>::|->)(?<method>\S+)$/', $backtrace[$iFunc]['function'], $matches) | ||
? array( | ||
'class' => $matches['class'], | ||
'function' => $matches['method'], | ||
'type' => $matches['type'], | ||
) | ||
: array() | ||
); | ||
$return['classCalled'] = $return['class']; | ||
} | ||
if ($return['type'] === '->') { | ||
$return['classContext'] = \get_class($backtrace[$iFunc]['object']); | ||
$return = self::callerInfoClassCalled($return); | ||
} | ||
if (isset($backtrace[$iFileLine])) { | ||
$return['file'] = $backtrace[$iFileLine]['file']; | ||
$return['line'] = $backtrace[$iFileLine]['line']; | ||
} elseif (isset($backtrace[$numFrames - 1])) { | ||
$return['file'] = $backtrace[$numFrames - 1]['file']; | ||
$return['line'] = 0; | ||
} | ||
if ($return['type'] === '->') { | ||
$return['classContext'] = \get_class($backtrace[$iFunc]['object']); | ||
$return = self::callerInfoClassCalled($return); | ||
} | ||
return $return; | ||
} | ||
|
@@ -232,92 +292,40 @@ private static function callerInfoClassCalled($info) | |
// class : classname of parent (or where method defined) | ||
// object : scope / context | ||
$info['classCalled'] = $info['classContext']; | ||
if ($info['function'] === '{closure}') { | ||
return $info; | ||
} | ||
$classDeclared = null; | ||
if ($info['classContext'] !== $info['class']) { | ||
if ($info['function'] === '{closure}') { | ||
return $info; | ||
} | ||
$reflector = new \ReflectionMethod($info['classContext'], $info['function']); | ||
$classDeclared = $reflector->getDeclaringClass()->getName(); | ||
if ($classDeclared === $info['classContext']) { | ||
// method is (re)declared in classContext, yet that's not what's being executed | ||
// we must have called parent::method() | ||
$info['classCalled'] = $info['class']; | ||
} | ||
} | ||
if ($classDeclared === $info['classContext']) { | ||
// method is (re)declared in classContext, yet that's not what's being executed | ||
// we must have called parent::method() | ||
$info['classCalled'] = $info['class']; | ||
} | ||
return $info; | ||
} | ||
|
||
/** | ||
* Get backtrace from either passed exception, | ||
* debug_backtrace or xdebug_get_function_stack | ||
* Get trace from exception | ||
* | ||
* @param int $options options bitmask | ||
* @param int $limit limit the number of stack frames returned. | ||
* @param \Exception|\Throwable $exception (optional) Exception from which to get backtrace | ||
* @param Exception|Throwable $exception Exception instance | ||
* | ||
* @return array|false | ||
* @return array | ||
*/ | ||
private static function getBacktrace($options = 0, $limit = 0, $exception = null) | ||
private static function getExceptionTrace($exception) | ||
{ | ||
if ($exception instanceof \ParseError) { | ||
if ($exception instanceof ParseError) { | ||
return array(); | ||
} | ||
$limit = $limit ?: null; | ||
if ($exception) { | ||
$backtrace = $exception->getTrace(); | ||
\array_unshift($backtrace, array( | ||
'file' => $exception->getFile(), | ||
'line' => $exception->getLine(), | ||
)); | ||
$backtrace = Normalizer::normalize($backtrace); | ||
return \array_slice($backtrace, 0, $limit); | ||
} | ||
$options = static::translateOptions($options); | ||
$backtrace = \debug_backtrace($options, $limit ? $limit + 2 : 0); | ||
if (\array_key_exists('file', \end($backtrace)) === true) { | ||
// We're NOT in shutdown | ||
$backtrace = Normalizer::normalize($backtrace); | ||
$backtrace = SkipInternal::removeInternalFrames($backtrace); | ||
return \array_slice($backtrace, 0, $limit); | ||
} | ||
/* | ||
We appear to be in shutdown - use xdebug | ||
*/ | ||
return static::getBacktraceXdebug($limit); | ||
} | ||
|
||
/** | ||
* Get backtrace via xdebug | ||
* | ||
* @param int $limit limit the number of stack frames returned. | ||
* | ||
* @return array|false | ||
*/ | ||
private static function getBacktraceXdebug($limit) | ||
{ | ||
$backtrace = static::xdebugGetFunctionStack(); | ||
if ($backtrace === false) { | ||
return false; | ||
} | ||
$backtrace = \array_reverse($backtrace); | ||
$backtrace = Normalizer::normalize($backtrace); | ||
$backtrace = SkipInternal::removeInternalFrames($backtrace); | ||
$backtrace = \array_slice($backtrace, 0, $limit ?: null); | ||
$error = \error_get_last(); | ||
if ($error !== null && $error['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR)) { | ||
// xdebug_get_function_stack doesn't include the frame that triggered the error! | ||
$errorFileLine = array( | ||
'file' => $error['file'], | ||
'line' => $error['line'], | ||
); | ||
if (\array_intersect_assoc($errorFileLine, $backtrace[0]) !== $errorFileLine) { | ||
\array_unshift($backtrace, $errorFileLine); | ||
} | ||
} | ||
\end($backtrace); | ||
$key = \key($backtrace); | ||
unset($backtrace[$key]['function']); // remove "{main}" | ||
return $backtrace; | ||
$backtrace = $exception->getTrace(); | ||
\array_unshift($backtrace, array( | ||
'file' => $exception->getFile(), | ||
'line' => $exception->getLine(), | ||
)); | ||
return Normalizer::normalize($backtrace); | ||
} | ||
|
||
/** | ||
|
@@ -329,6 +337,7 @@ private static function getBacktraceXdebug($limit) | |
*/ | ||
private static function translateOptions($options) | ||
{ | ||
$options = $options ?: 0; | ||
$phpOptions = DEBUG_BACKTRACE_IGNORE_ARGS; | ||
if ($options & self::INCL_ARGS) { | ||
$phpOptions &= ~DEBUG_BACKTRACE_IGNORE_ARGS; | ||
|
@@ -340,33 +349,24 @@ private static function translateOptions($options) | |
} | ||
|
||
/** | ||
* wrapper for xdebug_get_function_stack | ||
* accounts for bug 1529 (may report incorrect file) | ||
* Fix xdebug bugs | ||
* | ||
* xdebug.collect_params ini must be set prior to running code to be backtraced for params (args) to be collected | ||
* https://bugs.xdebug.org/view.php?id=695 - doesn't set the call type key | ||
* https://bugs.xdebug.org/view.php?id=1529 - __get : wrong file | ||
* | ||
* @return array|false | ||
* @see https://bugs.xdebug.org/view.php?id=695 | ||
* @see https://bugs.xdebug.org/view.php?id=1529 | ||
* @see https://xdebug.org/docs/all_settings#xdebug.collect_params | ||
* @param array $stack xdebug stack | ||
* | ||
* @return array | ||
*/ | ||
private static function xdebugGetFunctionStack() | ||
private static function xdebugFix(array $stack) | ||
{ | ||
if (static::isXdebugFuncStackAvail() === false) { | ||
return false; | ||
} | ||
$stack = \xdebug_get_function_stack(); | ||
$xdebugVer = \phpversion('xdebug'); | ||
if (\version_compare($xdebugVer, '2.6.0', '>=')) { | ||
return $stack; | ||
} | ||
$count = \count($stack); | ||
for ($i = 0; $i < $count; $i++) { | ||
$frame = \array_merge(array( | ||
'function' => null, | ||
), $stack[$i]); | ||
if (!isset($frame['type']) && isset($frame['class'])) { | ||
// XDebug pre 2.1.1 doesn't set the call type key http://bugs.xdebug.org/view.php?id=695 | ||
// XDebug pre 2.1.1 doesn't set the call type key https://bugs.xdebug.org/view.php?id=695 | ||
$stack[$i]['type'] = 'static'; | ||
} | ||
if ($frame['function'] !== '__get') { | ||
|
Oops, something went wrong.