Passed
Push — master ( 708a84...159ce3 )
by Alexander
07:14
created

HtmlRenderer   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Test Coverage

Coverage 77.22%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 180
c 1
b 0
f 0
dl 0
loc 373
ccs 139
cts 180
cp 0.7722
rs 2.32
wmc 76

19 Methods

Rating   Name   Duplication   Size   Complexity  
B renderCallStack() 0 17 8
A __construct() 0 5 1
A render() 0 4 1
B renderCallStackItem() 0 25 9
A withTraceLine() 0 5 1
A renderVerbose() 0 4 1
A withMaxTraceLines() 0 5 1
A htmlEncode() 0 3 1
A renderRequest() 0 22 5
A createServerInformationLink() 0 29 6
A createFrameworkVersionLink() 0 3 1
A withMaxSourceLines() 0 5 1
C addTypeLinks() 0 32 11
A renderPreviousExceptions() 0 7 2
A getTypeUrl() 0 11 3
A isCoreFile() 0 3 2
A renderTemplate() 0 24 6
A renderCurl() 0 9 2
C argumentsToString() 0 45 14

How to fix   Complexity   

Complex Class

Complex classes like HtmlRenderer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HtmlRenderer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler;
6
7
use Alexkart\CurlBuilder\Command;
8
9
final class HtmlRenderer extends ThrowableRenderer
10
{
11
    private int $maxSourceLines = 19;
12
    private int $maxTraceLines = 13;
13
14
    private string $traceLine = '{html}';
15
16
    private string $templatePath;
17
18
    private string $errorTemplate;
19
    private string $exceptionTemplate;
20
21 5
    public function __construct(array $templates = [])
22
    {
23 5
        $this->templatePath = $templates['path'] ?? dirname(__DIR__) . '/templates';
24 5
        $this->errorTemplate = $templates['error'] ?? $this->templatePath . '/error.php';
25 5
        $this->exceptionTemplate = $templates['exception'] ?? $this->templatePath . '/exception.php';
26 5
    }
27
28
    public function withMaxSourceLines(int $maxSourceLines): self
29
    {
30
        $new = clone $this;
31
        $new->maxSourceLines = $maxSourceLines;
32
        return $new;
33
    }
34
35
    public function withMaxTraceLines(int $maxTraceLines): self
36
    {
37
        $new = clone $this;
38
        $new->maxTraceLines = $maxTraceLines;
39
        return $new;
40
    }
41
42
    public function withTraceLine(string $traceLine): self
43
    {
44
        $new = clone $this;
45
        $new->traceLine = $traceLine;
46
        return $new;
47
    }
48
49 3
    public function render(\Throwable $t): string
50
    {
51 3
        return $this->renderTemplate($this->errorTemplate, [
52 3
            'throwable' => $t,
53
        ]);
54
    }
55
56 2
    public function renderVerbose(\Throwable $t): string
57
    {
58 2
        return $this->renderTemplate($this->exceptionTemplate, [
59 2
            'throwable' => $t,
60
        ]);
61
    }
62
63 2
    private function htmlEncode(string $text): string
64
    {
65 2
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
66
    }
67
68 5
    private function renderTemplate(string $path, array $params): string
69
    {
70 5
        if (!file_exists($path)) {
71 1
            throw new \RuntimeException("Template not found at $path");
72
        }
73
74 4
        $renderer = function (): void {
75 4
            extract(func_get_arg(1), EXTR_OVERWRITE);
76 4
            require func_get_arg(0);
77 4
        };
78
79 4
        $obInitialLevel = ob_get_level();
80 4
        ob_start();
81 4
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $flag of ob_implicit_flush(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

81
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(/** @scrutinizer ignore-type */ false) : ob_implicit_flush(0);
Loading history...
82
        try {
83 4
            $renderer->bindTo($this)($path, $params);
84 4
            return ob_get_clean();
85
        } catch (\Throwable $e) {
86
            while (ob_get_level() > $obInitialLevel) {
87
                if (!@ob_end_clean()) {
88
                    ob_clean();
89
                }
90
            }
91
            throw $e;
92
        }
93
    }
94
95
    /**
96
     * Renders the previous exception stack for a given Exception.
97
     * @param \Throwable $t the exception whose precursors should be rendered.
98
     * @return string HTML content of the rendered previous exceptions.
99
     * Empty string if there are none.
100
     * @throws \Throwable
101
     */
102 1
    public function renderPreviousExceptions(\Throwable $t): string
103
    {
104 1
        if (($previous = $t->getPrevious()) !== null) {
105
            $templatePath = $this->templatePath . '/previousException.php';
106
            return $this->renderTemplate($templatePath, ['throwable' => $previous]);
107
        }
108 1
        return '';
109
    }
110
111
    /**
112
     * Renders a single call stack element.
113
     * @param string|null $file name where call has happened.
114
     * @param int|null $line number on which call has happened.
115
     * @param string|null $class called class name.
116
     * @param string|null $method called function/method name.
117
     * @param array $args array of method arguments.
118
     * @param int $index number of the call stack element.
119
     * @return string HTML content of the rendered call stack element.
120
     * @throws \Throwable
121
     */
122 1
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $method, array $args, int $index): string
123
    {
124 1
        $lines = [];
125 1
        $begin = $end = 0;
126 1
        if ($file !== null && $line !== null) {
127 1
            $line--; // adjust line number from one-based to zero-based
128 1
            $lines = @file($file);
129 1
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
130
                return '';
131
            }
132 1
            $half = (int)(($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
133 1
            $begin = $line - $half > 0 ? $line - $half : 0;
134 1
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
135
        }
136 1
        $templatePath = $this->templatePath . '/callStackItem.php';
137 1
        return $this->renderTemplate($templatePath, [
138 1
            'file' => $file,
139 1
            'line' => $line,
140 1
            'class' => $class,
141 1
            'method' => $method,
142 1
            'index' => $index,
143 1
            'lines' => $lines,
144 1
            'begin' => $begin,
145 1
            'end' => $end,
146 1
            'args' => $args,
147
        ]);
148
    }
149
150
    /**
151
     * Renders call stack.
152
     * @param \Throwable $t exception to get call stack from
153
     * @return string HTML content of the rendered call stack.
154
     * @throws \Throwable
155
     */
156 1
    public function renderCallStack(\Throwable $t): string
157
    {
158 1
        $out = '<ul>';
159 1
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
160 1
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
161 1
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
162 1
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
163 1
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
164 1
            $function = null;
165 1
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
166 1
                $function = $trace[$i]['function'];
167
            }
168 1
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
169 1
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
170
        }
171 1
        $out .= '</ul>';
172 1
        return $out;
173
    }
