Completed
Push — stable ( cbd099...ce6aca )
by Nuno
14:51 queued 11s
created

Writer::getFileRelativePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 4
cts 5
cp 0.8
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.032
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 5
                    foreach ($this->ignore as $ignore) {
191 1
                        if (preg_match($ignore, $frame->getFile())) {
192 1
                            return false;
193
                        }
194
                    }
195
196 5
                    return true;
197 5
                }
198
            )
199 5
            ->getArray();
200
    }
201
202
    /**
203
     * Renders the title of the exception.
204
     *
205
     * @param  \Whoops\Exception\Inspector  $inspector
206
     *
207
     * @return \NunoMaduro\Collision\Contracts\Writer
208
     */
209 5
    protected function renderTitle(Inspector $inspector): WriterContract
210
    {
211 5
        $exception = $inspector->getException();
212 5
        $message = rtrim($exception->getMessage());
213 5
        $class = $inspector->getExceptionName();
214
215 5
        $this->render("<bg=red;options=bold> $class </>");
216 5
        $this->output->writeln('');
217 5
        $this->output->writeln("<fg=default;options=bold>  $message</>");
218
219 5
        return $this;
220
    }
221
222
    /**
223
     * Renders the solution of the exception, if any.
224
     *
225
     * @param  \Whoops\Exception\Inspector  $inspector
226
     *
227
     * @return \NunoMaduro\Collision\Contracts\Writer
228
     */
229 5
    protected function renderSolution(Inspector $inspector): WriterContract
230
    {
231 5
        $throwable = $inspector->getException();
232 5
        $solutions = $this->solutionsRepository->getFromThrowable($throwable);
233
234 5
        foreach ($solutions as $solution) {
235
            /** @var \Facade\IgnitionContracts\Solution $solution */
236
            $title = $solution->getSolutionTitle();
237
            $description = $solution->getSolutionDescription();
238
            $links = $solution->getDocumentationLinks();
239
240
            $description = trim((string) preg_replace("/\n/", "\n    ", $description));
241
242
            $this->render(sprintf(
243
                '<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',
244
                rtrim($title, '.'),
245
                $description,
246
                implode(', ', array_map(function (string $link) {
247
                    return sprintf("\n    <fg=blue>%s</>", $link);
248
                }, $links))
249
            ));
250
        }
251
252 5
        return $this;
253
    }
254
255
    /**
256
     * Renders the editor containing the code that was the
257
     * origin of the exception.
258
     *
259
     * @param  \Whoops\Exception\Frame  $frame
260
     *
261
     * @return \NunoMaduro\Collision\Contracts\Writer
262
     */
263 4
    protected function renderEditor(Frame $frame): WriterContract
264
    {
265 4
        $file = $this->getFileRelativePath((string) $frame->getFile());
266
267 4
        $this->render('at <fg=green>'.$file.'</>'.':<fg=green>'.$frame->getLine().'</>');
268
269 4
        $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
270
271 4
        $this->output->writeln($content);
272
273 4
        return $this;
274
    }
275
276
    /**
277
     * Renders the trace of the exception.
278
     *
279
     * @param  array  $frames
280
     *
281
     * @return \NunoMaduro\Collision\Contracts\Writer
282
     */
283 4
    protected function renderTrace(array $frames): WriterContract
284
    {
285 4
        $vendorFrames = 0;
286 4
        $userFrames = 0;
287 4
        foreach ($frames as $i => $frame) {
288 4
            if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
289 1
                $vendorFrames++;
290 1
                continue;
291
            }
292
293 3
            if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
294 2
                break;
295
            }
296
297 3
            $userFrames++;
298
299 3
            $file = $this->getFileRelativePath($frame->getFile());
300 3
            $line = $frame->getLine();
301 3
            $class = empty($frame->getClass()) ? '' : $frame->getClass().'::';
302 3
            $function = $frame->getFunction();
303 3
            $args = $this->argumentFormatter->format($frame->getArgs());
304 3
            $pos = str_pad((int) $i + 1, 4, ' ');
305
306 3
            if ($vendorFrames > 0) {
307
                $this->output->write(
308
                    sprintf("\n      \e[2m+%s vendor frames \e[22m", $vendorFrames)
309
                );
310
                $vendorFrames = 0;
311
            }
312
313 3
            $this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");
314 3
            $this->render("<fg=white>    $class$function($args)</>", false);
315
        }
316
317
        /** Let's consider add this later...
318
        if ($vendorFrames > 0) {
319
            $this->output->write(
320
                sprintf("\n      \e[2m+%s vendor frames \e[22m\n", $vendorFrames)
321
            );
322
            $vendorFrames = 0;
323
        }
324
        */
325
326 4
        return $this;
327
    }
328
329
    /**
330
     * Renders an message into the console.
331
     *
332
     * @param  string  $message
333
     * @param  bool  $break
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
     * @param  string $filePath
352
     *
353
     * @return string
354
     */
355 5
    protected function getFileRelativePath(string $filePath): string
356
    {
357 5
        $cwd = (string) getcwd();
358
359 5
        if (! empty($cwd)) {
360 5
            return str_replace("$cwd/", '', $filePath);
361
        }
362
363
        return $filePath;
364
    }
365
}
366