Passed
Push — master ( 1aec81...38365c )
by Marwan
06:35
created

Dumper::dump()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 57
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 35
c 3
b 0
f 0
nc 12
nop 1
dl 0
loc 57
rs 8.4266

How to fix   Long Method   

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
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox\Helper;
13
14
use MAKS\Velox\Frontend\HTML;
15
16
/**
17
 * A class that dumps variables and exception in a nice formatting.
18
 */
19
class Dumper
20
{
21
    /**
22
     * Accent color of exceptions page and dump block.
23
     */
24
    public static string $accentColor = '#ff3a60';
25
26
    /**
27
     * Contrast color of exceptions page and dump block.
28
     */
29
    public static string $contrastColor = '#030035';
30
31
    /**
32
     * Colors of syntax tokens.
33
     *
34
     * @var string[]
35
     */
36
    public static array $syntaxHighlightColors = [
37
        'comment' => '#aeaeae',
38
        'keyword' => '#00bfff',
39
        'string'  => '#e4ba80',
40
        'default' => '#e8703a',
41
        'html'    => '#ab8703',
42
    ];
43
44
    /**
45
     * Additional CSS styling of syntax tokens.
46
     *
47
     * @var string[]
48
     */
49
    public static array $syntaxHighlightStyles = [
50
        'comment' => 'font-weight: lighter;',
51
        'keyword' => 'font-weight: bold;',
52
        'string'  => '',
53
        'default' => '',
54
        'html'    => '',
55
    ];
56
57
    private static array $syntaxHighlightTokens = ['comment', 'keyword', 'string', 'default', 'html'];
58
59
60
61
    /**
62
     * Dumps a variable and dies.
63
     *
64
     * @param mixed ...$variable
65
     *
66
     * @return void The result will simply get echoed.
67
     */
68
    public static function dd(...$variable): void
69
    {
70
        self::dump(...$variable);
71
        die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
72
    }
73
74
    /**
75
     * Dumps a variable in a nice HTML block with syntax highlighting.
76
     *
77
     * @param mixed ...$variable
78
     *
79
     * @return void The result will simply get echoed.
80
     */
81
    public static function dump(...$variable): void
82
    {
83
        $isCli = self::isCli();
84
85
        $accentColor   = self::$accentColor;
86
        $contrastColor = self::$contrastColor;
87
88
        if (!$isCli) {
89
            self::setSyntaxHighlighting();
90
        }
91
92
        $trace = 'Trace: N/A';
93
        $backtrace = array_filter(array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), function($backtrace) use (&$trace) {
0 ignored issues
show
Unused Code introduced by
The assignment to $backtrace is dead and can be removed.
Loading history...
94
            static $hasFound = false;
95
            if (!$hasFound && in_array($backtrace['function'], ['dump', 'dd'])) {
96
                $trace = $backtrace['file'] . ':' . $backtrace['line'];
97
                $hasFound = true;
98
99
                return true;
100
            }
101
102
            return false;
103
        });
104
105
        $markup = [
106
            'traceBlock' => HTML::div($trace, [
107
                'style' => "background:#fff;color:{$accentColor};font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:12px;padding:4px 8px;margin-bottom:18px;"
108
            ]),
109
            'dumpBlock'  => HTML::div('%s', [
110
                'style' => "display:table;background:{$contrastColor};color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:18px;padding:18px;margin:8px;"
111
            ]),
112
            'statsBlock' => HTML::div('START_TIME + %.2fms', [
113
                'style' => "display:table;background:{$accentColor};color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:12px;font-weight:bold;padding:12px;margin:8px;"
114
            ]),
115
        ];
116
117
        foreach ($variable as $dump) {
118
            if (!$isCli) {
119
                $code = highlight_string('<?php ' . self::exportExpression($dump), true);
120
                $html = sprintf(
121
                    $markup['dumpBlock'],
0 ignored issues
show
Bug introduced by
$markup['dumpBlock'] of type MAKS\Velox\Frontend\HTML is incompatible with the type string expected by parameter $format of sprintf(). ( Ignorable by Annotation )

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

121
                    /** @scrutinizer ignore-type */ $markup['dumpBlock'],
Loading history...
122
                    preg_replace(
123
                        '/&lt;\?php&nbsp;/',
124
                        $markup['traceBlock'],
0 ignored issues
show
Bug introduced by
$markup['traceBlock'] of type MAKS\Velox\Frontend\HTML is incompatible with the type string|string[] expected by parameter $replacement of preg_replace(). ( Ignorable by Annotation )

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

124
                        /** @scrutinizer ignore-type */ $markup['traceBlock'],
Loading history...
125
                        $code
126
                    )
127
                );
128
129
                echo $html;
130
            } else {
131
                echo self::exportExpression($dump);
132
            }
133
        }
134
135
        $time  = (microtime(true) - START_TIME) * 1000;
136
        $stats = $isCli ? "\n[%.2fms]\n" : $markup['statsBlock'];
137
        echo sprintf($stats, $time);
138
    }
