Passed
Push — master ( 228c08...e198b9 )
by Marwan
01:18
created

Dumper::exportExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 19
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 28
rs 9.6333
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 = array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1))[0];
93
        $trace = $trace['file'] . ':' . $trace['line'];
94
95
        $markup = [
96
            'traceBlock' => HTML::div($trace, [
97
                '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;"
98
            ]),
99
            'dumpBlock'  => HTML::div('%s', [
100
                'style' => "display:table;background:{$contrastColor};color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:18px;padding:18px;margin:8px;"
101
            ]),
102
            'statsBlock' => HTML::div('START_TIME + %.2fms', [
103
                '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;"
104
            ]),
105
        ];
106
107
        foreach ($variable as $dump) {
108
            if (!$isCli) {
109
                $code = highlight_string('<?php ' . self::exportExpression($dump), true);
110
                $html = sprintf(
111
                    $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

111
                    /** @scrutinizer ignore-type */ $markup['dumpBlock'],
Loading history...
112
                    preg_replace(
113
                        '/&lt;\?php&nbsp;/',
114
                        $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

114
                        /** @scrutinizer ignore-type */ $markup['traceBlock'],
Loading history...
115
                        $code
116
                    )
117
                );
118
119
                echo $html;
120
            } else {
121
                echo self::exportExpression($dump);
122
            }
123
        }
124
125
        $time  = (microtime(true) - START_TIME) * 1000;
126
        $stats = $isCli ? "\n[%.2fms]\n" : $markup['statsBlock'];
127
        echo sprintf($stats, $time);
128
    }
129
130
    /**
131
     * Dumps an exception in a nice HTML page or as string and exits the script.
132
     *
133
     * @param \Throwable $exception
134
     *
135
     * @return void The result will be echoed as HTML page or a string representation of the exception if the interface is CLI.
136
     */
137
    public static function dumpException(\Throwable $exception): void
138
    {
139
        if (self::isCli()) {
140
            echo $exception;
141
            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...
142
        }
143
144
        self::setSyntaxHighlighting();
145
146
        $file        = $exception->getFile();
147
        $line        = $exception->getLine();
148
        $message     = $exception->getMessage();
149
        $trace       = $exception->getTrace();
150
        $traceString = $exception->getTraceAsString();
151
        $name        = get_class($exception);
152
        $filename    = basename($file);
153
        $lines       = null;
154
155
        if (file_exists($file)) {
156
            $lines = file($file);
157
        }
158
159
        $accentColor   = self::$accentColor;
160
        $contrastColor = self::$contrastColor;
161
162
        $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}";
163
164
        (new HTML(false))
165
            ->node('<!DOCTYPE html>')
166
            ->open('html', ['lang' => 'en'])
167
                ->open('head')
168
                    ->title('Oops, something went wrong')
169
                    ->style($style)
170
                ->close()
171
                ->open('body')
172
                    ->open('div', ['class' => 'container'])
173
                        ->h1("Uncaught {$name}")
174
                        ->p("An <b>{$name}</b> was thrown on line {$line} of file {$filename} which prevented further execution of the code.")
175
                        ->open('div', ['class' => 'message'])
176
                            ->h3($name)
177
                            ->p($message)
178
                        ->close()
179
                        ->h2('Thrown in:')
180
                        ->execute(function (HTML $html) use ($file, $line, $lines) {
181
                            if (isset($lines)) {
182
                                $html->p($file);
183
                                $html->open('ul', ['class' => 'code']);
184
                                for ($i = $line - 3; $i < $line + 4; $i++) {
185
                                    if ($i > 0 && $i < count($lines)) {
186
                                        $highlightedCode = highlight_string('<?php ' . $lines[$i], true);
187
                                        $highlightedCode = preg_replace(
188
                                            ['/\n/', '/<br ?\/?>/', '/&lt;\?php&nbsp;/'],
189
                                            ['', '', ''],
190
                                            $highlightedCode
191
                                        );
192
                                        if ($i == $line - 1) {
193
                                            $arrow = str_pad('>', strlen("{$i}"), '=', STR_PAD_LEFT);
194
                                            $html
195
                                                ->open('li', ['class' => 'exception-line'])
196
                                                    ->span($arrow, ['class' => 'line'])
197
                                                    ->node($highlightedCode)
198
                                                ->close();
199
                                        } else {
200
                                            $number = strval($i + 1);
201
                                            $html
202
                                                ->open('li')
203
                                                    ->span($number, ['class' => 'line'])
204
                                                    ->node($highlightedCode)
205
                                                ->close();
206
                                        }
207
                                    }
208
                                }
209
                                $html->close();
210
                            }
211
                        })
212
                        ->h2('Stack trace:')
213
                        ->execute(function (HTML $html) use ($trace, $traceString) {
214
                            if (count($trace)) {
215
                                $html->p('<i>Hover on fields with * to reveal more info.</i>');
216
                                $html->open('table', ['class' => 'trace'])
217
                                    ->open('thead')
218
                                        ->open('tr')
219
                                            ->th('No.')
220
                                            ->th('File *')
221
                                            ->th('Line')
222
                                            ->th('Class')
223
                                            ->th('Function')
224
                                            ->th('Arguments *')
225
                                        ->close()
226
                                    ->close()
227
                                    ->open('tbody')
228
                                    ->execute(function (HTML $html) use ($trace) {
229
                                        foreach ($trace as $i => $trace) {
230
                                            $count = (int)$i + 1;
231
                                            $html
232
                                            ->open('tr', ['class' => $count % 2 == 0 ? 'even' : 'odd'])
233
                                                ->td(strval($count), ['class' => 'number'])
234
                                                ->td(isset($trace['file']) ? basename($trace['file']) : '', ['class' => 'file', 'title' => $trace['file'] ?? false])
235
                                                ->td(strval($trace['line'] ?? ''), ['class' => 'line'])
236
                                                ->td(strval($trace['class'] ?? ''), ['class' => 'class'])
237
                                                ->td(strval($trace['function'] ?? ''), ['class' => 'function'])
238
                                                ->open('td', ['class' => 'arguments'])
239
                                                ->execute(function (HTML $html) use ($trace) {
240
                                                    if (isset($trace['args'])) {
241
                                                        foreach ($trace['args'] as $i => $arg) {
242
                                                            $html->span(gettype($arg), ['title' => print_r($arg, true)]);
243
                                                        }
244
                                                    } else {
245
                                                        $html->node('NULL');
246
                                                    }
247
                                                })
248
                                                ->close()
249
                                            ->close();
250
                                        }
251
                                    })
252
                                    ->close()
253
                                ->close();
254
                            } else {
255
                                $html->pre($traceString);
256
                            }
257
                        })
258
                    ->close()
259
                ->close()
260
            ->close()
261
        ->echo();
262
263
        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...
264
    }
265
266
    /**
267
     * Dumps an expression using `var_export()` or `print_r()`.
268
     *
269
     * @param mixed $expression
270
     * @return string
271
     */
272
    private static function exportExpression($expression): string
273
    {
274
        $export = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $export is dead and can be removed.
Loading history...
275
276
        try {
277
            $export = var_export($expression, true);
278
        } catch (\Throwable $e) {
279
            $class = self::class;
280
            $line1 = "// {$class} failed to dump the variable. Reason: {$e->getMessage()}. " . PHP_EOL;
281
            $line2 = "// here is a dump of the variable using print_r()" . PHP_EOL . PHP_EOL . PHP_EOL;
282
283
            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

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