Passed
Push — stable ( 85ed36...e7655b )
by Nuno
12:43 queued 11s
created

Writer   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 84.4%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 12
dl 0
loc 365
ccs 92
cts 109
cp 0.844
rs 9.44
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 5
A write() 0 20 5
A ignoreFilesIn() 0 6 1
A showTrace() 0 6 1
A showTitle() 0 6 1
A showEditor() 0 6 1
A setOutput() 0 6 1
A getOutput() 0 4 1
A getFrames() 0 22 4
A renderTitleAndDescription() 0 15 2
A renderSolution() 0 25 2
A renderEditor() 0 14 1
B renderTrace() 0 45 8
A render() 0 10 2
A getFileRelativePath() 0 10 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
     * @param  \NunoMaduro\Collision\Contracts\SolutionsRepository|null  $solutionsRepository
97
     * @param  \Symfony\Component\Console\Output\OutputInterface|null  $output
98
     * @param  \NunoMaduro\Collision\Contracts\ArgumentFormatter|null  $argumentFormatter
99
     * @param  \NunoMaduro\Collision\Contracts\Highlighter|null  $highlighter
100
     */
101 13
    public function __construct(
102
        SolutionsRepository $solutionsRepository = null,
103
        OutputInterface $output = null,
104
        ArgumentFormatterContract $argumentFormatter = null,
105
        HighlighterContract $highlighter = null
106
    ) {
107 13
        $this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository();
108 13
        $this->output = $output ?: new ConsoleOutput();
109 13
        $this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter;
110 13
        $this->highlighter = $highlighter ?: new Highlighter;
111 13
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 5
    public function write(Inspector $inspector): void
117
    {
118 5
        $this->renderTitleAndDescription($inspector);
119
120 5
        $frames = $this->getFrames($inspector);
121
122 5
        $editorFrame = array_shift($frames);
123
124 5
        if ($this->showEditor && $editorFrame !== null) {
125 4
            $this->renderEditor($editorFrame);
126
        }
127
128 5
        $this->renderSolution($inspector);
129
130 5
        if ($this->showTrace && ! empty($frames)) {
131 4
            $this->renderTrace($frames);
132
        } else {
133 1
            $this->output->writeln('');
134
        }
135 5
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 1
    public function ignoreFilesIn(array $ignore): WriterContract
141
    {
142 1
        $this->ignore = $ignore;
143
144 1
        return $this;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 1
    public function showTrace(bool $show): WriterContract
151
    {
152 1
        $this->showTrace = $show;
153
154 1
        return $this;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function showTitle(bool $show): WriterContract
161
    {
162
        $this->showTitle = $show;
163
164
        return $this;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 1
    public function showEditor(bool $show): WriterContract
171
    {
172 1
        $this->showEditor = $show;
173
174 1
        return $this;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 1
    public function setOutput(OutputInterface $output): WriterContract
181
    {
182 1
        $this->output = $output;
183
184 1
        return $this;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 7
    public function getOutput(): OutputInterface
191
    {
192 7
        return $this->output;
193
    }
194
195
    /**
196
     * Returns pertinent frames.
197
     *
198
     * @param  \Whoops\Exception\Inspector  $inspector
199
     *
200
     * @return array
201
     */
202 5
    protected function getFrames(Inspector $inspector): array
203
    {
204 5
        return $inspector->getFrames()
205 5
            ->filter(
206
                function ($frame) {
207
                    // If we are in verbose mode, we always
208
                    // display the full stack trace.
209 5
                    if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
210 1
                        return true;
211
                    }
212
213 4
                    foreach ($this->ignore as $ignore) {
214 1
                        if (preg_match($ignore, $frame->getFile())) {
215 1
                            return false;
216
                        }
217
                    }
218
219 4
                    return true;
220 5
                }
221
            )
222 5
            ->getArray();
223
    }
224
225
    /**
226
     * Renders the title of the exception.
227
     *
228
     * @param  \Whoops\Exception\Inspector  $inspector
229
     *
230
     * @return \NunoMaduro\Collision\Contracts\Writer
231
     */
232 5
    protected function renderTitleAndDescription(Inspector $inspector): WriterContract
233
    {
234 5
        $exception = $inspector->getException();
235 5
        $message = rtrim($exception->getMessage());
236 5
        $class = $inspector->getExceptionName();
237
238 5
        if ($this->showTitle) {
239 5
            $this->render("<bg=red;options=bold> $class </>");
240 5
            $this->output->writeln('');
241
        }
242
243 5
        $this->output->writeln("<fg=default;options=bold>  $message</>");
244
245 5
        return $this;
246
    }
247
248
    /**
249
     * Renders the solution of the exception, if any.
250
     *
251
     * @param  \Whoops\Exception\Inspector  $inspector
252
     *
253
     * @return \NunoMaduro\Collision\Contracts\Writer
254
     */
255 5
    protected function renderSolution(Inspector $inspector): WriterContract
256
    {
257 5
        $throwable = $inspector->getException();
258 5
        $solutions = $this->solutionsRepository->getFromThrowable($throwable);
259
260 5
        foreach ($solutions as $solution) {
261
            /** @var \Facade\IgnitionContracts\Solution $solution */
262
            $title = $solution->getSolutionTitle();
263
            $description = $solution->getSolutionDescription();
264
            $links = $solution->getDocumentationLinks();
265
266
            $description = trim((string) preg_replace("/\n/", "\n    ", $description));
267
268
            $this->render(sprintf(
269
                '<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',
270
                rtrim($title, '.'),
271
                $description,
272
                implode(', ', array_map(function (string $link) {
273
                    return sprintf("\n    <fg=blue>%s</>", $link);
274
                }, $links))
275
            ));
276
        }
277
278 5
        return $this;
279
    }
280
281
    /**
282
     * Renders the editor containing the code that was the
283
     * origin of the exception.
284
     *
285
     * @param  \Whoops\Exception\Frame  $frame
286
     *
287
     * @return \NunoMaduro\Collision\Contracts\Writer
288
     */
289 4
    protected function renderEditor(Frame $frame): WriterContract
290
    {
291 4
        $file = $this->getFileRelativePath((string) $frame->getFile());
292
293
        // getLine() might return null so cast to int to get 0 instead
294 4
        $line = (int) $frame->getLine();
295 4
        $this->render('at <fg=green>'.$file.'</>'.':<fg=green>'.$line.'</>');
296
297 4
        $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
298
299 4
        $this->output->writeln($content);
300
301 4
        return $this;
302
    }
303
304
    /**
305
     * Renders the trace of the exception.
306
     *
307
     * @param  array  $frames
308
     *
309
     * @return \NunoMaduro\Collision\Contracts\Writer
310
     */
311 4
    protected function renderTrace(array $frames): WriterContract
312
    {
313 4
        $vendorFrames = 0;
314 4
        $userFrames = 0;
315 4
        foreach ($frames as $i => $frame) {
316 4
            if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
317 1
                $vendorFrames++;
318 1
                continue;
319
            }
320
321 3
            if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
322 2
                break;
323
            }
324
325 3
            $userFrames++;
326
327 3
            $file = $this->getFileRelativePath($frame->getFile());
328 3
            $line = $frame->getLine();
329 3
            $class = empty($frame->getClass()) ? '' : $frame->getClass().'::';
330 3
            $function = $frame->getFunction();
331 3
            $args = $this->argumentFormatter->format($frame->getArgs());
332 3
            $pos = str_pad((string) ((int) $i + 1), 4, ' ');
333
334 3
            if ($vendorFrames > 0) {
335
                $this->output->write(
336
                    sprintf("\n      \e[2m+%s vendor frames \e[22m", $vendorFrames)
337
                );
338
                $vendorFrames = 0;
339
            }
340
341 3
            $this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");
342 3
            $this->render("<fg=white>    $class$function($args)</>", false);
343
        }
344
345
        /** Let's consider add this later...
346
         * if ($vendorFrames > 0) {
347
         * $this->output->write(
348
         * sprintf("\n      \e[2m+%s vendor frames \e[22m\n", $vendorFrames)
349
         * );
350
         * $vendorFrames = 0;
351
         * }
352
         */
353
354 4
        return $this;
355
    }
356
357
    /**
358
     * Renders an message into the console.
359
     *
360
     * @param  string  $message
361
     * @param  bool  $break
362
     *
363
     * @return $this
364
     */
365 5
    protected function render(string $message, bool $break = true): WriterContract
366
    {
367 5
        if ($break) {
368 5
            $this->output->writeln('');
369
        }
370
371 5
        $this->output->writeln("  $message");
372
373 5
        return $this;
374
    }
375
376
    /**
377
     * Returns the relative path of the given file path.
378
     *
379
     * @param  string $filePath
380
     *
381
     * @return string
382
     */
383 5
    protected function getFileRelativePath(string $filePath): string
384
    {
385 5
        $cwd = (string) getcwd();
386
387 5
        if (! empty($cwd)) {
388 5
            return str_replace("$cwd/", '', $filePath);
389
        }
390
391
        return $filePath;
392
    }
393
}
394