139
140
    /**
141
     * Dumps an exception in a nice HTML page or as string and exits the script.
142
     *
143
     * @param \Throwable $exception
144
     *
145
     * @return void The result will be echoed as HTML page or a string representation of the exception if the interface is CLI.
146
     */
147
    public static function dumpException(\Throwable $exception): void
148
    {
149
        if (self::isCli()) {
150
            echo $exception;
151
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
152
        }
153
154
        self::setSyntaxHighlighting();
155
156
        $file        = $exception->getFile();
157
        $line        = $exception->getLine();
158
        $message     = $exception->getMessage();
159
        $trace       = $exception->getTrace();
160
        $traceString = $exception->getTraceAsString();
161
        $name        = get_class($exception);
162
        $filename    = basename($file);
163
        $lines       = null;
164
165
        if (file_exists($file)) {
166
            $lines = file($file);
167
        }
168
169
        $accentColor   = self::$accentColor;
170
        $contrastColor = self::$contrastColor;
171
172
        $style = ":root{--accent-color:{$accentColor};--contrast-color:{$contrastColor}}*,::after,::before{box-sizing:border-box}body{background:#fff;font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;margin:0}h1,h2,h3,h4,h5,h6{margin:0}h1,h2{color:var(--accent-color)}h1{font-size:32px}h2{font-size:28px}h3{color:#fff}.container{width:85vw;max-width:1200px;min-height:100vh;background:#fff;padding:7vh 3vw 10vh 3vw;margin:0 auto;overflow:hidden}.message{background:var(--accent-color);color:#fff;padding:2em 1em;margin:0 0 3em 0;}.code{overflow-y:scroll;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:14px;margin:0 0 3em 0;-ms-overflow-style: none;scrollbar-width: none}.code::-webkit-scrollbar{display:none}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}ul{padding:2em 1em;margin:1em 0;background:var(--contrast-color)}ul li{white-space:pre;list-style-type:none;font-family:monospace}ul li span.line{display:inline-block;color:#fff;text-align:right;padding:4px 8px;user-select:none}ul li.exception-line span.line{color:var(--accent-color);font-weight:bold}ul li.exception-line span.line+code>span>span:not(:first-child){padding-bottom:3px;border-bottom:2px solid var(--accent-color)}table{width:100%;border-collapse:collapse;border-spacing:0}table th{background:var(--contrast-color);color:#fff;text-align:left;padding-top:12px;padding-bottom:12px}table td,table th{border-bottom:1px solid rgba(0,0,0,0.15);padding:6px}table tr:nth-child(even){background-color:rgba(0,0,0,0.05)}table td.number{text-align:left}table td.line{text-align:left}table td.class,table td.function{font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:14px;font-weight:700}table td.arguments span{display:inline-block;background:rgba(0,0,0,.15);color:var(--accent-color);font-style:italic;padding:2px 4px;margin:0 4px 0 0;border-radius:4px}";
173
174
        (new HTML(false))
175
            ->node('<!DOCTYPE html>')
176
            ->open('html', ['lang' => 'en'])
177
                ->open('head')
178
                    ->title('Oops, something went wrong')
179
                    ->style($style)
180
                ->close()
181
                ->open('body')
182
                    ->open('div', ['class' => 'container'])
183
                        ->h1("Uncaught {$name}")
184
                        ->p("An <b>{$name}</b> was thrown on line {$line} of file {$filename} which prevented further execution of the code.")
185
                        ->open('div', ['class' => 'message'])
186
                            ->h3($name)
187
                            ->p($message)
188
                        ->close()
189
                        ->h2('Thrown in:')
190
                        ->execute(function (HTML $html) use ($file, $line, $lines) {
191
                            if (isset($lines)) {
192
                                $html->p($file);
193
                                $html->open('ul', ['class' => 'code']);
194
                                for ($i = $line - 3; $i < $line + 4; $i++) {
195
                                    if ($i > 0 && $i < count($lines)) {
196
                                        $highlightedCode = highlight_string('<?php ' . $lines[$i], true);
197
                                        $highlightedCode = preg_replace(
198
                                            ['/\n/', '/<br ?\/?>/', '/&lt;\?php&nbsp;/'],
199
                                            ['', '', ''],
200
                                            $highlightedCode
201
                                        );
202
                                        if ($i == $line - 1) {
203
                                            $arrow = str_pad('>', strlen("{$i}"), '=', STR_PAD_LEFT);
204
                                            $html
205
                                                ->open('li', ['class' => 'exception-line'])
206
                                                    ->span($arrow, ['class' => 'line'])
207
                                                    ->node($highlightedCode)
208
                                                ->close();
209
                                        } else {
210
                                            $number = strval($i + 1);
211
                                            $html
212
                                                ->open('li')
213
                                                    ->span($number, ['class' => 'line'])
214
                                                    ->node($highlightedCode)
215
                                                ->close();
216
                                        }
217
                                    }
218
                                }
219
                                $html->close();
220
                            }
221
                        })
222
                        ->h2('Stack trace:')
223
                        ->execute(function (HTML $html) use ($trace, $traceString) {
224
                            if (count($trace)) {
225
                                $html->p('<i>Hover on fields with * to reveal more info.</i>');
226
                                $html->open('table', ['class' => 'trace'])
227
                                    ->open('thead')
228
                                        ->open('tr')
229
                                            ->th('No.')
230
                                            ->th('File *')
231
                                            ->th('Line')
232
                                            ->th('Class')
233
                                            ->th('Function')
234
                                            ->th('Arguments *')
235
                                        ->close()
236
                                    ->close()
237
                                    ->open('tbody')
238
                                    ->execute(function (HTML $html) use ($trace) {
239
                                        foreach ($trace as $i => $trace) {
240
                                            $count = (int)$i + 1;
241
                                            $html
242
                                            ->open('tr', ['class' => $count % 2 == 0 ? 'even' : 'odd'])
243
                                                ->td(strval($count), ['class' => 'number'])
244
                                                ->td(isset($trace['file']) ? basename($trace['file']) : '', ['class' => 'file', 'title' => $trace['file'] ?? false])
245
                                                ->td(strval($trace['line'] ?? ''), ['class' => 'line'])
246
                                                ->td(strval($trace['class'] ?? ''), ['class' => 'class'])
247
                                                ->td(strval($trace['function'] ?? ''), ['class' => 'function'])
248
                                                ->open('td', ['class' => 'arguments'])
249
                                                ->execute(function (HTML $html) use ($trace) {
250
                                                    if (isset($trace['args'])) {
251
                                                        foreach ($trace['args'] as $i => $arg) {
252
                                                            $html->span(gettype($arg), ['title' => print_r($arg, true)]);
253
                                                        }
254
                                                    } else {
255
                                                        $html->node('NULL');
256
                                                    }
257
                                                })
258
                                                ->close()
259
                                            ->close();
260
                                        }
261
                                    })
262
                                    ->close()
263
                                ->close();
264
                            } else {
265
                                $html->pre($traceString);
266
                            }
267
                        })
268
                    ->close()
269
                ->close()
270
            ->close()
271
        ->echo();
272
273
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
274
    }
