Test Failed
Pull Request — stable (#68)
by Nuno
18:22
created

Writer::renderSolution()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 9.4494

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 9.1448
c 0
b 0
f 0
ccs 7
cts 16
cp 0.4375
cc 5
nc 12
nop 1
crap 9.4494
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 Whoops\Exception\Frame;
15
use Whoops\Exception\Inspector;
16
use Facade\IgnitionContracts\Solution;
17
use Facade\IgnitionContracts\ProvidesSolution;
18
use Symfony\Component\Console\Input\ArrayInput;
19
use Symfony\Component\Console\Style\SymfonyStyle;
20
use Symfony\Component\Console\Output\ConsoleOutput;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
23
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
24
use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;
25
26
/**
27
 * This is an Collision Writer implementation.
28
 *
29
 * @author Nuno Maduro <[email protected]>
30
 */
31
class Writer implements WriterContract
32
{
33
    /**
34
     * The number of frames if no verbosity is specified.
35
     */
36
    const VERBOSITY_NORMAL_FRAMES = 1;
37
38
    /**
39
     * Holds an instance of the Output.
40
     *
41
     * @var \Symfony\Component\Console\Output\OutputInterface
42
     */
43
    protected $output;
44
45
    /**
46
     * Holds an instance of the Argument Formatter.
47
     *
48
     * @var \NunoMaduro\Collision\Contracts\ArgumentFormatter
49
     */
50
    protected $argumentFormatter;
51
52
    /**
53
     * Holds an instance of the Highlighter.
54
     *
55
     * @var \NunoMaduro\Collision\Contracts\Highlighter
56
     */
57
    protected $highlighter;
58
59
    /**
60
     * Ignores traces where the file string matches one
61
     * of the provided regex expressions.
62
     *
63
     * @var string[]
64
     */
65
    protected $ignore = [];
66
67
    /**
68
     * Declares whether or not the trace should appear.
69
     *
70
     * @var bool
71
     */
72
    protected $showTrace = true;
73
74
    /**
75
     * Declares whether or not the editor should appear.
76
     *
77
     * @var bool
78
     */
79
    protected $showEditor = true;
80
81
    /**
82
     * Creates an instance of the writer.
83
     *
84
     * @param  \Symfony\Component\Console\Output\OutputInterface|null  $output
85
     * @param  \NunoMaduro\Collision\Contracts\ArgumentFormatter|null  $argumentFormatter
86
     * @param  \NunoMaduro\Collision\Contracts\Highlighter|null  $highlighter
87
     */
88 14
    public function __construct(
89
        OutputInterface $output = null,
90
        ArgumentFormatterContract $argumentFormatter = null,
91
        HighlighterContract $highlighter = null
92
    )
93
    {
94 14
        $this->output = $output ?: new SymfonyStyle(new ArrayInput([]), new ConsoleOutput);
95 14
        $this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter;
96 14
        $this->highlighter = $highlighter ?: new Highlighter;
97 14
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 5
    public function write(Inspector $inspector): void
103
    {
104 5
        $this->renderTitle($inspector);
105
106 5
        $this->renderSolution($inspector);
107
108 5
        $frames = $this->getFrames($inspector);
109
110 5
        $editorFrame = array_shift($frames);
111
112 5
        if ($this->showEditor && $editorFrame !== null) {
113 4
            $this->renderEditor($editorFrame);
114
        }
115
116 5
        if ($this->showTrace && ! empty($frames)) {
117 4
            $this->renderTrace($frames);
118
        } else {
119 1
            $this->output->writeln('');
120
        }
121 5
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 1
    public function ignoreFilesIn(array $ignore): WriterContract
127
    {
128 1
        $this->ignore = $ignore;
129
130 1
        return $this;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 1
    public function showTrace(bool $show): WriterContract
137
    {
138 1
        $this->showTrace = $show;
139
140 1
        return $this;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 1
    public function showEditor(bool $show): WriterContract
147
    {
148 1
        $this->showEditor = $show;
149
150 1
        return $this;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156 2
    public function setOutput(OutputInterface $output): WriterContract
157
    {
158 2
        $this->output = $output;
159
160 2
        return $this;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 7
    public function getOutput(): OutputInterface
167
    {
168 7
        return $this->output;
169
    }
170
171
    /**
172
     * Returns pertinent frames.
173
     *
174
     * @param  \Whoops\Exception\Inspector  $inspector
175
     *
176
     * @return array
177
     */
178 5
    protected function getFrames(Inspector $inspector): array
179
    {
180 5
        return $inspector->getFrames()
181 5
            ->filter(
182
                function ($frame) {
183 5
                    foreach ($this->ignore as $ignore) {
184 1
                        if (preg_match($ignore, $frame->getFile())) {
185 1
                            return false;
186
                        }
187
                    }
188
189 5
                    return true;
190 5
                }
191
            )
192 5
            ->getArray();
193
    }
194
195
    /**
196
     * Renders the title of the exception.
197
     *
198
     * @param  \Whoops\Exception\Inspector  $inspector
199
     *
200
     * @return \NunoMaduro\Collision\Contracts\Writer
201
     */
202 5
    protected function renderTitle(Inspector $inspector): WriterContract
203
    {
204 5
        $exception = $inspector->getException();
205 5
        $message = $exception->getMessage();
206 5
        $class = $inspector->getExceptionName();
207
208 5
        $this->render("<fg=red;options=bold> $class </>");
209 5
        $this->output->writeln("   $message");
210
211 5
        return $this;
212
    }
213
214
    /**
215
     * Renders the solution of the exception, if any.
216
     *
217
     * @param  \Whoops\Exception\Inspector  $inspector
218
     *
219
     * @return \NunoMaduro\Collision\Contracts\Writer
220
     */
221 5
    protected function renderSolution(Inspector $inspector): WriterContract
222
    {
223 5
        $throwable = $inspector->getException();
224 5
        $solutions = [];
225
226 5
        if ($throwable instanceof Solution) {
227
            $solutions[] = $throwable;
228
        }
229
230 5
        if ($throwable instanceof ProvidesSolution) {
231
            $solutions[] = $throwable->getSolution();
232
        }
233
234 5
        foreach ($solutions as $solution) {
235
            $this->output->newline();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method newline() does only exist in the following implementations of said interface: Illuminate\Console\OutputStyle, PHPStan\Command\ErrorsConsoleStyle, Symfony\Component\Console\Style\OutputStyle, Symfony\Component\Console\Style\SymfonyStyle.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
236
            /** @var \Facade\IgnitionContracts\Solution $solution */
237
            $title = $solution->getSolutionTitle();
238
            $description = $solution->getSolutionDescription();
239
            $links = $solution->getDocumentationLinks();
240
241
            $this->output->block("  $title \n  $description", null, 'fg=black;bg=green', ' ', true);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method block() does only exist in the following implementations of said interface: Illuminate\Console\OutputStyle, PHPStan\Command\ErrorsConsoleStyle, Symfony\Component\Console\Style\SymfonyStyle.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
242
243
            foreach ($links as $link) {
244
                $this->render($link);
245
            }
246
        }
247
248 5
        return $this;
249
    }
250
251
    /**
252
     * Renders the editor containing the code that was the
253
     * origin of the exception.
254
     *
255
     * @param  \Whoops\Exception\Frame  $frame
256
     *
257
     * @return \NunoMaduro\Collision\Contracts\Writer
258
     */
259 4
    protected function renderEditor(Frame $frame): WriterContract
260
    {
261 4
        $this->render('at <fg=green>' . $frame->getFile() . '</>' . ':<fg=green>' . $frame->getLine() . '</>');
262
263 4
        $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
264
265 4
        $this->output->writeln($content);
266
267 4
        return $this;
268
    }
269
270
    /**
271
     * Renders the trace of the exception.
272
     *
273
     * @param  array  $frames
274
     *
275
     * @return \NunoMaduro\Collision\Contracts\Writer
276
     */
277 4
    protected function renderTrace(array $frames): WriterContract
278
    {
279 4
        $this->render('<comment>Exception trace:</comment>');
280 4
        foreach ($frames as $i => $frame) {
281 4
            if ($i > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
282 3
                $this->render('<info>Please use the argument <fg=red>-v</> to see more details.</info>');
283 3
                break;
284
            }
285
286 4
            $file = $frame->getFile();
287 4
            $line = $frame->getLine();
288 4
            $class = empty($frame->getClass()) ? '' : $frame->getClass() . '::';
289 4
            $function = $frame->getFunction();
290 4
            $args = $this->argumentFormatter->format($frame->getArgs());
291 4
            $pos = str_pad((int) $i + 1, 4, ' ');
292
293 4
            $this->render("<comment><fg=cyan>$pos</>$class$function($args)</comment>");
294 4
            $this->render("    <fg=green>$file</>:<fg=green>$line</>", false);
295
        }
296
297 4
        return $this;
298
    }
299
300
    /**
301
     * Renders an message into the console.
302
     *
303
     * @param  string  $message
304
     * @param  bool  $break
305
     *
306
     * @return $this
307
     */
308 5
    protected function render(string $message, bool $break = true): WriterContract
309
    {
310 5
        if ($break) {
311 5
            $this->output->writeln('');
312
        }
313
314 5
        $this->output->writeln("  $message");
315
316 5
        return $this;
317
    }
318
319
    /**
320
     * Formats a message as a block of text.
321
     *
322
     * @param  string|array  $messages The message to write in the block
323
     * @param  string|null  $type The block type (added in [] on first line)
324
     * @param  string|null  $style The style to apply to the whole block
325
     * @param  string  $prefix The prefix for the block
326
     * @param  bool  $padding Whether to add vertical padding
327
     * @param  bool  $escape Whether to escape the message
328
     */
329
    protected function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true)
330
    {
331
        $messages = \is_array($messages) ? array_values($messages) : [$messages];
332
333
        $this->autoPrependBlock();
0 ignored issues
show
Bug introduced by
The method autoPrependBlock() does not seem to exist on object<NunoMaduro\Collision\Writer>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
334
        $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape));
0 ignored issues
show
Bug introduced by
The method createBlock() does not seem to exist on object<NunoMaduro\Collision\Writer>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method writeln() does not seem to exist on object<NunoMaduro\Collision\Writer>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
335
        $this->newLine();
0 ignored issues
show
Bug introduced by
The method newLine() does not seem to exist on object<NunoMaduro\Collision\Writer>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
336
    }
337
}
338