Passed
Pull Request — master (#25)
by Evgeniy
02:04
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 rtrim;
37
use function stripos;
38
39
/**
40
 * Formats exception into HTML string.
41
 */
42
final class HtmlRenderer extends ThrowableRenderer
43
{
44
    /**
45
     * @var string The full path to the default template directory.
46
     */
47
    private string $defaultTemplatePath;
48
49
    /**
50
     * @var string The full path of the template file for rendering exceptions without call stack information.
51
     *
52
     * This template should be used in production.
53
     */
54
    private string $template;
55
56
    /**
57
     * @var string The full path of the template file for rendering exceptions with call stack information.
58
     *
59
     * This template should be used in development.
60
     */
61
    private string $verboseTemplate;
62
63
    /**
64
     * @var int The maximum number of source code lines to be displayed. Defaults to 19.
65
     */
66
    private int $maxSourceLines;
67
68
    /**
69
     * @var int The maximum number of trace source code lines to be displayed. Defaults to 13.
70
     */
71
    private int $maxTraceLines;
72
73
    /**
74
     * @param array $settings Settings can have the following keys:
75
     * - template: string, full path of the template file for rendering exceptions without call stack information.
76
     * - verboseTemplate: string, full path of the template file for rendering exceptions with call stack information.
77
     * - maxSourceLines: int, maximum number of source code lines to be displayed. Defaults to 19.
78
     * - maxTraceLines: int, maximum number of trace source code lines to be displayed. Defaults to 13.
79
     */
80 5
    public function __construct(array $settings = [])
81
    {
82 5
        $this->defaultTemplatePath = dirname(__DIR__, 2) . '/templates';
83 5
        $this->template = $settings['template'] ?? $this->defaultTemplatePath . '/production.php';
84 5
        $this->verboseTemplate = $settings['verboseTemplate'] ?? $this->defaultTemplatePath . '/development.php';
85 5
        $this->maxSourceLines = $settings['maxSourceLines']  ?? 19;
86 5
        $this->maxTraceLines = $settings['maxTraceLines']  ?? 13;
87 5
    }
88
89 3
    public function render(Throwable $t, ServerRequestInterface $request = null): string
90
    {
91 3
        return $this->renderTemplate($this->template, [
92 3
            'request' => $request,
93 3
            'throwable' => $t,
94
        ]);
95
    }
96
97 2
    public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): string
98
    {
99 2
        return $this->renderTemplate($this->verboseTemplate, [
100 2
            'request' => $request,
101 2
            'throwable' => $t,
102
        ]);
103
    }
104
105
    /**
106
     * Encodes special characters into HTML entities for use as a content.
107
     *
108
     * @param string $content The content to be encoded.
109
     *
110
     * @return string Encoded content.
111
     */
112 2
    private function htmlEncode(string $content): string
113
    {
114 2
        return htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
115
    }
116
117
    /**
118
     * Renders a template.
119
     *
120
     * @param string $path The full path of the template file for rendering.
121
     * @param array $parameters The name-value pairs that will be extracted and made available in the template file.
122
     *
123
     * @throws Throwable
124
     *
125
     * @return string The rendering result.
126
     */
127 5
    private function renderTemplate(string $path, array $parameters): string
128
    {
129 5
        if (!file_exists($path)) {
130 1
            throw new RuntimeException("Template not found at $path");
131
        }
132
133 4
        $renderer = function (): void {
134 4
            extract(func_get_arg(1), EXTR_OVERWRITE);
135 4
            require func_get_arg(0);
136 4
        };
137
138 4
        $obInitialLevel = ob_get_level();
139 4
        ob_start();
140 4
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
141
142
        try {
143 4
            $renderer->bindTo($this)($path, $parameters);
144 4
            return ob_get_clean();
145
        } catch (Throwable $e) {
146
            while (ob_get_level() > $obInitialLevel) {
147
                if (!@ob_end_clean()) {
148
                    ob_clean();
149
                }
150
            }
151
            throw $e;
152
        }
153
    }
154
155
    /**
156
     * Renders the previous exception stack for a given Exception.
157
     *
158
     * @param Throwable $t The exception whose precursors should be rendered.
159
     *
160
     * @throws Throwable
161
     *
162
     * @return string HTML content of the rendered previous exceptions. Empty string if there are none.
163
     */
164 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...
165
    {
166 1
        if (($previous = $t->getPrevious()) !== null) {
167
            $templatePath = $this->defaultTemplatePath . '/_previous-exception.php';
168
            return $this->renderTemplate($templatePath, ['throwable' => $previous]);
169
        }
170
171 1
        return '';
172
    }
173
174
    /**
175
     * Renders call stack.
176
     *
177
     * @param Throwable $t The exception to get call stack from.
178
     *
179
     * @throws Throwable
180
     *
181
     * @return string HTML content of the rendered call stack.
182
     */
183 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...
184
    {
185 1
        $out = '<ul>';
186 1
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
187
188 1
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
189 1
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
190 1
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
191 1
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
192 1
            $function = null;
193 1
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
194 1
                $function = $trace[$i]['function'];
195
            }
196 1
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
197 1
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
198
        }
199
200 1
        $out .= '</ul>';
201 1
        return $out;
202
    }
