Passed
Pull Request — master (#25)
by Evgeniy
02:24
created

HtmlRenderer::argumentsToString()   C

Complexity

Conditions 14
Paths 30

Size

Total Lines 49
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 16.5534

Importance

Changes 0
Metric Value
eloc 36
dl 0
loc 49
ccs 26
cts 34
cp 0.7647
rs 6.2666
c 0
b 0
f 0
cc 14
nc 30
nop 1
crap 16.5534

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler\Renderer;
6
7
use Alexkart\CurlBuilder\Command;
8
use Psr\Http\Message\ServerRequestInterface;
9
use RuntimeException;
10
use Throwable;
11
use Yiisoft\ErrorHandler\ThrowableRenderer;
12
13
use function array_values;
14
use function dirname;
15
use function extract;
16
use function file;
17
use function file_exists;
18
use function func_get_arg;
19
use function get_class;
20
use function htmlspecialchars;
21
use function implode;
22
use function is_array;
23
use function is_bool;
24
use function is_object;
25
use function is_resource;
26
use function is_string;
27
use function ksort;
28
use function mb_strlen;
29
use function mb_substr;
30
use function ob_clean;
31
use function ob_get_clean;
32
use function ob_get_level;
33
use function ob_end_clean;
34
use function ob_implicit_flush;
35
use function ob_start;
36
use function realpath;
37
use function rtrim;
38
use function stripos;
39
use function strpos;
40
41
/**
42
 * Formats exception into HTML string.
43
 */
44
final class HtmlRenderer extends ThrowableRenderer
45
{
46
    /**
47
     * @var string The full path to the default template directory.
48
     */
49
    private string $defaultTemplatePath;
50
51
    /**
52
     * @var string The full path of the template file for rendering exceptions without call stack information.
53
     *
54
     * This template should be used in production.
55
     */
56
    private string $template;
57
58
    /**
59
     * @var string The full path of the template file for rendering exceptions with call stack information.
60
     *
61
     * This template should be used in development.
62
     */
63
    private string $verboseTemplate;
64
65
    /**
66
     * @var int The maximum number of source code lines to be displayed. Defaults to 19.
67
     */
68
    private int $maxSourceLines;
69
70
    /**
71
     * @var int The maximum number of trace source code lines to be displayed. Defaults to 13.
72
     */
73
    private int $maxTraceLines;
74
75
    /**
76
     * @var string|null The trace header line with placeholders to be be substituted. Defaults to null.
77
     *
78
     * The placeholders are {file}, {line} and {ide}. A typical use case is the creation of IDE-specific links,
79
     * since when you click on a trace header link, it opens directly in the IDE. You can also insert custom content.
80
     *
81
     * Example IDE link:
82
     *
83
     * ```
84
     * <a href="ide://open?file={file}&line={line}">{ide}</a>
85
     * ```
86
     */
87
    private ?string $traceHeaderLine;
88
89
    /**
90
     * @param array $settings Settings can have the following keys:
91
     * - template: string, full path of the template file for rendering exceptions without call stack information.
92
     * - verboseTemplate: string, full path of the template file for rendering exceptions with call stack information.
93
     * - maxSourceLines: int, maximum number of source code lines to be displayed. Defaults to 19.
94
     * - maxTraceLines: int, maximum number of trace source code lines to be displayed. Defaults to 13.
95
     * - traceHeaderLine: string, trace header line with placeholders to be be substituted. Defaults to null.
96
     */
97 5
    public function __construct(array $settings = [])
98
    {
99 5
        $this->defaultTemplatePath = dirname(__DIR__, 2) . '/templates';
100 5
        $this->template = $settings['template'] ?? $this->defaultTemplatePath . '/production.php';
101 5
        $this->verboseTemplate = $settings['verboseTemplate'] ?? $this->defaultTemplatePath . '/development.php';
102 5
        $this->maxSourceLines = $settings['maxSourceLines']  ?? 19;
103 5
        $this->maxTraceLines = $settings['maxTraceLines']  ?? 13;
104 5
        $this->traceHeaderLine = $settings['traceHeaderLine'] ?? null;
105 5
    }
106
107 3
    public function render(Throwable $t, ServerRequestInterface $request = null): string
108
    {
109 3
        return $this->renderTemplate($this->template, [
110 3
            'request' => $request,
111 3
            'throwable' => $t,
112
        ]);
113
    }
114
115 2
    public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): string
116
    {
117 2
        return $this->renderTemplate($this->verboseTemplate, [
118 2
            'request' => $request,
119 2
            'throwable' => $t,
120
        ]);
121
    }
122
123
    /**
124
     * Encodes special characters into HTML entities for use as a content.
125
     *
126
     * @param string $content The content to be encoded.
127
     *
128
     * @return string Encoded content.
129
     */
130 2
    private function htmlEncode(string $content): string
131
    {
132 2
        return htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
133
    }
134
135
    /**
136
     * Renders a template.
137
     *
138
     * @param string $path The full path of the template file for rendering.
139
     * @param array $parameters The name-value pairs that will be extracted and made available in the template file.
140
     *
141
     * @throws Throwable
142
     *
143
     * @return string The rendering result.
144
     */
145 5
    private function renderTemplate(string $path, array $parameters): string
146
    {
147 5
        if (!file_exists($path)) {
148 1
            throw new RuntimeException("Template not found at $path");
149
        }
150
151 4
        $renderer = function (): void {
152 4
            extract(func_get_arg(1), EXTR_OVERWRITE);
153 4
            require func_get_arg(0);
154 4
        };
155
156 4
        $obInitialLevel = ob_get_level();
157 4
        ob_start();
158 4
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
159
160
        try {
161 4
            $renderer->bindTo($this)($path, $parameters);
162 4
            return ob_get_clean();
163
        } catch (Throwable $e) {
164
            while (ob_get_level() > $obInitialLevel) {
165
                if (!@ob_end_clean()) {
166
                    ob_clean();
167
                }
168
            }
169
            throw $e;
170
        }
171
    }
172
173
    /**
174
     * Renders the previous exception stack for a given Exception.
175
     *
176
     * @param Throwable $t The exception whose precursors should be rendered.
177
     *
178
     * @throws Throwable
179
     *
180
     * @return string HTML content of the rendered previous exceptions. Empty string if there are none.
181
     */
182 1
    private function renderPreviousExceptions(Throwable $t): string
0 ignored issues
show
Unused Code introduced by
The method renderPreviousExceptions() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
183
    {
184 1
        if (($previous = $t->getPrevious()) !== null) {
185
            $templatePath = $this->defaultTemplatePath . '/_previous-exception.php';
186
            return $this->renderTemplate($templatePath, ['throwable' => $previous]);
187
        }
188
189 1
        return '';
190
    }
191
192
    /**
193
     * Renders call stack.
194
     *
195
     * @param Throwable $t The exception to get call stack from.
196
     *
197
     * @throws Throwable
198
     *
199
     * @return string HTML content of the rendered call stack.
200
     */
201 1
    private function renderCallStack(Throwable $t): string
0 ignored issues
show
Unused Code introduced by
The method renderCallStack() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
202
    {
203 1
        $out = '<ul>';
204 1
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
205
206 1
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
207 1
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
208 1
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
209 1
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
210 1
            $function = null;
211 1
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
212 1
                $function = $trace[$i]['function'];
213
            }
214 1
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
215 1
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
216
        }
