Passed
Pull Request — master (#853)
by butschster
06:40
created

ConsoleRenderer::render()   B

Complexity

Conditions 9
Paths 26

Size

Total Lines 54
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 9.0018

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 30
c 2
b 0
f 0
dl 0
loc 54
ccs 34
cts 35
cp 0.9714
rs 8.0555
cc 9
nc 26
nop 3
crap 9.0018

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