174
175
    /**
176
     * Determines whether given name of the file belongs to the framework.
177
     * @param string|null $file name to be checked.
178
     * @return bool whether given name of the file belongs to the framework.
179
     */
180 1
    public function isCoreFile(?string $file): bool
181
    {
182 1
        return $file === null || strpos((string)realpath($file), Info::frameworkPath() . DIRECTORY_SEPARATOR) === 0;
183
    }
184
185
    /**
186
     * Adds informational links to the given PHP type/class.
187
     * @param string $code type/class name to be linkified.
188
     * @param string|null $title custom title to use
189
     * @return string linkified with HTML type/class name.
190
     */
191 1
    private function addTypeLinks(string $code, string $title = null): string
0 ignored issues
show
Unused Code introduced by
The method addTypeLinks() 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...
192
    {
193 1
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
194 1
            [, $class, $method] = $matches;
195 1
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
196
        } else {
197 1
            $class = $code;
198 1
            $method = null;
199 1
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class);
200
        }
201 1
        $url = null;
202 1
        $shouldGenerateLink = true;
203 1
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
204
            try {
205 1
                $reflection = new \ReflectionClass($class);
206 1
                if ($reflection->hasMethod($method)) {
207 1
                    $reflectionMethod = $reflection->getMethod($method);
208 1
                    $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
209
                } else {
210 1
                    $shouldGenerateLink = false;
211
                }
212
            } catch (\Throwable $e) {
213
                $shouldGenerateLink = false;
214
            }
215
        }
216 1
        if ($shouldGenerateLink) {
217 1
            $url = $this->getTypeUrl($class, $method);
218
        }
219 1
        if ($url === null) {
220 1
            return $text;
221
        }
222 1
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
223
    }
224
225
    /**
226
     * Returns the informational link URL for a given PHP type/class.
227
     * @param string|null $class the type or class name.
228
     * @param string|null $method the method name.
229
     * @return string|null the informational link URL.
230
     * @see addTypeLinks()
231
     */
232 1
    private function getTypeUrl(?string $class, ?string $method): ?string