275
276
    /**
277
     * Dumps an expression using `var_export()` or `print_r()`.
278
     *
279
     * @param mixed $expression
280
     * @return string
281
     */
282
    private static function exportExpression($expression): string
283
    {
284
        $export = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $export is dead and can be removed.
Loading history...
285
286
        try {
287
            $export = var_export($expression, true);
288
        } catch (\Throwable $e) {
289
            $class = self::class;
290
            $line1 = "// {$class} failed to dump the variable. Reason: {$e->getMessage()}. " . PHP_EOL;
291
            $line2 = "// here is a dump of the variable using print_r()" . PHP_EOL . PHP_EOL . PHP_EOL;
292
293
            return $line1 . $line2 . print_r($expression, true);
0 ignored issues
show
Bug introduced by
Are you sure print_r($expression, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

293
            return $line1 . $line2 . /** @scrutinizer ignore-type */ print_r($expression, true);
Loading history...
294
        }
295
296
        // convert array construct to square brackets
297
        $afToSqPatterns = [
298
            '/(\()array\(/'                    => '$1[',
299
            '/\)(\))/'                         => ']$1',
300
            '/array \(/'                       => '[',
301
            '/^([ ]*)\)(,?)$/m'                => '$1]$2',
302
            '/=>[ ]?\n[ ]+\[/'                 => '=> [',
303
            '/([ ]*)(\'[^\']+\') => ([\[\'])/' => '$1$2 => $3',
304
        ];
305
306
        return preg_replace(
307
            array_keys($afToSqPatterns),
308
            array_values($afToSqPatterns),
309
            $export
310
        );
311
    }
312
313
    private static function setSyntaxHighlighting(): void
314
    {
315
        $tokens = self::$syntaxHighlightTokens;
316
317
        foreach ($tokens as $token) {
318
            $color = self::$syntaxHighlightColors[$token] ?? ini_get("highlight.{$token}");
319
            $style = self::$syntaxHighlightStyles[$token] ?? chr(8);
320
321
            $highlighting = sprintf('%s;%s', $color, $style);
322
323
            ini_set("highlight.{$token}", $highlighting);
324
        }
325
    }
326
327
    private static function isCli(): bool
328
    {
329
        return strpos(php_sapi_name(), 'cli') !== false;
330
    }
331
}
332