Issues (30)

classes/Helper/Dumper.php (4 issues)

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\App;
15
use MAKS\Velox\Frontend\HTML;
16
use MAKS\Velox\Helper\Misc;
17
18
/**
19
 * A class that dumps variables and exception in a nice formatting.
20
 *
21
 * @package Velox\Helper
22
 * @since 1.0.0
23
 */
24
class Dumper
25
{
26
    /**
27
     * Regular expressions to transform `var_export()` result
28
     * from array construct (`array()`) to valid square brackets array (`[]`).
29
     *
30
     * @var array
31
     *
32
     * @since 1.5.6
33
     */
34
    protected const VAR_EXPORT_CONVERSIONS = [
35
        // replace array construct opening alone
36
        '/array \(/' => '[',
37
        // replace array construct opening inside a function call
38
        '/(\()array\(/' => '$1[',
39
        // replace array construct opening for stdClass
40
        '/\(object\) array\(/' => '(object)[',
41
        // replace array construct closing not part of a string
42
        '/\)(\))(?=([^\']*\'[^\']*\')*[^\']*$)/' => ']$1',
43
        // replace array construct closing alone
44
        '/^([ ]*)\)(,?)$/m' => '$1]$2',
45
        // replace array construct closing inside a function call
46
        '/(\n)([ ]*)\]\)/' => '$1$2])',
47
        // replace array key with nested array
48
        '/([ ]*)(\'[^\']+\') => ([\[\'])/' => '$1$2 => $3',
49
        // replace array construct/bracket opening after arrow with newline and spaces
50
        '/=>[ ]?\n[ ]+(\[|\()/' => '=> $1',
51
        // replace any valid php after arrow with a newline and spaces
52
        '/=>[ ]?\n[ ]+([a-zA-Z0-9_\x7f-\xff])/' => '=> $1',
53
        // replace empty array brackets array with a newline and spaces
54
        '/\[[ ]?\n[ ]*\]/' => '[]',
55
        // replace NULL with null
56
        '/NULL/' => 'null',
57
    ];
58
59
    /**
60
     * Regular expressions to transform `var_dump()` result
61
     * from var dump syntax to a valid square brackets array (`[]`).
62
     *
63
     * @var array
64
     *
65
     * @since 1.5.6
66
     */
67
    protected const VAR_DUMP_CONVERSIONS = [
68
        // replace unnecessary line breaks after arrow with spaces only
69
        '/(=>)\s*(.+)/' => ' $1 $2',
70
        // replace opening curly brace with opening square bracket
71
        '/{\n/' => "[\n",
72
        // replace closing curly brace with closing square bracket
73
        '/}\n/' => "]\n",
74
        // replace multiline empty square brackets with single line square brackets
75
        '/\[\n\s*\]/' => "[]",
76
        // add comma to all line endings except the ones wrapped in double quotes and the ones preceded by opening brackets
77
        '/(?<!\[)\n(?=([^"]*["][^"]*["])*[^"]*$)/' => ",\n",
78
        // add object type info as comment after array opening bracket
79
        '/&?(object\(.+\))(#\d+) \(\d+\) (\[)/' => '/* $1 [SPL-ID: $2] */ $3',
80
        // add resource type info as comment in a single line
81
        '/&?(resource\(\d+\) ([\w ]+) \((\w+)\))(,)*/' => '/* $1 */ "$3"$4',
82
        // remove the type hint and variable length for strings, and arrays at the beginning of line
83
        '/^&?(?:string|array|\w+)(\(.+\)) /m' => '',
84
        // remove the type hint and variable length for strings, and arrays after arrow
85
        '/(=>) &?(?:string|array|\w+)(\(.+\)) ([\["])/' => '$1 $3',
86
        // replace bool($var), int($var), float($var), enum($var) with $var
87
        '/&?(?:bool|int|float|enum)\((.+?)\)/' => '$1',
88
        // replace uninitialized($var) with empty __NONE__ and add type info as comment
89
        '/(uninitialized\(.+\))/' => '/* $1 */ __NONE__',
90
        // replace NULL with null
91
        '/NULL/' => 'null',
92
        // replace all backslashes with escaped backslashes
93
        '/(\\\\)/' => '\\\\$1',
94
        // replace all single quotes with an escaped single quotes
95
        '/(\')/' => '\\\\$1',
96
        // replace private visibility with a better formatted one
97
        '/\["(.+?)":"(.+)":(private)\]/' => '["$1":$3($2)]',
98
        // replace key with visibility in double quotes in square brackets with key in single quotes and add visibility as comment
99
        '/\["(.+?)":(.+?)\] (=>) (.+)/' => "'$1' $3 /* $2 */ $4",
100
        // replace key in double quotes in square brackets with key in single quotes
101
        '/\["(.*)"\] (=>)/' => "'$1' $2",
102
        // replace numeric key in square brackets with key
103
        '/\[(-?\d+)\] (=>)/' => '$1 $2',
104
        // replace string opening double quotes with single quotes
105
        '/(=>)([ ]\/\*.*\*\/)? "/' => "$1$2 '",
106
        // replace string closing double quotes with single quotes
107
        '/(.+)"(,)( \/\/.*)?\n/' => "$1'$2$3\n",
108
        // replace double quotes at the beginning of line with single quotes
109
        '/^"/m' => "'",
110
        // combine consequent comments with semicolon
111
        '/[ ]\*\/ \/\*[ ]/' => '; ',
112
        // replace *RECURSION* with __RECURSION__
113
        '/\*(RECURSION)\*/' => '__$1__',
114
    ];
115
116
    /**
117
     * Whether or not to use `var_dump()` instead of `var_export()` to dump the variables.
118
     *
119
     * NOTE: The dumper will always fall back to `var_dump()` if `var_export()` fails.
120
     *
121
     * @var bool
122
     */
123
    public static bool $useVarDump = false;
124
125
    /**
126
     * Accent color of exceptions page and dump block.
127
     *
128
     * @var string
129
     */
130
    public static string $accentColor = '#ff3a60';
131
132
    /**
133
     * Contrast color of exceptions page and dump block.
134
     *
135
     * @var string
136
     */
137
    public static string $contrastColor = '#030035';
138
139
    /**
140
     * Dumper CSS styles.
141
     * The array contains styles for:
142
     * - `exceptionPage`
143
     * - `traceBlock`
144
     * - `dumpBlock`
145
     * - `timeBlock`
146
     * - `detailsBlock`
147
     *
148
     * Currently set dumper colors can be inject in CSS using the `%accentColor%` and `%contrastColor%` placeholders.
149
     *
150
     * @var array
151
     *
152
     * @since 1.5.2
153
     */
154
    public static array $styles = [
155
        'exceptionPage' => ":root{--light:#fff;--dark:#000;--accent-color:%accentColor%;--contrast-color:%contrastColor%;--font-normal:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;--font-mono:'Fira Code','Ubuntu Mono',Courier,monospace;--font-base-size:16px;--container-width:85vw;--container-max-width:1364px}@media (max-width:992px){:root{--font-base-size:14px;--container-width:100%;--container-max-width:100vw}}*,::after,::before{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--accent-color) rgba(0,0,0,.15)}::-webkit-scrollbar{width:8px;height:8px;opacity:1;-webkit-appearance:none}::-webkit-scrollbar-thumb{background:var(--accent-color);border-radius:4px}::-webkit-scrollbar-track,::selection{background:rgba(0,0,0,.15)}body{background:var(--light);color:var(--dark);font-family:var(--font-normal);font-size:var(--font-base-size);line-height:1.5;margin:0}h1,h2,h3,h4,h5,h6{margin:0}h1{color:var(--accent-color);font-size:2rem}h2{color:var(--accent-color);font-size:1.75rem}h3{color:var(--light)}p{font-size:1rem;margin:1rem 0}a{color:var(--accent-color)}a:hover{text-decoration:underline}ul{padding:1.5rem 1rem;margin:1rem 0}li{white-space:pre;list-style-type:none}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.monospace,code{font-family:var(--font-mono);word-wrap:break-word;word-break:break-all}.container{width:var(--container-width);max-width:var(--container-max-width);min-height:100vh;background:var(--light);padding:7vh calc((var(--container-max-width) * .03)) 10vh;margin:0 auto;overflow:hidden}.capture-section,.info-section,.trace-section{margin-bottom:3rem}.message{background:var(--accent-color);color:var(--light);padding:2rem 1rem 1rem 1rem}.scrollable{overflow-x:scroll}.code{display:block;width:max-content;min-width:100%;background:var(--contrast-color);font-family:var(--font-mono);font-size:.875rem;margin:0;overflow-y:scroll;-ms-overflow-style:none;scrollbar-width:none;cursor:initial}.code::-webkit-scrollbar{display:none}.code *{background:0 0}.code-line{display:inline-block;width:calc(3ch + (2 * .75ch));background:rgba(255,255,255,.25);color:var(--light);text-align:right;padding:.25rem .75ch;margin:0 1.5ch 0 0;user-select:none}.code-line.exception-line{color:var(--accent-color);font-weight:700}.code-line.exception-line+code>span>span:not(:first-child){padding-bottom:3px;border-bottom:2px solid var(--accent-color)}.button{display:inline-block;vertical-align:baseline;background:var(--accent-color);color:var(--light);font-size:1rem;text-decoration:none;padding:.5rem 1rem;margin:0 0 1rem 0;border:none;border-radius:2.5rem;cursor:pointer}.button:hover{background:var(--contrast-color);text-decoration:inherit}.button:last-child{margin-bottom:0}.table{width:100%;border-collapse:collapse;border-spacing:0}.table .table-cell{padding:.75rem}.table .table-head .table-cell{background:var(--contrast-color);color:var(--light);text-align:left;padding-top:.75rem;padding-bottom:.75rem}.table-cell.compact{width:1%}.table-row{background:var(--light);border-top:1px solid rgba(0,0,0,.15)}.table .table-row:hover{background:rgba(0,0,0,.065)!important}.table .table-row.additional .table-cell{padding:0}.table .table-row.odd,.table .table-row.odd+.additional{background:var(--light)}.table .table-row.even,.table .table-row.even+.additional{background:rgba(0,0,0,.035)}.table .table-row.even+.additional,.table .table-row.odd+.additional{border-top:none}.pop-up{cursor:help}.line,.number{text-align:center}.class,.function{font-size:.875rem;font-weight:700}.arguments{white-space:nowrap}.argument{display:inline-block;background:rgba(0,0,0,.125);color:var(--accent-color);font-size:.875rem;font-style:italic;padding:.125rem .5rem;margin:0 .25rem 0 0;border-radius:2.5rem}.argument:hover{background:var(--accent-color);color:var(--contrast-color)}.accordion{cursor:pointer;position:relative}.accordion-summary{width:1.5rem;height:1.5rem;background:var(--accent-color);color:var(--light);line-height:1.5rem;text-align:center;list-style:none;border-radius:50%;position:absolute;top:-2.2925rem;left:1.425rem;user-select:none;cursor:pointer}.accordion-summary:hover{background:var(--contrast-color)}.accordion-details{padding:0}",
156
        'traceBlock'    => "background:#fff;color:%accentColor%;font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:12px;padding:4px 8px;margin-bottom:18px;",
157
        'dumpBlock'     => "display:table;background:%contrastColor%;color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:18px;padding:18px;margin-bottom:8px;",
158
        'timeBlock'     => "display:table;background:%accentColor%;color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:12px;font-weight:bold;padding:12px;margin-bottom:8px;",
159
        'detailsBlock'  => "background:%accentColor%;color:#fff;font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:12px;font-weight:bold;padding:12px;margin-bottom:8px;cursor:pointer;user-select:none;",
160
    ];
161
162
    /**
163
     * Colors of syntax tokens.
164
     *
165
     * @var array
166
     */
167
    public static array $syntaxHighlightColors = [
168
        'comment' => '#aeaeae',
169
        'keyword' => '#00bfff',
170
        'string'  => '#e4ba80',
171
        'default' => '#e8703a',
172
        'html'    => '#ab8703',
173
    ];
174
175
    /**
176
     * Additional CSS styling of syntax tokens.
177
     *
178
     * @var array
179
     */
180
    public static array $syntaxHighlightStyles = [
181
        'comment' => 'font-weight: lighter;',
182
        'keyword' => 'font-weight: bold;',
183
        'string'  => '',
184
        'default' => '',
185
        'html'    => '',
186
    ];
187
188
    /**
189
     * PHP highlighting syntax tokens.
190
     *
191
     * @var string[]
192
     */
193
    private static array $syntaxHighlightTokens = ['comment', 'keyword', 'string', 'default', 'html'];
194
195
196
    /**
197
     * Dumps a variable and dies.
198
     *
199
     * @param mixed ...$variable
200
     *
201
     * @return void The result will simply get echoed.
202
     *
203
     * @codeCoverageIgnore
204
     */
205
    public static function dd(...$variable): void
206
    {
207
        self::dump(...$variable);
208
209
        App::terminate();
210
    }
211
212
    /**
213
     * Dumps a variable in a nice HTML block with syntax highlighting.
214
     *
215
     * @param mixed ...$variable
216
     *
217 3
     * @return void The result will simply get echoed.
218
     */
219 3
    public static function dump(...$variable): void
220 3
    {
221
        $caller = self::getValidCallerTrace();
222 3
        $blocks = self::getDumpingBlocks();
223
224 3
        $dump = '';
225 3
226 3
        foreach ($variable as $var) {
227 3
            $trace = sprintf($blocks['traceBlock'], $caller);
228
            $highlightedDump = self::exportExpressionWithSyntaxHighlighting($var, $trace);
229 3
            $block = sprintf($blocks['dumpBlock'], $highlightedDump);
230
231
            $dump .= sprintf($blocks['detailsBlock'], $block);
232 3
        }
233 3
234
        $time = (microtime(true) - START_TIME) * 1000;
235 3
        $dump .= sprintf($blocks['timeBlock'], $time);
236 3
237
        if (self::isCli()) {
238 3
            echo $dump;
239
240
            return;
241
        }
242
243
        // @codeCoverageIgnoreStart
244
        (new HTML(false))
245
            ->open('div', ['id' => $id = 'dump-' . uniqid()])
246
                ->style("#{$id} * { background: transparent; padding: 0; }")
247
                ->div($dump)
248
            ->close()
249
        ->echo();
250
        // @codeCoverageIgnoreEnd
251
    }
252
253
    /**
254
     * Dumps an exception in a nice HTML page or as string and exits the script.
255
     *
256
     * @param \Throwable $exception
257
     *
258
     * @return void The result will be echoed as HTML page or a string representation of the exception if the interface is CLI.
259
     *
260
     * @codeCoverageIgnore
261
     */
262
    public static function dumpException(\Throwable $exception): void
263
    {
264
        if (self::isCli()) {
265
            echo $exception;
266
267
            App::terminate();
268
        }
269
270
        self::setSyntaxHighlighting();
271
272
        $reflection  = new \ReflectionClass($exception);
273
        $file        = $exception->getFile();
274
        $line        = $exception->getLine();
275
        $message     = $exception->getMessage();
276
        $trace       = $exception->getTrace();
277
        $traceString = $exception->getTraceAsString();
278
        $name        = $reflection->getName();
279
        $shortName   = $reflection->getShortName();
280
        $fileName    = basename($file);
281
282
        $style = Misc::interpolate(
283
            static::$styles['exceptionPage'],
284
            [
285
                'accentColor'   => static::$accentColor,
286
                'contrastColor' => static::$contrastColor
287
            ],
288
            '%%'
289
        );
290
        $favicon = '<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="512" height="512"><circle cx="256" cy="256" r="256" fill="#F00" /></svg>';
291
292
        (new HTML(false))
293
            ->node('<!DOCTYPE html>')
294
            ->open('html', ['lang' => 'en'])
295
                ->open('head')
296
                    ->title('Oops! Something went wrong')
297
                    ->link(null, ['rel' => 'icon', 'href' => 'data:image/svg+xml;base64,' . base64_encode($favicon)])
298
                    ->style($style, ['type' => 'text/css'])
299
                ->close()
300
301
                ->open('body')
302
                    ->open('div', ['class' => 'container'])
303
                        ->open('section', ['class' => 'info-section'])
304
                            ->h1('Uncaught "' . Misc::transform($shortName, 'title') . '"')
305
                            ->p(
306
                                "<code><b>{$shortName}</b></code> was thrown on line <code><b>{$line}</b></code> of file " .
307
                                "<code><b>{$fileName}</b></code> which prevented further execution of the code."
308
                            )
309
                            ->open('div', ['class' => 'message'])
310
                                ->h3($name)
311
                                // we need to decode and encode because some messages come escaped
312
                                ->p(nl2br(htmlspecialchars(htmlspecialchars_decode((string)$message), ENT_QUOTES, 'UTF-8')))
313
                            ->close()
314
                        ->close()
315
316
                        ->open('section', ['class' => 'capture-section'])
317
                            ->h2('Thrown in:')
318
                            ->execute(function (HTML $html) use ($file, $line) {
319
                                if (!file_exists($file)) {
320
                                    return;
321
                                }
322
323
                                $html
324
                                    ->open('p')
325
                                        ->node("File: <code><b>{$file}</b></code>")
326
                                        ->entity('nbsp')
327
                                        ->entity('nbsp')
328
                                        ->a('Open in <b>VS Code</b>', [
329
                                            'href'  => sprintf('vscode://file/%s:%d', $file, $line),
330
                                            'class' => 'button',
331
                                        ])
332
                                    ->close();
333
334
                                $html->div(Dumper::highlightFile($file, $line), ['class' => 'scrollable']);
335
                            })
336
                        ->close()
337
338
                        ->open('section', ['class' => 'trace-section'])
339
                            ->h2('Stack trace:')
340
                            ->execute(function (HTML $html) use ($trace, $traceString) {
341
                                if (!count($trace)) {
342
                                    $html->pre($traceString);
343
344
                                    return;
345
                                }
346
347
                                $html->node(Dumper::tabulateStacktrace($trace));
348
                            })
349
                        ->close()
350
                    ->close()
351
                ->close()
352
            ->close()
353
        ->echo();
354
355
        App::terminate();
356
    }
357
358
    /**
359
     * Highlights the passed file with the possibility to focus a specific line.
360
     *
361
     * @param string $file The file to highlight.
362
     * @param int $line The line to focus.
363
     *
364
     * @return string The hightailed file as HTML.
365
     *
366
     * @since 1.5.5
367
     *
368
     * @codeCoverageIgnore
369
     */
370
    private static function highlightFile(string $file, ?int $line = null): string
371
    {
372
        return (new HTML(false))
373
            ->open('div', ['class' => 'code-highlight'])
374
                ->open('ul', ['class' => 'code'])
375
                    ->execute(function (HTML $html) use ($file, $line) {
376
                        $file   = (string)$file;
377
                        $line   = (int)$line;
378
                        $lines  = file_exists($file) ? file($file) : [];
379
                        $count  = count($lines);
380
                        $offset = !$line ? $count : 5;
381
382
                        for ($i = $line - $offset; $i < $line + $offset; $i++) {
383
                            if (!($i > 0 && $i < $count)) {
384
                                continue;
385
                            }
386
387
                            $highlightedCode = highlight_string('<?php ' . $lines[$i], true);
388
                            $highlightedCode = preg_replace(
389
                                ['/\n/', '/<br ?\/?>/', '/&lt;\?php&nbsp;/'],
390
                                ['', '', ''],
391
                                $highlightedCode
392
                            );
393
394
                            $causer = $i === $line - 1;
395
                            $number = strval($i + 1);
396
397
                            if ($causer) {
398
                                $number = str_pad('>', strlen($number), '=', STR_PAD_LEFT);
399
                            }
400
401
                            $html
402
                                ->open('li')
403
                                    ->condition($causer === true)
404
                                    ->span($number, ['class' => 'code-line exception-line'])
405
                                    ->condition($causer === false)
406
                                    ->span($number, ['class' => 'code-line'])
407
                                    ->node($highlightedCode)
408
                                ->close();
409
                        }
410
                    })
411
                ->close()
412
            ->close()
413
        ->return();
414
    }
415
416
    /**
417
     * Tabulates the passed stacktrace in an HTML table.
418
     *
419
     * @param array $trace Exception stacktrace array.
420
     *
421
     * @return string The tabulated trace as HTML.
422
     *
423
     * @since 1.5.5
424
     *
425
     * @codeCoverageIgnore
426
     */
427
    private static function tabulateStacktrace(array $trace): string
428
    {
429
        return (new HTML(false))
430
            ->p('<i>Fields with * can reveal more info. * Hoverable. ** Clickable.</i>')
431
            ->open('div', ['class' => 'scrollable'])
432
                ->open('table', ['class' => 'table'])
433
                    ->open('thead', ['class' => 'table-head'])
434
                        ->open('tr', ['class' => 'table-row'])
435
                            ->th('No.&nbsp;**', ['class' => 'table-cell compact'])
436
                            ->th('File&nbsp;*', ['class' => 'table-cell'])
437
                            ->th('Line', ['class' => 'table-cell compact'])
438
                            ->th('Class', ['class' => 'table-cell'])
439
                            ->th('Function', ['class' => 'table-cell'])
440
                            ->th('Arguments&nbsp;*', ['class' => 'table-cell'])
441
                        ->close()
442
                    ->close()
443
                    ->open('tbody', ['class' => 'table-body'])
444
                        ->execute(function (HTML $html) use ($trace) {
445
                            foreach ($trace as $i => $trace) {
446
                                $count = (int)$i + 1;
447
448
                                $html
449
                                    ->open('tr', ['class' => 'table-row ' . ($count % 2 == 0 ? 'even' : 'odd')])
450
                                        ->td(isset($trace['file']) ? '' : strval($count), ['class' => 'table-cell number'])
451
                                        ->td(
452
                                            isset($trace['file'])
453
                                                ? sprintf('<a href="vscode://file/%s:%d" title="Open in VS Code">%s</a>', $trace['file'], $trace['line'], basename($trace['file']))
454
                                                : 'N/A',
455
                                            ['class' => 'table-cell file pop-up', 'title' => $trace['file'] ?? 'N/A']
456
                                        )
457
                                        ->td(strval($trace['line'] ?? 'N/A'), ['class' => 'table-cell line'])
458
                                        ->td(strval($trace['class'] ?? 'N/A'), ['class' => 'table-cell class monospace'])
459
                                        ->td(strval($trace['function'] ?? 'N/A'), ['class' => 'table-cell function monospace'])
460
                                        ->open('td', ['class' => 'table-cell arguments monospace'])
461
                                            ->execute(function (HTML $html) use ($trace) {
462
                                                if (!isset($trace['args'])) {
463
                                                    $html->node('NULL');
464
465
                                                    return;
466
                                                }
467
468
                                                foreach ($trace['args'] as $argument) {
469
                                                    $html->span(gettype($argument), [
470
                                                        'class' => 'argument pop-up',
471
                                                        'title' => htmlspecialchars(
472
                                                            Misc::callObjectMethod(Dumper::class, 'exportExpression', $argument),
473
                                                            ENT_QUOTES,
474
                                                            'UTF-8'
475
                                                        ),
476
                                                    ]);
477
                                                }
478
                                            })
479
                                        ->close()
480
                                    ->close()
481
                                    ->execute(function (HTML $html) use ($trace, $count) {
482
                                        isset($trace['file']) && $html
483
                                            ->open('tr', ['class' => 'table-row additional', 'id' => 'trace-' . $count])
484
                                                ->open('td', ['class' => 'table-cell', 'colspan' => 6])
485
                                                    ->open('details', ['class' => 'accordion'])
486
                                                        ->summary(strval($count), ['class' => 'accordion-summary'])
487
                                                        ->div(
488
                                                            Dumper::highlightFile($trace['file'] ?? '', $trace['line'] ?? null),
489
                                                            ['class' => 'accordion-details']
490
                                                        )
491
                                                    ->close()
492
                                                ->close()
493
                                            ->close();
494
                                    });
495
                            }
496
                        })
497
                    ->close()
498
                ->close()
499
            ->close()
500
        ->return();
501
    }
502
503
    /**
504
     * Returns dump of the passed variable using `var_export()`.
505
     *
506
     * @param mixed $variable
507
     *
508
     * @return string
509
     *
510 1
     * @since 1.5.6
511
     */
512 1
    protected static function varExport($variable): string
513 1
    {
514 1
        $dump = var_export($variable, true);
515 1
        $dump = preg_replace(
516
            array_keys(static::VAR_EXPORT_CONVERSIONS),
517
            array_values(static::VAR_EXPORT_CONVERSIONS),
518 1
            $dump
519
        );
520
        $dump = rtrim(trim(strval($dump)), ',');
521
522
        // var_export() indents using 3 spaces and messes the indentation up
523 1
        // with odd numbers starting from number 3, this omits spaces
524 1
        // for odd numbers making it indents using 2 spaces instead of 3
525 1
        $dump = preg_replace_callback('/([ ]{3,})/', function ($matches) {
526
            $indentation = strlen(strlen($matches[1]) % 2 === 0 ? $matches[1] : substr($matches[1], 0, -1));
527
            return str_repeat(' ', $indentation);
528 1
        }, $dump);
529
530
        return $dump;
531
    }
532
533
    /**
534
     * Returns dump of the passed variable using `var_dump()`.
535
     *
536
     * @param mixed $variable
537
     *
538
     * @return string
539
     *
540 2
     * @since 1.5.6
541
     */
542 2
    protected static function varDump($variable): string
543 2
    {
544 2
        ob_start();
545 2
        var_dump($variable);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($variable) looks like debug code. Are you sure you do not want to remove it?
Loading history...
546 2
        $dump = ob_get_clean();
547 2
        $dump = preg_replace(
548
            array_keys(static::VAR_DUMP_CONVERSIONS),
549
            array_values(static::VAR_DUMP_CONVERSIONS),
550 2
            $dump
551
        );
552 2
        $dump = rtrim(trim(strval($dump)), ',');
553
554
        return $dump;
555
    }
556
557
    /**
558
     * Dumps an expression using `var_export()` or `var_dump()`.
559
     *
560
     * @param mixed $expression
561
     *
562 3
     * @return string
563
     */
564 3
    public static function exportExpression($expression): string
565
    {
566 3
        $recursive = strpos(print_r($expression, true), '*RECURSION*') !== false;
0 ignored issues
show
It seems like print_r($expression, true) can also be of type true; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

566
        $recursive = strpos(/** @scrutinizer ignore-type */ print_r($expression, true), '*RECURSION*') !== false;
Loading history...
567 2
568 3
        $dump = static::$useVarDump == true || $recursive == true
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
569
            ? self::varDump($expression)
570 3
            : self::varExport($expression);
571
572
        $info = static::$useVarDump == false && $recursive == true ? Misc::interpolate(
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
573
            '// {class} failed to dump the variable.{eol}' .
574 1
            '// Reason: var_export() does not handle circular references.{eol}' .
575 3
            '// Here is a dump of the variable using var_dump() formatted in a valid PHP array.{eol}{eol}',
576
            ['class' => static::class, 'eol' => PHP_EOL]
577 3
        ) : '';
578
579 3
        $dump = $info . $dump;
580
581
        return $dump;
582
    }
583
584
    /**
585
     * Dumps an expression using `var_export()` or `var_dump()` with syntax highlighting.
586
     *
587
     * @param mixed $expression
588
     * @param string|null $phpReplacement `<?php` replacement.
589
     *
590 3
     * @return string
591
     */
592 3
    private static function exportExpressionWithSyntaxHighlighting($expression, ?string $phpReplacement = ''): string
593
    {
594 3
        self::setSyntaxHighlighting();
595
596 3
        $export = self::exportExpression($expression);
597 3
598
        $code = highlight_string('<?php ' . $export, true);
599 3
        $html = preg_replace(
600
            '/&lt;\?php&nbsp;/',
601
            $phpReplacement ?? '',
602
            $code,
603
            1
604 3
        );
605
606
        if (!self::isCli()) {
607
            // @codeCoverageIgnoreStart
608
            return $html;
609
            // @codeCoverageIgnoreEnd
610 3
        }
611
612 3
        $mixed = preg_replace_callback(
613 3
            '/@CLR\((#\w+)\)/',
614 3
            fn ($matches) => self::getAnsiCodeFromHexColor($matches[1]),
615 3
            preg_replace(
616
                ['/<\w+\s+style="color:\s*(#[a-z0-9]+)">(.*?)<\/\w+>/im', '/<br ?\/?>/', '/&nbsp;/'],
617
                ["\e[@CLR($1)m$2\e[0m", "\n", " "],
618
                $html
619
            )
620 3
        );
621
622 3
        $ansi = trim(html_entity_decode(strip_tags($mixed)));
623
624
        return $ansi;
625
    }
626
627
    /**
628
     * Returns an array containing HTML/ANSI wrapping blocks.
629
     * Available blocks are: `traceBlock`, `dumpBlock`, `timeBlock`, and `detailsBlock`.
630
     * All this blocks will contain a placeholder for a `*printf()` function to inject content.
631
     *
632 3
     * @return void
633
     */
634 3
    private static function getDumpingBlocks(): array
635
    {
636
        $isCli = self::isCli();
637 3
638 3
        $colors = [
639
            'accentColor'   => static::$accentColor,
640
            'contrastColor' => static::$contrastColor,
641 3
        ];
642 3
643
        $traceBlock = HTML::div('%s', [
644
            'style' => Misc::interpolate(static::$styles['traceBlock'], $colors, '%%')
645 3
        ]);
646 3
647
        $dumpBlock = HTML::div('%s', [
648
            'style' => Misc::interpolate(static::$styles['dumpBlock'], $colors, '%%')
649 3
        ]);
650 3
651
        $timeBlock = HTML::div('START_TIME + %.2fms', [
652
            'style' => Misc::interpolate(static::$styles['timeBlock'], $colors, '%%')
653 3
        ]);
654 3
655 3
        $detailsBlock = (new HTML(false))
656 3
            ->open('details', ['open' => null])
657
                ->summary('Expand/Collapse', [
658 3
                    'style' => Misc::interpolate(static::$styles['detailsBlock'], $colors, '%%')
659 3
                ])
660 3
                ->main('%s')
661
            ->close()
662 3
        ->return();
663 3
664 3
        if ($isCli) {
665 3
            $traceBlock   = "\n// \e[33;1mTRACE:\e[0m \e[34;46m[%s]\e[0m \n\n";
666 3
            $dumpBlock    = "%s";
667
            $timeBlock    = "\n\n// \e[36mSTART_TIME\e[0m + \e[35m%.2f\e[0mms \n\n\n";
668
            $detailsBlock = "%s";
669 3
        }
670
671
        return compact('traceBlock', 'dumpBlock', 'timeBlock', 'detailsBlock');
672
    }
673
674
    /**
675
     * Returns the last caller trace before `dd()` or `dump()` if the format of `file:line`.
676
     *
677 3
     * @return string
678
     */
679 3
    private static function getValidCallerTrace(): string
680
    {
681 3
        $trace = 'Trace: N/A';
682
683 3
        array_filter(array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), function ($backtrace) use (&$trace) {
684 3
            static $hasFound = false;
685 3
            if (!$hasFound && in_array($backtrace['function'], ['dump', 'dd'])) {
686
                $trace = $backtrace['file'] . ':' . $backtrace['line'];
687 3
                $hasFound = true;
688
689
                return true;
690 3
            }
691
692
            return false;
693 3
        });
694
695
        return $trace;
696
    }