233
    {
234 1
        if (strncmp($class, 'Yiisoft\\', 8) !== 0) {
235 1
            return null;
236
        }
237 1
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
238 1
        $url = "http://www.yiiframework.com/doc-3.0/$page.html";
239 1
        if ($method) {
240 1
            $url .= "#$method()-detail";
241
        }
242 1
        return $url;
243
    }
244
245
    /**
246
     * Converts arguments array to its string representation.
247
     *
248
     * @param array $args arguments array to be converted
249
     * @return string string representation of the arguments array
250
     */
251 1
    public function argumentsToString(array $args): string
252
    {
253 1
        $count = 0;
254 1
        $isAssoc = $args !== array_values($args);
255 1
        foreach ($args as $key => $value) {
256 1
            $count++;
257 1
            if ($count >= 5) {
258 1
                if ($count > 5) {
259 1
                    unset($args[$key]);
260
                } else {
261 1
                    $args[$key] = '...';
262
                }
263 1
                continue;
264
            }
265 1
            if (is_object($value)) {
266 1
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
267 1
            } elseif (is_bool($value)) {
268 1
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
269 1
            } elseif (is_string($value)) {
270 1
                $fullValue = $this->htmlEncode($value);
271 1
                if (mb_strlen($value, 'UTF-8') > 32) {
272
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
273
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
274
                } else {
275 1
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
276
                }
277 1
            } elseif (is_array($value)) {
278 1
                unset($args[$key]);
279 1
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
280
            } elseif ($value === null) {
281
                $args[$key] = '<span class="keyword">null</span>';
282
            } elseif (is_resource($value)) {
283
                $args[$key] = '<span class="keyword">resource</span>';
284
            } else {
285
                $args[$key] = '<span class="number">' . $value . '</span>';
286
            }
287 1
            if (is_string($key)) {
288 1
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
289 1
            } elseif ($isAssoc) {
290
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
291
            }
292
        }
293
294 1
        ksort($args);
295 1
        return implode(', ', $args);
296
    }
297
298
    /**
299
     * Renders the information about request.
300
     * @return string the rendering result
301
     */
302 1
    public function renderRequest(): string
303
    {
304 1
        if ($this->request === null) {
305
            return '';
306
        }
307
308 1
        $request = $this->request;
309 1
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
310
311 1
        foreach ($request->getHeaders() as $name => $values) {
312 1
            if ($name === 'Host') {
313
                continue;
314
            }
315
316 1
            foreach ($values as $value) {
317 1
                $output .= "$name: $value\n";
318
            }
319
        }
320
321 1
        $output .= "\n" . $request->getBody() . "\n\n";
322
323 1
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
324
    }
325
326 1
    public function renderCurl(): string
327
    {
328
        try {
329 1
            $output = (new Command())->setRequest($this->request)->build();
330
        } catch (\Throwable $e) {
331
            $output = 'Error generating curl command: ' . $e->getMessage();
332
        }
333
334 1
        return $this->htmlEncode($output);
335
    }
336
337
338
    /**
339
     * Creates string containing HTML link which refers to the home page of determined web-server software
340
     * and its full name.
341
     * @return string server software information hyperlink.
342
     */
343 1
    public function createServerInformationLink(): string
344
    {
345 1
        if ($this->request === null) {
346
            return '';
347
        }
348
349
350 1
        $serverSoftware = $this->request->getServerParams()['SERVER_SOFTWARE'] ?? null;
351 1
        if ($serverSoftware === null) {
352 1
            return '';
353
        }
354
355
        $serverUrls = [
356
            'http://httpd.apache.org/' => ['apache'],
357
            'http://nginx.org/' => ['nginx'],
358
            'http://lighttpd.net/' => ['lighttpd'],
359
            'http://gwan.com/' => ['g-wan', 'gwan'],
360
            'http://iis.net/' => ['iis', 'services'],
361
            'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
362
        ];
363
364
        foreach ($serverUrls as $url => $keywords) {
365
            foreach ($keywords as $keyword) {
366
                if (stripos($serverSoftware, $keyword) !== false) {
367
                    return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($serverSoftware) . '</a>';
368
                }
369
            }
370
        }
371
        return '';
372
    }
373
374
    /**
375
     * Creates string containing HTML link which refers to the page with the current version
376
     * of the framework and version number text.
377
     * @return string framework version information hyperlink.
378
     */
379 1
    public function createFrameworkVersionLink(): string
380
    {
381 1
        return '<a href="http://github.com/yiisoft/app/" target="_blank">' . $this->htmlEncode(Info::frameworkVersion()) . '</a>';
382
    }
383
}
384