1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-14 18:55:56 +02:00

Add new static Debug::backtrace() method to the Debug class. This returns a backtrace array that is simpler and more PW-specific than PHP's version. By default it excludes likely irrelevant (for most) hook-related method calls that usually fill up the backtrace.

This commit is contained in:
Ryan Cramer
2019-07-26 11:41:05 -04:00
parent 072536dc72
commit ddb4aebf60
3 changed files with 235 additions and 4 deletions

View File

@@ -154,4 +154,234 @@ class Debug {
self::$timers = array();
}
/**
* Return a backtrace array that is simpler and more PW-specific relative to PHPs debug_backtrace
*
* @param array $options
* @return array|string
* @since 3.0.136
*
*/
static public function backtrace(array $options = array()) {
$defaults = array(
'limit' => 0, // the limit argument for the debug_backtrace call
'flags' => DEBUG_BACKTRACE_PROVIDE_OBJECT, // flags for PHP debug_backtrace method
'showHooks' => false, // show internal methods for hook calls?
'getString' => false, // get newline separated string rather than array?
'maxCount' => 10, // max size for arrays
'maxStrlen' => 100, // max length for strings
'maxDepth' => 5, // max allowed recursion depth when converting variables to strings
'ellipsis' => ' …', // show this ellipsis when a long value is truncated
'skipCalls' => array(), // method/function calls to skip
);
$options = array_merge($defaults, $options);
if($options['limit']) $options['limit']++;
$traces = @debug_backtrace($options['flags'], $options['limit']);
$config = wire('config');
$rootPath = ProcessWire::getRootPath(true);
$rootPath2 = $config && $config->paths ? $config->paths->root : $rootPath;
array_shift($traces); // shift of the simpleBacktrace call, which is not needed
$apiVars = array();
$result = array();
$cnt = 0;
foreach(wire('all') as $name => $value) {
if(!is_object($value)) continue;
$apiVars[wireClassName($value)] = '$' . $name;
}
foreach($traces as $n => $trace) {
if(!is_array($trace) || !isset($trace['function']) || !isset($trace['file'])) {
continue;
} else if(count($options['skipCalls']) && in_array($trace['function'], $options['skipCalls'])) {
continue;
}
$obj = null;
$class = '';
$type = '';
$args = $trace['args'];
$argStr = '';
$file = $trace['file'];
$basename = basename($file);
$function = $trace['function'];
$isHookableCall = false;
if(isset($trace['object'])) {
$obj = $trace['object'];
$class = wireClassName($obj);
} else if(isset($trace['class'])) {
$class = wireClassName($trace['class']);
}
if($class) {
$type = isset($trace['type']) ? $trace['type'] : '.';
}
if(!$options['showHooks']) {
if($basename === 'Wire.php' && !wireMethodExists('Wire', $function)) continue;
if($class === 'WireHooks' || $basename === 'WireHooks.php') continue;
}
if(strpos($function, '___') === 0) {
$isHookableCall = '___';
} else if($obj && !method_exists($obj, $function) && method_exists($obj, "___$function")) {
$isHookableCall = true;
}
if($type === '->' && isset($apiVars[$class])) {
// use API var name when available
if(strtolower($class) === strtolower(ltrim($apiVars[$class], '$'))) {
$class = $apiVars[$class];
} else {
$class = "$class " . $apiVars[$class];
}
}
if($basename === 'Wire.php' && $class !== 'Wire') {
$ref = new \ReflectionClass($trace['class']);
$file = $ref->getFileName();
}
// rootPath and rootPath2 can be different if one of them represented by a symlink
$file = str_replace($rootPath, '/', $file);
if($rootPath2 !== $rootPath) $file = str_replace($rootPath2, '/', $file);
if(($function === '__call' || $function == '_callMethod') && count($args)) {
$function = array_shift($args);
}
if(!$options['showHooks'] && $isHookableCall === '___') {
$function = substr($function, 3);
}
if(!empty($args)) {
$newArgs = array();
if($isHookableCall && count($args) === 1 && is_array($args[0])) {
$newArgs = $args[0];
}
foreach($args as $arg) {
if(is_object($arg)) {
$arg = wireClassName($arg) . ' $obj';
} else if(is_array($arg)) {
$count = count($arg);
if($count < 4) {
$arg = $count ? self::toStr($arg, array('maxDepth' => 2)) : '[]';
} else {
$arg = 'array(' . count($arg) . ')';
}
} else if(is_string($arg)) {
if(strlen("$arg") > $options['maxStrlen']) $arg = substr($arg, 0, $options['maxStrlen']) . ' …';
$arg = '"' . $arg . '"';
} else if(is_bool($arg)) {
$arg = $arg ? 'true' : 'false';
} else {
// leave as-is
}
$newArgs[] = $arg;
}
$argStr = implode(', ', $newArgs);
if($argStr === '[]') $argStr = '';
}
$call = "$class$type$function($argStr)";
$file = "$file:$trace[line]";
if($options['getString']) {
$result[] = "$cnt. $file » $call";
} else {
$result[] = array(
'file' => $file,
'call' => $call,
);
}
$cnt++;
}
if($options['getString']) $result = implode("\n", $result);
return $result;
}
/**
* Convert value to string for backtrace method
*
* @param $value
* @param array $options
* @return null|string
*
*/
static protected function toStr($value, array $options = array()) {
$defaults = array(
'maxCount' => 10, // max size for arrays
'maxStrlen' => 100, // max length for strings
'maxDepth' => 5,
'ellipsis' => ' …'
);
static $depth = 0;
$options = count($options) ? array_merge($defaults, $options) : $defaults;
$depth++;
if(is_object($value)) {
// object
$str = wireClassName($value);
if($str === 'HookEvent') {
$str .= ' $event';
} else if(method_exists($value, '__toString')) {
$value = (string) $value;
if($value !== $str) {
if(strlen($value) > $options['maxStrlen']) {
$value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis'];
}
$str .= "($value)";
}
}
} else if(is_array($value)) {
// array
if(empty($value)) {
$str = '[]';
} else if($depth >= $options['maxDepth']) {
$str = "array(" . count($value) . ")";
} else {
$suffix = '';
if(count($value) > $options['maxCount']) {
$value = array_slice($value, 0, $options['maxCount']);
$suffix = $options['ellipsis'];
}
foreach($value as $k => $v) {
$value[$k] = self::toStr($v, $options);
}
$str = '[ ' . implode(', ', $value) . $suffix . ' ]';
}
} else if(is_string($value)) {
// string
if(strlen($value) > $options['maxStrlen']) {
$value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis'];
}
$hasDQ = strpos($value, '"') !== false;
$hasSQ = strpos($value, "'") !== false;
if(($hasDQ && $hasSQ) || $hasSQ) {
$value = str_replace('"', '\\"', $value);
$str = '"' . $value . '"';
} else {
$str = "'$value'";
}
} else if(is_bool($value)) {
// true or false
$str = $value ? 'true' : 'false';
} else {
// int, float or other
$str = $value;
}
$depth--;
return $str;
}
}

