Writer::showTitle()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
/**
4
 * This file is part of Collision.
5
 *
6
 * (c) Nuno Maduro <[email protected]>
7
 *
8
 *  For the full copyright and license information, please view the LICENSE
9
 *  file that was distributed with this source code.
10
 */
11
12
namespace NunoMaduro\Collision;
13
14
use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;
15
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
16
use NunoMaduro\Collision\Contracts\SolutionsRepository;
17
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
18
use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;
19
use Symfony\Component\Console\Output\ConsoleOutput;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Whoops\Exception\Frame;
22
use Whoops\Exception\Inspector;
23
24
/**
25
 * This is an Collision Writer implementation.
26
 *
27
 * @author Nuno Maduro <[email protected]>
28
 */
29
class Writer implements WriterContract
30
{
31
    /**
32
     * The number of frames if no verbosity is specified.
33
     */
34
    const VERBOSITY_NORMAL_FRAMES = 1;
35
36
    /**
37
     * Holds an instance of the solutions repository.
38
     *
39
     * @var \NunoMaduro\Collision\Contracts\SolutionsRepository
40
     */
41
    private $solutionsRepository;
42
43
    /**
44
     * Holds an instance of the Output.
45
     *
46
     * @var \Symfony\Component\Console\Output\OutputInterface
47
     */
48
    protected $output;
49
50
    /**
51
     * Holds an instance of the Argument Formatter.
52
     *
53
     * @var \NunoMaduro\Collision\Contracts\ArgumentFormatter
54
     */
55
    protected $argumentFormatter;
56
57
    /**
58
     * Holds an instance of the Highlighter.
59
     *
60
     * @var \NunoMaduro\Collision\Contracts\Highlighter
61
     */
62
    protected $highlighter;
63
64
    /**
65
     * Ignores traces where the file string matches one
66
     * of the provided regex expressions.
67
     *
68
     * @var string[]
69
     */
70
    protected $ignore = [];
71
72
    /**
73
     * Declares whether or not the trace should appear.
74
     *
75
     * @var bool
76
     */
77
    protected $showTrace = true;
78
79
    /**
80
     * Declares whether or not the title should appear.
81
     *
82
     * @var bool
83
     */
84
    protected $showTitle = true;
85
86
    /**
87
     * Declares whether or not the editor should appear.
88
     *
89
     * @var bool
90
     */
91
    protected $showEditor = true;
92
93
    /**
94
     * Creates an instance of the writer.
95
     */
96 13
    public function __construct(
97
        SolutionsRepository $solutionsRepository = null,
98
        OutputInterface $output = null,
99
        ArgumentFormatterContract $argumentFormatter = null,
100
        HighlighterContract $highlighter = null
101
    ) {
102 13
        $this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository();
103 13
        $this->output              = $output ?: new ConsoleOutput();
104 13
        $this->argumentFormatter   = $argumentFormatter ?: new ArgumentFormatter();
105 13
        $this->highlighter         = $highlighter ?: new Highlighter();
106 13
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 5
    public function write(Inspector $inspector): void
112
    {
113 5
        $this->renderTitleAndDescription($inspector);
114
115 5
        $frames = $this->getFrames($inspector);
116
117 5
        $editorFrame = array_shift($frames);
118
119 5
        if ($this->showEditor && $editorFrame !== null) {
120 4
            $this->renderEditor($editorFrame);
121
        }
122
123 5
        $this->renderSolution($inspector);
124
125 5
        if ($this->showTrace && !empty($frames)) {
126 4
            $this->renderTrace($frames);
127
        } else {
128 1
            $this->output->writeln('');
129
        }
130 5
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 1
    public function ignoreFilesIn(array $ignore): WriterContract
136
    {
137 1
        $this->ignore = $ignore;
138
139 1
        return $this;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 1
    public function showTrace(bool $show): WriterContract
146
    {
147 1
        $this->showTrace = $show;
148
149 1
        return $this;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function showTitle(bool $show): WriterContract
156
    {
157
        $this->showTitle = $show;
158
159
        return $this;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 1
    public function showEditor(bool $show): WriterContract
166
    {
167 1
        $this->showEditor = $show;
168
169 1
        return $this;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 1
    public function setOutput(OutputInterface $output): WriterContract
176
    {
177 1
        $this->output = $output;
178
179 1
        return $this;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 7
    public function getOutput(): OutputInterface
186
    {
187 7
        return $this->output;
188
    }
189
190
    /**
191
     * Returns pertinent frames.
192
     */
193 5
    protected function getFrames(Inspector $inspector): array
194
    {
195 5
        return $inspector->getFrames()
196 5
            ->filter(
197
                function ($frame) {
198
                    // If we are in verbose mode, we always
199
                    // display the full stack trace.
200 5
                    if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
201 1
                        return true;
202
                    }
203
204 4
                    foreach ($this->ignore as $ignore) {
205 1
                        if (preg_match($ignore, $frame->getFile())) {
206 1
                            return false;
207
                        }
208
                    }
209
210 4
                    return true;
211 5
                }
212
            )
213 5
            ->getArray();
214
    }
215
216
    /**
217
     * Renders the title of the exception.
218
     */
219 5
    protected function renderTitleAndDescription(Inspector $inspector): WriterContract
220
    {
221 5
        $exception = $inspector->getException();
222 5
        $message   = rtrim($exception->getMessage());
223 5
        $class     = $inspector->getExceptionName();
224
225 5
        if ($this->showTitle) {
226 5
            $this->render("<bg=red;options=bold> $class </>");
227 5
            $this->output->writeln('');
228
        }
229
230 5
        $this->output->writeln("<fg=default;options=bold>  $message</>");
231
232 5
        return $this;
233
    }
234
235
    /**
236
     * Renders the solution of the exception, if any.
237
     */
238 5
    protected function renderSolution(Inspector $inspector): WriterContract
239
    {
240 5
        $throwable = $inspector->getException();
241 5
        $solutions = $this->solutionsRepository->getFromThrowable($throwable);
242
243 5
        foreach ($solutions as $solution) {
244
            /** @var \Facade\IgnitionContracts\Solution $solution */
245
            $title       = $solution->getSolutionTitle();
246
            $description = $solution->getSolutionDescription();
247
            $links       = $solution->getDocumentationLinks();
248
249
            $description = trim((string) preg_replace("/\n/", "\n    ", $description));
250
251
            $this->render(sprintf(
252
                '<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',
253
                rtrim($title, '.'),
254
                $description,
255
                implode(', ', array_map(function (string $link) {
256
                    return sprintf("\n    <fg=blue>%s</>", $link);
257
                }, $links))
258
            ));
259
        }
260
261 5
        return $this;
262
    }
263
264
    /**
265
     * Renders the editor containing the code that was the
266
     * origin of the exception.
267
     */
268 4
    protected function renderEditor(Frame $frame): WriterContract
269
    {
270 4
        $file = $this->getFileRelativePath((string) $frame->getFile());
271
272
        // getLine() might return null so cast to int to get 0 instead
273 4
        $line = (int) $frame->getLine();
274 4
        $this->render('at <fg=green>' . $file . '</>' . ':<fg=green>' . $line . '</>');
275
276 4
        $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
277
278 4
        $this->output->writeln($content);
279
280 4
        return $this;
281
    }
282
283
    /**
284
     * Renders the trace of the exception.
285
     */
286 4
    protected function renderTrace(array $frames): WriterContract
287
    {
288 4
        $vendorFrames = 0;
289 4
        $userFrames   = 0;
290 4
        foreach ($frames as $i => $frame) {
291 4
            if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
292 1
                $vendorFrames++;
293 1
                continue;
294
            }
295
296 3
            if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
297 2
                break;
298
            }
299
300 3
            $userFrames++;
301
302 3
            $file     = $this->getFileRelativePath($frame->getFile());
303 3
            $line     = $frame->getLine();
304 3
            $class    = empty($frame->getClass()) ? '' : $frame->getClass() . '::';
305 3
            $function = $frame->getFunction();
306 3
            $args     = $this->argumentFormatter->format($frame->getArgs());
307 3
            $pos      = str_pad((string) ((int) $i + 1), 4, ' ');
308
309 3
            if ($vendorFrames > 0) {
310
                $this->output->write(
311
                    sprintf("\n      \e[2m+%s vendor frames \e[22m", $vendorFrames)
312
                );
313
                $vendorFrames = 0;
314
            }
315
316 3
            $this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");
317 3
            $this->render("<fg=white>    $class$function($args)</>", false);
318
        }
319
320
        /* Let's consider add this later...
321
         * if ($vendorFrames > 0) {
322
         * $this->output->write(
323
         * sprintf("\n      \e[2m+%s vendor frames \e[22m\n", $vendorFrames)
324
         * );
325
         * $vendorFrames = 0;
326
         * }.
327
         */
328
329 4
        return $this;
330
    }
331
332
    /**
333
     * Renders an message into the console.
334
     *
335
     * @return $this
336
     */
337 5
    protected function render(string $message, bool $break = true): WriterContract
338
    {
339 5
        if ($break) {
340 5
            $this->output->writeln('');
341
        }
342
343 5
        $this->output->writeln("  $message");
344
345 5
        return $this;
346
    }
347
348
    /**
349
     * Returns the relative path of the given file path.
350
     */
351 5
    protected function getFileRelativePath(string $filePath): string
352
    {
353 5
        $cwd = (string) getcwd();
354
355 5
        if (!empty($cwd)) {
356 5
            return str_replace("$cwd/", '', $filePath);
357
        }
358
359
        return $filePath;
360
    }
361
}
362