217
218 1
        $out .= '</ul>';
219 1
        return $out;
220
    }
221
222
    /**
223
     * Renders a single call stack element.
224
     *
225
     * @param string|null $file The name where call has happened.
226
     * @param int|null $line The number on which call has happened.
227
     * @param string|null $class The called class name.
228
     * @param string|null $function The called function/method name.
229
     * @param array $args The array of method arguments.
230
     * @param int $index The number of the call stack element.
231
     *
232
     * @throws Throwable
233
     *
234
     * @return string HTML content of the rendered call stack element.
235
     */
236 1
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $function, array $args, int $index): string
237
    {
238 1
        $lines = [];
239 1
        $begin = $end = 0;
240
241 1
        if ($file !== null && $line !== null) {
242 1
            $line--; // adjust line number from one-based to zero-based
243 1
            $lines = @file($file);
244 1
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
245
                return '';
246
            }
247 1
            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
248 1
            $begin = $line - $half > 0 ? $line - $half : 0;
249 1
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
250
        }
251
252 1
        return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [
253 1
            'file' => $file,
254 1
            'line' => $line,
255 1
            'class' => $class,
256 1
            'function' => $function,
257 1
            'index' => $index,
258 1
            'lines' => $lines,
259 1
            'begin' => $begin,
260 1
            'end' => $end,
261 1
            'args' => $args,
262
        ]);
263
    }
