Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Created August 11, 2020 18:43
Show Gist options
  • Save kmuenkel/c6ed7fbcb226c361bb142efe299939cb to your computer and use it in GitHub Desktop.
Save kmuenkel/c6ed7fbcb226c361bb142efe299939cb to your computer and use it in GitHub Desktop.
<?php
namespace App\Helpers;
use Exception;
use ReflectionMethod;
use ReflectionFunction;
use ReflectionException;
/**
* Class DebugTrace
* @package App\Helpers
*/
class DebugTrace
{
const TRUNCATE_AT = 16;
/**
* @var array
*/
protected $content = [];
/**
* @param array $trace
* @return array
*/
public function getTrace(array $trace = array())
{
$backtrace = $trace ?: debug_backtrace();
$lines = array();
/** @var array $trace */
foreach ($backtrace as $trace) {
//Handle the fact that not all desired fields will be present in each backtrace record.
$fields = array('file', 'line', 'function', 'class', 'args');
$trace = array_intersect_key($trace, array_flip($fields));
$trace = array_merge(array_fill_keys($fields, ''), $trace);
$trace['args'] = $trace['args'] ? array_map(array($this, 'normalizeArgs'), $trace['args']) : array();
if ($trace['function']) {
$function = $trace['class'] ? array($trace['class'], $trace['function']) : $trace['function'];
$trace['args'] = $this->applyParameterNames($trace['args'], $function);
}
//String the backtrace values together in an easier-to-read fashion
$line = $trace['file'].':'.$trace['line'];
($line == ':') && $line = uniqid('closure_');
$function = trim($trace['class'].'::'.$trace['function'], ':');
$lines[$line] = array($function => $trace['args']);
}
return $lines;
}
/**
* @param array $args
* @param string|array $function
* @param bool $includeDefaults
* @return array
*/
public function applyParameterNames(array $args, $function, $includeDefaults = true)
{
list($class, $function) = array_pad((array)$function, -2, null);
try {
$reflection = $class ? new ReflectionMethod($class, $function) : new ReflectionFunction($function);
$parameterNames = $defaults = array();
foreach ($reflection->getParameters() as $param) {
$parameterNames[] = $param->name;
if ($includeDefaults) {
$defaults[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
}
}
$args = $args + $defaults;
$args = array_pad($args, count($parameterNames), null);
$argNames = $parameterNames + array_keys($args);
$args = !empty($args) && !empty($argNames) ? array_combine($argNames, $args) : $args;
} catch (ReflectionException $e) {
//
}
return $args;
}
/**
* @param mixed $arg
* @return string
*/
public function normalizeArgs($arg)
{
switch (gettype($arg)) {
case 'object':
$newArg = get_class($arg);
break;
case 'resource':
$newArg = get_resource_type($arg) ?: 'resource';
break;
case 'array':
$newArg = $this->normalizeArrays($arg);
break;
default:
$newArg = $arg;
}
//Usage of the $newArg rather than reassigning $arg avoid a segmentation fault occurring in normalizeArrays.
return $newArg;
}
/**
* @param array $arg
* @return array
*/
protected function normalizeArrays(array $arg)
{
$newArg = array();
foreach ($arg as $index => $elm) {
$newArg[$index] = $this->normalizeArgs($elm);
}
//Usage of the $newArg rather than reassigning $arg avoids a referential override of object to string names.
return $newArg;
}
/**
* @param mixed $arg
* @return string
*/
protected function stringifyArgs($arg)
{
switch (gettype($arg)) {
case 'string':
if (strlen($arg) > self::TRUNCATE_AT) {
$arg = substr($arg, 0, self::TRUNCATE_AT).'...';
}
$arg = '"'.$arg.'"';
break;
case 'array':
$arg = 'array('.count($arg).')';
break;
case 'boolean':
$arg = $arg ? 'true' : 'false';
break;
case 'NULL':
$arg = 'null';
break;
}
return $arg;
}
/**
* @param array $trace
* @return array
*/
protected function flatten(array $trace)
{
/**
* @var string $line
* @var array $args
*/
foreach ($trace as $line => $args) {
$function = key($args);
$args = current($args);
foreach ($args as $param => $arg) {
$args[$param] = $param.':'.$this->stringifyArgs($arg);
}
$args = implode(', ', $args);
$function .= '('.$args.')';
$trace[$line] = $function;
}
return $trace;
}
/**
* @param mixed $arg
* @return bool
*/
public function isException($arg)
{
return is_object($arg) && $arg instanceof Exception;
}
/**
* @return array
*/
public function truncate()
{
$content = $this->content;
$content['trace'] = $this->flatten($content['trace']);
return $content;
}
/**
* @param mixed|null $content
* @return $this
*/
public function generate($content = null)
{
$content = (array)$content;
$trace = array();
if (!empty($content) && $this->isException($err = current($content))) {
/** @var Exception $err */
$key = key($content);
$content[$key] = get_class($err).': "'.$err->getMessage().'"';
$trace = $err->getTrace();
}
$content = array(
'debug' => $content,
'trace' => $this->getTrace($trace)
);
$this->content = $content;
return $this;
}
/**
* @return array
*/
public function getContent(): array
{
return $this->content;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment