Completed
Push — stable ( 256003...556b2e )
by Nuno
16:53 queued 15:09
created

Writer::setOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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