Passed
Pull Request — master (#217)
by
unknown
01:45
created

HtmlRenderer   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 173
dl 0
loc 362
ccs 0
cts 226
cp 0
rs 2.56
c 4
b 0
f 0
wmc 73

19 Methods

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

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