I have always disliked the way PHP printed stacktraces, coming from a Java world. So I decided to implement my own ExceptionFormatter
, a class that will format Exceptions/Throwables to my liking.
class ExceptionFormatter
{
/** @var \Exception|\Throwable */
private $exception;
/** @var string */
private $formattedString;
/**
* @param \Exception|\Throwable $exception
*/
private function __construct($exception)
{
$this->exception = $exception;
$this->formattedString = $this->formatException();
}
private function formatException()
{
return $this->formatExceptionMessage()
.$this->formatExceptionTrace()
.$this->getCauseIfApplicable();
}
private function formatExceptionMessage()
{
$exceptionClass = get_class($this->exception);
$exceptionMessage = $this->exception->getMessage();
$fileAndLine = $this->formatFileAndLine($this->exception->getFile(), $this->exception->getLine());
if ($exceptionMessage === '')
return "${exceptionClass} (${fileAndLine})\n";
return "${exceptionClass}: ${exceptionMessage} (${fileAndLine})\n";
}
private function formatFileAndLine($file, $line)
{
return "${file}:${line}";
}
private function formatExceptionTrace()
{
$exceptionTrace = $this->exception->getTrace();
$formattedTrace = [];
foreach($exceptionTrace as $trace) {
$formattedTrace[] = "\tat ".$this->formatTraceElement($trace);
}
return implode("\n", $formattedTrace);
}
private function formatTraceElement($traceElement)
{
$fileAndLine = $this->formatFileAndLine(
isset($traceElement['file']) ? $traceElement['file'] : 'unknown',
isset($traceElement['line']) ? $traceElement['line'] : 'unknown'
);
if ($this->isFunctionCall($traceElement)) {
$functionCall = $this->formatFunctionCall($traceElement);
$arguments = $this->formatArguments($traceElement);
return "${functionCall}(${arguments}) (${fileAndLine})";
}
return $fileAndLine;
}
private function isFunctionCall($traceElement)
{
return array_key_exists('function', $traceElement);
}
private function formatFunctionCall($traceElement)
{
return (isset($traceElement['class']) ? $traceElement['class'] : '')
.(isset($traceElement['type']) ? $traceElement['type'] : '')
.$traceElement['function'];
}
private function formatArguments($traceElement)
{
/** @var string[] $arguments */
$arguments = $traceElement['args'];
$formattedArgs = [];
foreach ($arguments as $arg) {
$formattedArgs[] = $this->formatArgument($arg);
}
return implode(', ', $formattedArgs);
}
private function formatArgument($arg)
{
if (is_string($arg)) {
return "\"".$arg."\"";
} else if (is_array($arg)) {
return 'Array';
} else if ($arg === null) {
return 'null';
} else if (is_bool($arg)) {
return $arg ? 'true' : 'false';
} else if (is_object($arg)) {
return get_class($arg);
} else if (is_resource($arg)) {
return get_resource_type($arg);
} else {
return $arg;
}
}
private function getCauseIfApplicable()
{
$previousException = $this->exception->getPrevious();
if ($previousException !== null)
return "\nCaused by: " . self::format($previousException);
return '';
}
/**
* Converts an Exception to a Java-style stack trace string.
*
* @param \Exception|\Throwable The Exception/Throwable to format as a "pretty" string.
* @return string
*/
public static function format($exception)
{
$formatter = new ExceptionFormatter($exception);
return $formatter->getFormattedString();
}
public function getFormattedString()
{
return $this->formattedString;
}
}
The output looks like this:
LogicException: Lulz! (/in/WNLbO:158) at nestedFunction() (/in/WNLbO:164) at throwExceptionForLulz(Array, "String", stdClass, false, "a", "b") (/in/WNLbO:168) Caused by: RuntimeException: This is the cause (/in/WNLbO:150) at throwCause() (/in/WNLbO:156) at nestedFunction() (/in/WNLbO:164) at throwExceptionForLulz(Array, "String", stdClass, false, "a", "b") (/in/WNLbO:168)
Usage:
try {
methodThatThrowsException();
} catch (Exception $e) {
echo ExceptionFormatter::format($e);
}
I have tried to keep the whole thing as simple and self-explanatory as possible (which happens to be my excuse for the lack of comments).
reinventing-the-wheel
? \$\endgroup\$