|
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; |
|
|
|
|
|
|
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'], |
|
|
|
|
|
|
112
|
|
|
preg_replace( |
|
113
|
|
|
'/<\?php /', |
|
114
|
|
|
$markup['traceBlock'], |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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 ?\/?>/', '/<\?php /'], |
|
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; |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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
|
|
|
|
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.