Created
August 11, 2020 18:43
-
-
Save kmuenkel/c6ed7fbcb226c361bb142efe299939cb to your computer and use it in GitHub Desktop.
This file contains hidden or 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
<?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