*/ final class StackTraceFormatter { private function __construct() { } /** * Formats an exception in a java-like format. * * @param Throwable $e exception to format * @return string formatted exception * * @see https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Throwable.html#printStackTrace() */ public static function format(Throwable $e): string { $s = ''; $seen = []; /** @var Frames|null $enclosing */ $enclosing = null; do { if ($enclosing) { self::writeNewline($s); $s .= 'Caused by: '; } if (isset($seen[spl_object_id($e)])) { $s .= '[CIRCULAR REFERENCE: '; self::writeInlineHeader($s, $e); $s .= ']'; break; } $seen[spl_object_id($e)] = $e; $frames = self::frames($e); self::writeInlineHeader($s, $e); self::writeFrames($s, $frames, $enclosing); $enclosing = $frames; } while ($e = $e->getPrevious()); return $s; } /** * @phan-suppress-next-line PhanTypeMismatchDeclaredParam * @param Frames $frames * @phan-suppress-next-line PhanTypeMismatchDeclaredParam * @param Frames|null $enclosing */ private static function writeFrames(string &$s, array $frames, ?array $enclosing): void { $n = count($frames); if ($enclosing) { for ($m = count($enclosing); $n && $m && $frames[$n - 1] === $enclosing[$m - 1]; $n--, $m--) { } } for ($i = 0; $i < $n; $i++) { $frame = $frames[$i]; self::writeNewline($s, 1); $s .= 'at '; if ($frame['class'] !== null) { $s .= self::formatName($frame['class']); $s .= '.'; } $s .= self::formatName($frame['function']); $s .= '('; if ($frame['file'] !== null) { $s .= basename($frame['file']); if ($frame['line']) { $s .= ':'; $s .= $frame['line']; } } else { $s .= 'Unknown Source'; } $s .= ')'; } if ($n !== count($frames)) { self::writeNewline($s, 1); $s .= sprintf('... %d more', count($frames) - $n); } } private static function writeInlineHeader(string &$s, Throwable $e): void { $s .= self::formatName(get_class($e)); if ($e->getMessage() !== '') { $s .= ': '; $s .= $e->getMessage(); } } private static function writeNewline(string &$s, int $indent = 0): void { $s .= "\n"; $s .= str_repeat("\t", $indent); } /** * @return Frames * * @psalm-suppress PossiblyUndefinedArrayOffset */ private static function frames(Throwable $e): array { $frames = []; $trace = $e->getTrace(); $traceCount = count($trace); for ($i = 0; $i < $traceCount + 1; $i++) { $frames[] = [ 'function' => $trace[$i]['function'] ?? '{main}', 'class' => $trace[$i]['class'] ?? null, 'file' => $trace[$i - 1]['file'] ?? null, 'line' => $trace[$i - 1]['line'] ?? null, ]; } $frames[0]['file'] = $e->getFile(); $frames[0]['line'] = $e->getLine(); /** @var Frames $frames */ return $frames; } private static function formatName(string $name): string { return strtr($name, ['\\' => '.']); } }