697
698
    /**
699
     * Converts a hex color to the closest standard ANSI color code.
700
     * Standard ANSI colors include: black, red, green, yellow, blue, magenta, cyan and white.
701
     *
702 3
     * @return int
703
     */
704
    private static function getAnsiCodeFromHexColor(string $color): int
705 3
    {
706
        $colors = [
707
            'black'   => ['ansi' => 30, 'rgb' => [0, 0, 0]],
708
            'red'     => ['ansi' => 31, 'rgb' => [255, 0, 0]],
709
            'green'   => ['ansi' => 32, 'rgb' => [0, 128, 0]],
710
            'yellow'  => ['ansi' => 33, 'rgb' => [255, 255, 0]],
711
            'blue'    => ['ansi' => 34, 'rgb' => [0, 0, 255]],
712
            'magenta' => ['ansi' => 35, 'rgb' => [255, 0, 255]],
713
            'cyan'    => ['ansi' => 36, 'rgb' => [0, 255, 255]],
714
            'white'   => ['ansi' => 37, 'rgb' => [255, 255, 255]],
715
            'default' => ['ansi' => 39, 'rgb' => [128, 128, 128]],
716 3
        ];
717 3
718
        $hexClr = ltrim($color, '#');
719 3
        $hexNum = strval(strlen($hexClr));
720
        $hexPos = [
721
            '3' => [0, 0, 1, 1, 2, 2],
722
            '6' => [0, 1, 2, 3, 4, 5],
723
        ];
724 3
725 3
        [$r, $g, $b] = [
726 3
            $hexClr[$hexPos[$hexNum][0]] . $hexClr[$hexPos[$hexNum][1]],
727
            $hexClr[$hexPos[$hexNum][2]] . $hexClr[$hexPos[$hexNum][3]],
728
            $hexClr[$hexPos[$hexNum][4]] . $hexClr[$hexPos[$hexNum][5]],
729 3
        ];
730
731 3
        $color = [hexdec($r), hexdec($g), hexdec($b)];
732 3
733 3
        $distances = [];
734 3
        foreach ($colors as $name => $values) {
735 3
            $distances[$name] = sqrt(
736 3
                pow($values['rgb'][0] - $color[0], 2) +
737
                pow($values['rgb'][1] - $color[1], 2) +
738
                pow($values['rgb'][2] - $color[2], 2)
739
            );
740 3
        }
741 3
742 3
        $colorName = '';
743 3
        $minDistance = pow(2, 30);
744 3
        foreach ($distances as $key => $value) {
745 3
            if ($value < $minDistance) {
746
                $minDistance = $value;
747
                $colorName   = $key;
748
            }
749 3
        }
750
751
        return $colors[$colorName]['ansi'];
752
    }
753
754
    /**
755
     * Sets PHP syntax highlighting colors according to current class state.
756
     *
757
     * @return void
758
     *
759
     * @codeCoverageIgnore
760
     */
761
    private static function setSyntaxHighlighting(): void
762
    {
763
        if (self::isCli()) {
764
            // use default entries for better contrast.
765
            return;
766
        }
767
768
        $tokens = self::$syntaxHighlightTokens;
769
770
        foreach ($tokens as $token) {
771
            $color = self::$syntaxHighlightColors[$token] ?? ini_get("highlight.{$token}");
772
            $style = self::$syntaxHighlightStyles[$token] ?? chr(8);
773
774
            $highlighting = sprintf('%s;%s', $color, $style);
775
776
            ini_set("highlight.{$token}", $highlighting);
777
        }
778
    }
779
780
    /**
781
     * Checks whether the script is currently running in CLI mode or not.
782
     *
783 3
     * @return bool
784
     */
785 3
    private static function isCli(): bool
786
    {
787
        return PHP_SAPI === 'cli';
788
    }
789
}
790