Lines 95.00% 19 / 20
Methods 50.00% 1 / 2
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 messages 88.88% 8 / 9 0.00% 0 / 1 3.01
 render 100.00% 11 / 11 100.00% 1 / 1 4
17final class ExceptionChain
18{
19    /**
20     * The chain as "Class: message" strings, outermost first.
21     *
22     * Exception::getPrevious() is final, so userland cannot build a cycle;
23     * the visited-set is a defensive guard anyway (a native or exotic
24     * Throwable implementation could) and costs nothing per link.
25     *
26     * @return list<string>
27     */
28    public static function messages(\Throwable $e): array
29    {
30        $messages = [];
31        $seen = [];
32
33        for ($current = $e; $current !== null; $current = $current->getPrevious()) {
34            $hash = spl_object_id($current);
35
36            if (isset($seen[$hash])) {
37                break;
38            }
39
40            $seen[$hash] = true;
41            $messages[] = $current::class . ': ' . $current->getMessage();
42        }
43
44        return $messages;
45    }
46
47    /**
48     * Render the chain as console lines: the first line carries the caller's
49     * prefix (already styled by the caller), each cause follows as an
50     * indented "Caused by" line, in red when styling is on.
51     *
52     * @param string $firstLine e.g. "Sync failed: " — the outermost message is appended
53     * @param bool $colors Emit ANSI codes around the "Caused by" lines. Off
54     *        when the destination is not a TTY (pipes, CI logs, captured
55     *        output) — the styling decision belongs to the caller, which
56     *        knows where the string is headed; this method never inspects
57     *        the streams itself.
58     * @return string Multi-line string without a trailing newline
59     */
60    public static function render(\Throwable $e, string $firstLine, bool $colors = true): string
61    {
62        $messages = self::messages($e);
63
64        $output = $firstLine . array_shift($messages);
65
66        if (!$colors) {
67            foreach ($messages as $message) {
68                $output .= "\n  ↳ Caused by " . $message;
69            }
70
71            return $output;
72        }
73
74        foreach ($messages as $message) {
75            $output .= "\n" . ConsoleColors::FG_RED
76                . '  ↳ Caused by ' . $message
77                . ConsoleColors::RESET;
78        }
79
80        return $output;
81    }
82}