264
265
    /**
266
     * Converts arguments array to its string representation.
267
     *
268
     * @param array $args arguments array to be converted
269
     *
270
     * @return string The string representation of the arguments array.
271
     */
272 1
    private function argumentsToString(array $args): string
273
    {
274 1
        $count = 0;
275 1
        $isAssoc = $args !== array_values($args);
276
277 1
        foreach ($args as $key => $value) {
278 1
            $count++;
279
280 1
            if ($count >= 5) {
281 1
                if ($count > 5) {
282 1
                    unset($args[$key]);
283
                } else {
284 1
                    $args[$key] = '...';
285
                }
286 1
                continue;
287
            }
288
289 1
            if (is_object($value)) {
290 1
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
291 1
            } elseif (is_bool($value)) {
292 1
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
293 1
            } elseif (is_string($value)) {
294 1
                $fullValue = $this->htmlEncode($value);
295 1
                if (mb_strlen($value, 'UTF-8') > 32) {
296
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
297
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
298
                } else {
299 1
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
300
                }
301 1
            } elseif (is_array($value)) {
302 1
                unset($args[$key]);
303 1
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
304
            } elseif ($value === null) {
305
                $args[$key] = '<span class="keyword">null</span>';
306
            } elseif (is_resource($value)) {
307
                $args[$key] = '<span class="keyword">resource</span>';
308
            } else {
309
                $args[$key] = '<span class="number">' . $value . '</span>';
310
            }
311
312 1
            if (is_string($key)) {
313 1
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
314 1
            } elseif ($isAssoc) {
315
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
316
            }
317
        }
318
319 1
        ksort($args);
320 1
        return implode(', ', $args);
321
    }
322
323
    /**
324
     * Renders the information about request.
325
     *
326
     * @param ServerRequestInterface $request
327
     *
328
     * @return string The rendering result.
329
     */
330 1
    private function renderRequest(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method renderRequest() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
331
    {
332 1
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
333
334 1
        foreach ($request->getHeaders() as $name => $values) {
335 1
            if ($name === 'Host') {
336
                continue;
337
            }
338
339 1
            foreach ($values as $value) {
340 1
                $output .= "$name: $value\n";
341
            }
342
        }
343
344 1
        $output .= "\n" . $request->getBody() . "\n\n";
345
346 1
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
347
    }
348
349
    /**
350
     * Renders the information about curl request.
351
     *
352
     * @param ServerRequestInterface $request
353
     *
354
     * @return string The rendering result.
355
     */
356 1
    private function renderCurl(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method renderCurl() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
357
    {
358
        try {
359 1
            $output = (new Command())->setRequest($request)->build();
360
        } catch (Throwable $e) {
361
            $output = 'Error generating curl command: ' . $e->getMessage();
362
        }
363
364 1
        return $this->htmlEncode($output);
365
    }
366
367
    /**
368
     * Creates string containing HTML link which refers to the home page
369
     * of determined web-server software and its full name.
370
     *
371
     * @param ServerRequestInterface $request
372
     *
373
     * @return string The server software information hyperlink.
374
     */
375 1
    private function createServerInformationLink(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method createServerInformationLink() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
376
    {
377 1
        $serverSoftware = $request->getServerParams()['SERVER_SOFTWARE'] ?? null;
378
379 1
        if ($serverSoftware === null) {
380 1
            return '';
381
        }
382
383
        $serverUrls = [
384
            'https://httpd.apache.org/' => ['apache'],
385
            'https://nginx.org/' => ['nginx'],
386
            'https://lighttpd.net/' => ['lighttpd'],
387
            'https://iis.net/' => ['iis', 'services'],
388
            'https://www.php.net/manual/en/features.commandline.webserver.php' => ['development'],
389
        ];
390
391
        foreach ($serverUrls as $url => $keywords) {
392
            foreach ($keywords as $keyword) {
393
                if (stripos($serverSoftware, $keyword) !== false) {
394
                    return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">'
395
                        . $this->htmlEncode($serverSoftware) . '</a>';
396
                }
397
            }
398
        }
399
400
        return '';
401
    }
402
403
    /**
404
     * Determines whether given name of the file belongs to the framework.
405
     *
406
     * @param string|null $file The name to be checked.
407
     *
408
     * @return bool Whether given name of the file belongs to the framework.
409
     */
410 1
    public function isCoreFile(?string $file): bool
411
    {
412 1
        return $file === null || strpos(realpath($file), dirname(__DIR__, 3)) === 0;
413
    }
414
}
415