Test Failed
Pull Request — master (#853)
by butschster
08:51 queued 01:56
created

ConsoleRenderer::render()   B

Complexity

Conditions 9
Paths 26

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 9

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 30
c 4
b 1
f 0
dl 0
loc 53
ccs 32
cts 32
cp 1
rs 8.0555
cc 9
nc 26
nop 3
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Exceptions\Renderer;
6
7
use Codedungeon\PHPCliColors\Color;
8
use Spiral\Exceptions\Style\ConsoleStyle;
9
use Spiral\Exceptions\Style\PlainStyle;
10
use Spiral\Exceptions\Verbosity;
11
12
/**
13
 * Verbosity levels:
14
 *
15
 * 1) {@see Verbosity::BASIC} - only message header and line number
16
 * 2) {@see Verbosity::VERBOSE} - stack information
17
 * 3) {@see Verbosity::DEBUG} - stack and source information.
18
 */
19
class ConsoleRenderer extends AbstractRenderer
20
{
21
    // Lines to show around targeted line.
22
    public const SHOW_LINES = 2;
23
    protected const FORMATS = ['console', 'cli'];
24
25
    protected const COLORS = [
26
        'bg:red' => Color::BG_RED,
27
        'bg:cyan' => Color::BG_CYAN,
28
        'bg:magenta' => Color::BG_MAGENTA,
29
        'bg:white' => Color::BG_WHITE,
30
        'white' => Color::LIGHT_WHITE,
31
        'green' => Color::GREEN,
32
        'gray' => Color::GRAY,
33
        'black' => Color::BLACK,
34
        'red' => Color::RED,
35
        'yellow' => Color::YELLOW,
36
        'reset' => Color::RESET,
37
    ];
38
39
    private array $lines = [];
40
41
    private bool $colorsSupport;
42
43 7
    /**
44
     * @param bool|resource $stream
45 7
     */
46 7
    public function __construct(mixed $stream = null)
47
    {
48
        $stream ??= \defined('\STDOUT') ? \STDOUT : \fopen('php://stdout', 'wb');
49
50
        $this->colorsSupport = $this->isColorsSupported($stream);
51
    }
52 6
53
    /**
54 6
     * Disable or enable colorization support.
55
     */
56
    public function setColorsSupport(bool $enabled = true): void
57 7
    {
58
        $this->colorsSupport = $enabled;
59
    }
60
61
    public function render(
62 7
        \Throwable $exception,
63
        ?Verbosity $verbosity = null,
64 7
        string $format = null
65 7
    ): string {
66 7
        $verbosity ??= $this->defaultVerbosity;
67 7
68
        $exceptions = [$exception];
69 7
        $currentE = $exception;
70 7
71 7
        while ($exception = $exception->getPrevious()) {
72 7
            $exceptions[] = $exception;
73 7
        }
74
75 7
        $exceptions = \array_reverse($exceptions);
76 2
77 2
        $result = [];
78 2
        $rootDir = \getcwd();
79 5
80 1
        foreach ($exceptions as $exception) {
81
            $prefix = $currentE === $exception ? '' : 'Previous: ';
82
            $row = $this->renderHeader(
83 7
                \sprintf("%s[%s]\n%s", $prefix, $exception::class, $exception->getMessage()),
84
                $exception instanceof \Error ? 'bg:magenta,white' : 'bg:red,white'
85
            );
86
87
            $file = \str_starts_with($exception->getFile(), $rootDir)
88
                ? \substr($exception->getFile(), \strlen($rootDir) + 1)
89
                : $exception->getFile();
90
91
            $row .= $this->format(
92 7
                "<yellow>in</reset> <green>%s</reset><yellow>:</reset><white>%s</reset>\n",
93
                $file,
94 7
                $exception->getLine()
95
            );
96 7
97
            if ($verbosity->value >= Verbosity::DEBUG->value) {
98 7
                $row .= $this->renderTrace(
99 7
                    $exception,
100 7
                    new Highlighter(
101 7
                        $this->colorsSupport ? new ConsoleStyle() : new PlainStyle()
102
                    )
103 7
                );
104
            } elseif ($verbosity->value >= Verbosity::VERBOSE->value) {
105 7
                $row .= $this->renderTrace($exception);
106 7
            }
107 7
108 7
            $result[] = $row;
109 7
        }
110 7
111 7
        $this->lines = [];
112
113
        return \implode("\n", \array_reverse($result));
114 7
    }
115
116
    /**
117
     * Render title using outlining border.
118
     *
119
     * @param string $title Title.
120 3
     * @param string $style Formatting.
121
     */
122 3
    private function renderHeader(string $title, string $style, int $padding = 0): string
123 3
    {
124
        $result = '';
125
126
        $lines = \explode("\n", \str_replace("\r", '', $title));
127 3
128
        $length = 0;
129 3
        \array_walk($lines, static function ($v) use (&$length): void {
130 3
            $length = max($length, \mb_strlen($v));
131 3
        });
132 3
133 3
        $length += $padding;
134 3
135 3
        foreach ($lines as $line) {
136 3
            $result .= $this->format(
137
                "<{$style}>%s%s%s</reset>\n",
138 3
                \str_repeat('', $padding + 1),
139 3
                $line,
140 3
                \str_repeat('', $length - \mb_strlen($line) + 1)
141 3
            );
142
        }
143
144 3
        return $result;
145 3
    }
146 3
147 3
    /**
148 3
     * Render exception call stack.
149 3
     */
150
    private function renderTrace(\Throwable $e, Highlighter $h = null): string
151
    {
152
        $stacktrace = $this->getStacktrace($e);
153
        if (empty($stacktrace)) {
154
            return '';
155
        }
156
157
        $result = "\n";
158 3
        $rootDir = \getcwd();
159
160 3
        $pad = \strlen((string)\count($stacktrace));
161 2
162 2
        foreach ($stacktrace as $i => $trace) {
163 2
            $file = null;
164 2
            $classColor = 'while';
165 2
166
            if (isset($trace['file'])) {
167
                $file = \str_starts_with($trace['file'], $rootDir)
168
                    ? \substr($trace['file'], \strlen($rootDir) + 1)
169 3
                    : $trace['file'];
170
171
                $classColor = \str_starts_with($file, 'vendor/') ? 'gray' : 'white';
172
            }
173
174
            if (isset($trace['type'], $trace['class'])) {
175 7
                $line = $this->format(
176
                    "<$classColor>%s.</reset> <white>%s%s%s()</reset>",
177 7
                    \str_pad((string)((int)$i + 1), $pad, ' ', \STR_PAD_LEFT),
178 2
                    $trace['class'],
179
                    $trace['type'],
180 5
                    $trace['function']
181 5
                );
182 5
            } else {
183 5
                $line = $this->format(
184 5
                    ' <white>%s()</reset>',
185
                    $trace['function']
186
                );
187
            }
188 5
            if ($file !== null) {
189 5
                $line .= $this->format(
190
                    ' <yellow>at</reset> <green>%s</reset><yellow>:</reset><white>%s</reset>',
191
                    $file,
192 7
                    $trace['line']
193
                );
194
            }
195
196
            if (\in_array($line, $this->lines, true)) {
197
                continue;
198
            }
199
200
            $this->lines[] = $line;
201
202
            $result .= $line . "\n";
203
204
            if ($h !== null && !empty($trace['file'])) {
205
                $result .= $h->highlightLines(
206
                    \file_get_contents($trace['file']),
207
                    $trace['line'],
208
                    static::SHOW_LINES
209
                ) . "\n";
210
            }
211
        }
212
213
        return $result;
214
    }
215
216
    /**
217
     * Format string and apply color formatting (if enabled).
218
     */
219
    private function format(string $format, mixed ...$args): string
220
    {
221
        if (!$this->colorsSupport) {
222
            $format = \preg_replace('/<[^>]+>/', '', $format);
223
        } else {
224
            $format = \preg_replace_callback('/(<([^>]+)>)/', static function ($partial) {
225
                $style = '';
226
                foreach (\explode(',', \trim($partial[2], '/')) as $color) {
227
                    if (isset(self::COLORS[$color])) {
228
                        $style .= self::COLORS[$color];
229
                    }
230
                }
231
232
                return $style;
233
            }, $format);
234
        }
235
236
        return \sprintf($format, ...$args);
237
    }
238
239
    /**
240
     * Returns true if the STDOUT supports colorization.
241
     * @codeCoverageIgnore
242
     * @link https://github.com/symfony/Console/blob/master/Output/StreamOutput.php#L94
243
     */
244
    private function isColorsSupported(mixed $stream = STDOUT): bool
245
    {
246
        if ('Hyper' === \getenv('TERM_PROGRAM')) {
247
            return true;
248
        }
249
250
        try {
251
            if (\DIRECTORY_SEPARATOR === '\\') {
252
                return (\function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support($stream))
253
                    || \getenv('ANSICON') !== false
254
                    || \getenv('ConEmuANSI') === 'ON'
255
                    || \getenv('TERM') === 'xterm';
256
            }
257
258
            return @\stream_isatty($stream);
259
        } catch (\Throwable) {
260
            return false;
261
        }
262
    }
263
}
264