203
204
    /**
205
     * Renders a single call stack element.
206
     *
207
     * @param string|null $file The name where call has happened.
208
     * @param int|null $line The number on which call has happened.
209
     * @param string|null $class The called class name.
210
     * @param string|null $function The called function/method name.
211
     * @param array $args The array of method arguments.
212
     * @param int $index The number of the call stack element.
213
     *
214
     * @throws Throwable
215
     *
216
     * @return string HTML content of the rendered call stack element.
217
     */
218 1
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $function, array $args, int $index): string
219
    {
220 1
        $lines = [];
221 1
        $begin = $end = 0;
222
223 1
        if ($file !== null && $line !== null) {
224 1
            $line--; // adjust line number from one-based to zero-based
225 1
            $lines = @file($file);
226 1
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
227
                return '';
228
            }
229 1
            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
230 1
            $begin = $line - $half > 0 ? $line - $half : 0;
231 1
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
232
        }
233
234 1
        return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [
235 1
            'file' => $file,
236 1
            'line' => $line,
237 1
            'class' => $class,
238 1
            'function' => $function,
239 1
            'index' => $index,
240 1
            'lines' => $lines,
241 1
            'begin' => $begin,
242 1
            'end' => $end,
243 1
            'args' => $args,
244
        ]);
245
    }
246
247
    /**
248
     * Converts arguments array to its string representation.
249
     *
250
     * @param array $args arguments array to be converted
251
     *
252
     * @return string The string representation of the arguments array.
253
     */
254 1
    private function argumentsToString(array $args): string
255
    {
256 1
        $count = 0;
257 1
        $isAssoc = $args !== array_values($args);
258
259 1
        foreach ($args as $key => $value) {
260 1
            $count++;
261
262 1
            if ($count >= 5) {
263 1
                if ($count > 5) {
264 1
                    unset($args[$key]);
265
                } else {
266 1
                    $args[$key] = '...';
267
                }
268 1
                continue;
269
            }
270
271 1
            if (is_object($value)) {
272 1
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
273 1
            } elseif (is_bool($value)) {
274 1
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
275 1
            } elseif (is_string($value)) {
276 1
                $fullValue = $this->htmlEncode($value);
277 1
                if (mb_strlen($value, 'UTF-8') > 32) {
278
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
279
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
280
                } else {
281 1
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
282
                }
283 1
            } elseif (is_array($value)) {
284 1
                unset($args[$key]);
285 1
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
286
            } elseif ($value === null) {
287
                $args[$key] = '<span class="keyword">null</span>';
288
            } elseif (is_resource($value)) {
289
                $args[$key] = '<span class="keyword">resource</span>';
290
            } else {
291
                $args[$key] = '<span class="number">' . $value . '</span>';
292
            }
293
294 1
            if (is_string($key)) {
295 1
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
296 1
            } elseif ($isAssoc) {
297
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
298
            }
299
        }
300
301 1
        ksort($args);
302 1
        return implode(', ', $args);
303
    }
304
305
    /**
306
     * Renders the information about request.
307
     *
308
     * @param ServerRequestInterface $request
309
     *
310
     * @return string The rendering result.
311
     */
312 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...
313
    {
314 1
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
315
316 1
        foreach ($request->getHeaders() as $name => $values) {
317 1
            if ($name === 'Host') {
318
                continue;
319
            }
320
321 1
            foreach ($values as $value) {
322 1
                $output .= "$name: $value\n";
323
            }
324
        }
325
326 1
        $output .= "\n" . $request->getBody() . "\n\n";
327
328 1
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
329
    }
330
331
    /**
332
     * Renders the information about curl request.
333
     *
334
     * @param ServerRequestInterface $request
335
     *
336
     * @return string The rendering result.
337
     */
338 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...
339
    {
340
        try {
341 1
            $output = (new Command())->setRequest($request)->build();
342
        } catch (Throwable $e) {
343
            $output = 'Error generating curl command: ' . $e->getMessage();
344
        }
345
346 1
        return $this->htmlEncode($output);
347
    }
348
349
    /**
350
     * Creates string containing HTML link which refers to the home page
351
     * of determined web-server software and its full name.
352
     *
353
     * @param ServerRequestInterface $request
354
     *
355
     * @return string The server software information hyperlink.
356
     */
357 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...
358
    {
359 1
        $serverSoftware = $request->getServerParams()['SERVER_SOFTWARE'] ?? null;
360
361 1
        if ($serverSoftware === null) {
362 1
            return '';
363
        }
364
365
        $serverUrls = [
366
            'https://httpd.apache.org/' => ['apache'],
367
            'https://nginx.org/' => ['nginx'],
368
            'https://lighttpd.net/' => ['lighttpd'],
369
            'https://iis.net/' => ['iis', 'services'],
370
            'https://www.php.net/manual/en/features.commandline.webserver.php' => ['development'],
371
        ];
372
373
        foreach ($serverUrls as $url => $keywords) {
374
            foreach ($keywords as $keyword) {
375
                if (stripos($serverSoftware, $keyword) !== false) {
376
                    return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">'
377
                        . $this->htmlEncode($serverSoftware) . '</a>';
378
                }
379
            }
380
        }
381
382
        return '';
383
    }
384
}
385