View File

@@ -90,7 +90,7 @@ abstract class Notice extends WireData {
}
public function set($key, $value) {
if($key === 'text' && strpos($value, 'icon-') === 0 && strpos($value, ' ')) {
if($key === 'text' && is_string($value) && strpos($value, 'icon-') === 0 && strpos($value, ' ')) {
list($icon, $value) = explode(' ', $value, 2);
list(,$icon) = explode('-', $icon, 2);
$icon = $this->wire('sanitizer')->name($icon);

View File

@@ -760,12 +760,13 @@ class ProcessWire extends Wire {
* Get root path, check it, and optionally auto-detect it if not provided
*
* @param bool|string $rootPath Root path if already known, in which case well just modify as needed
* …or specify boolean true to get absolute root path, which disregards any symbolic links to core.
* @return string
*
*/
protected static function getRootPath($rootPath = '') {
public static function getRootPath($rootPath = '') {
if(strpos($rootPath, '..') !== false) {
if($rootPath !== true && strpos($rootPath, '..') !== false) {
$rootPath = realpath($rootPath);
}
@@ -777,7 +778,7 @@ class ProcessWire extends Wire {
if(!file_exists($rootPath . 'wire/core/ProcessWire.php')) $rootPath = '';
}
if(empty($rootPath)) {
if(empty($rootPath) || $rootPath === true) {
// if unable to determine from script filename, attempt to determine from current file
$parts = explode(DIRECTORY_SEPARATOR, __FILE__);
$parts = array_slice($parts, 0, -3); // removes "ProcessWire.php", "core" and "wire"