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:
@@ -154,4 +154,234 @@ class Debug {
|
||||
self::$timers = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a backtrace array that is simpler and more PW-specific relative to PHP’s 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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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 we’ll 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"
|
||||
|
Reference in New Issue
Block a user