1 | <?php |
||
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 |
|
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 |