Skip to content

Commit 4b920b8

Browse files
authored
Simplify json array dump (#1040)
* Tweak preview length * Render plain js without vardumper structure
1 parent 9b25a64 commit 4b920b8

6 files changed

Lines changed: 262 additions & 119 deletions

File tree

resources/vardumper.css

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* file that was distributed with this source code.
88
*/
99

10-
.sf-js-enabled .phpdebugbar pre.sf-dump .sf-dump-compact, .sf-js-enabled .sf-dump-str-collapse .sf-dump-str-collapse, .sf-js-enabled .sf-dump-str-expand .sf-dump-str-expand {
10+
.sf-js-enabled .phpdebugbar pre.sf-dump .sf-dump-compact {
1111
display: none;
1212
}
1313

@@ -18,14 +18,6 @@
1818
overflow: initial !important;
1919
}
2020

21-
.phpdebugbar pre.sf-dump:after {
22-
content: "";
23-
visibility: hidden;
24-
display: block;
25-
height: 0;
26-
clear: both;
27-
}
28-
2921
.phpdebugbar pre.sf-dump a {
3022
text-decoration: none;
3123
cursor: pointer;
@@ -34,20 +26,6 @@
3426
color: inherit;
3527
}
3628

37-
.phpdebugbar pre.sf-dump img {
38-
max-width: 50em;
39-
max-height: 50em;
40-
margin: .5em 0 0 0;
41-
padding: 0;
42-
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAHUlEQVQY02O8zAABilCaiQEN0EeA8QuUcX9g3QEAAjcC5piyhyEAAAAASUVORK5CYII=) #D3D3D3;
43-
}
44-
45-
.phpdebugbar pre.sf-dump code {
46-
display: inline;
47-
padding: 0;
48-
background: none;
49-
}
50-
5129
.phpdebugbar pre.sf-dump, .phpdebugbar pre.sf-dump .sf-dump-default {
5230
word-wrap: break-word;
5331
white-space: pre-wrap;

resources/vardumper.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,86 @@
4646
return data;
4747
}
4848

49+
renderPlain(value) {
50+
const pre = document.createElement('pre');
51+
pre.className = 'sf-dump';
52+
const savedDepth = this.expandedDepth;
53+
this.expandedDepth = 0;
54+
pre.innerHTML = this.plainToHtml(value, 0) + '\n';
55+
this.expandedDepth = savedDepth;
56+
return pre;
57+
}
58+
59+
plainToHtml(value, depth) {
60+
if (value === null) return '<span class=sf-dump-const>null</span>';
61+
switch (typeof value) {
62+
case 'boolean':
63+
return '<span class=sf-dump-const>' + value + '</span>';
64+
case 'number':
65+
return '<span class=sf-dump-num>' + this.esc(String(value)) + '</span>';
66+
case 'string':
67+
return '"<span class=sf-dump-str>' + this.esc(value) + '</span>"';
68+
case 'object': {
69+
const isIndexed = Array.isArray(value);
70+
const keys = isIndexed ? null : Object.keys(value);
71+
const len = isIndexed ? value.length : keys.length;
72+
73+
if (len === 0) return '[]';
74+
75+
const expanded = depth < this.expandedDepth;
76+
let html = '<span class=sf-dump-note>array:' + len + '</span> [';
77+
html += '<a class=sf-dump-toggle><span>' + (expanded ? '▼' : '▶') + '</span></a>';
78+
79+
// Preview
80+
const previewParts = [];
81+
const maxPreview = Math.min(len, 8);
82+
for (let i = 0; i < maxPreview; i++) {
83+
const k = isIndexed ? i : keys[i];
84+
const v = isIndexed ? value[i] : value[keys[i]];
85+
const pv = v === null ? 'null'
86+
: typeof v === 'string' ? '"' + this.esc(v.length > 40 ? v.substring(0, 40) + '…' : v) + '"'
87+
: typeof v === 'boolean' ? String(v)
88+
: typeof v === 'number' ? String(v)
89+
: '[…]';
90+
previewParts.push(isIndexed ? pv : this.esc(String(k)) + ': ' + pv);
91+
}
92+
let preview = previewParts.join(', ');
93+
if (len > maxPreview) preview += ', …';
94+
html += '<span class="sf-dump-preview' + (expanded ? ' sf-dump-hidden' : '') + '"> ' + preview + ' ]</span>';
95+
96+
if (expanded) {
97+
html += '<samp class=sf-dump-expanded>';
98+
html += this.plainChildrenToHtml(value, isIndexed, keys, depth);
99+
html += '</samp>';
100+
} else {
101+
const id = ++lazySeq;
102+
lazyStore.set(id, { plain: value, isArr: isIndexed, keys, depth, renderer: this, expandedDepth: this.expandedDepth });
103+
html += '<samp class=sf-dump-compact data-lazy=' + id + '></samp>';
104+
}
105+
html += '<span class="sf-dump-close' + (expanded ? '' : ' sf-dump-hidden') + '">]</span>';
106+
return html;
107+
}
108+
default:
109+
return this.esc(String(value));
110+
}
111+
}
112+
113+
plainChildrenToHtml(value, isIndexed, keys, depth) {
114+
const len = isIndexed ? value.length : keys.length;
115+
let html = '';
116+
for (let i = 0; i < len; i++) {
117+
if (i > 0) html += '\n';
118+
if (isIndexed) {
119+
html += '<span class=sf-dump-index>' + i + '</span> => ';
120+
html += this.plainToHtml(value[i], depth + 1);
121+
} else {
122+
html += '"<span class=sf-dump-key>' + this.esc(keys[i]) + '</span>" => ';
123+
html += this.plainToHtml(value[keys[i]], depth + 1);
124+
}
125+
}
126+
return html;
127+
}
128+
49129
esc(s) {
50130
return String(s).replace(escRe, m => escMap[m]);
51131
}
@@ -301,7 +381,11 @@
301381
const savedDepth = renderer.expandedDepth;
302382
renderer.expandedDepth = data.expandedDepth;
303383

304-
samp.innerHTML = renderer.childrenToHtml(data.children, data.cut, data.depth, data.ht);
384+
if (data.plain !== undefined) {
385+
samp.innerHTML = renderer.plainChildrenToHtml(data.plain, data.isArr, data.keys, data.depth);
386+
} else {
387+
samp.innerHTML = renderer.childrenToHtml(data.children, data.cut, data.depth, data.ht);
388+
}
305389

306390
renderer.expandedDepth = savedDepth;
307391
}

resources/widgets.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,22 @@
2626
*/
2727
let dumpRenderer;
2828
const renderValue = PhpDebugBar.Widgets.renderValue = function (value, prettify) {
29-
// Dump object (from JsonDataFormatter)
29+
// Dump node (from JsonDataFormatter via VarDumper)
3030
if (value && typeof value === 'object' && '_sd' in value) {
3131
if (!dumpRenderer) {
3232
dumpRenderer = new PhpDebugBar.Widgets.VarDumpRenderer();
3333
}
3434
return dumpRenderer.render(value);
3535
}
3636

37+
// Plain array/object (from JsonDataFormatter fast path) → render as tree
38+
if (value && typeof value === 'object') {
39+
if (!dumpRenderer) {
40+
dumpRenderer = new PhpDebugBar.Widgets.VarDumpRenderer();
41+
}
42+
return dumpRenderer.renderPlain(value);
43+
}
44+
3745
if (typeof value !== 'string') {
3846
if (prettify) {
3947
return htmlize(JSON.stringify(value, undefined, 2));

src/DataFormatter/JsonDataFormatter.php

Lines changed: 25 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,27 @@ class JsonDataFormatter extends DataFormatter implements AssetProvider
3333
*/
3434
public function formatVar(mixed $data, bool $deep = true): mixed
3535
{
36+
if (is_string($data)) {
37+
$maxLength = $this->getClonerOptions()['max_string'] ?? 10000;
38+
if (strlen($data) <= $maxLength) {
39+
return $data;
40+
}
41+
return substr($data, 0, $maxLength) . '[truncated ' . (strlen($data) - $maxLength) . ' chars]';
42+
}
43+
3644
if ($this->isSimpleValue($data)) {
3745
return $data;
3846
}
3947

40-
if (is_array($data) && ($node = $this->buildSimpleArray($data)) !== null) {
41-
$node['_sd'] = $this->getDumperOptions()['expanded_depth'] ?? 1;
42-
return $node;
48+
$maxItems = $this->getClonerOptions()['max_items'] ?? 1000;
49+
if ($deep && is_array($data) && $this->isSimpleArray($data, $maxItems)) {
50+
return $data;
4351
}
4452

4553
$dumper = $this->getDumper();
4654
if ($dumper instanceof DebugBarJsonDumper) {
4755
$result = $dumper->dumpAsArray($this->cloneVar($data, $deep));
48-
$result['_sd'] = $this->getDumperOptions()['expanded_depth'] ?? 1;
56+
$result['_sd'] = $this->getDumperOptions()['expanded_depth'] ?? 0;
4957
return $result;
5058
}
5159

@@ -68,72 +76,24 @@ protected function cloneVar(mixed $data, bool $deep): Data
6876
}
6977

7078
/**
71-
* Build the dump node structure directly for flat arrays of scalars/strings,
72-
* bypassing VarCloner + Data + dump callback chain entirely.
73-
*
74-
* Returns null if the array contains non-simple values (objects, nested arrays, etc.).
79+
* Check if an array contains only simple values (scalars/strings)
80+
* and can be passed through as plain JSON without the dump node structure.
7581
*/
76-
private function buildSimpleArray(array $data): ?array
82+
private function isSimpleArray(array $data, int &$budget = 1000): bool
7783
{
78-
$maxString = $this->getClonerOptions()['max_string'] ?? 10000;
79-
$maxItems = $this->getClonerOptions()['max_items'] ?? 1000;
80-
$isIndexed = array_is_list($data);
81-
82-
$children = [];
83-
$count = 0;
84-
foreach ($data as $k => $v) {
85-
if ($count >= $maxItems) {
86-
break;
87-
}
88-
89-
$node = match (true) {
90-
$v === null => ['t' => 's', 's' => 'n', 'v' => null],
91-
is_bool($v) => ['t' => 's', 's' => 'b', 'v' => $v],
92-
is_int($v) => ['t' => 's', 's' => 'i', 'v' => $v],
93-
is_float($v) => ['t' => 's', 's' => 'd', 'v' => $v],
94-
is_string($v) && strlen($v) <= $maxString => ['t' => 'r', 'v' => $v],
95-
is_string($v) => self::truncateString($v, $maxString),
96-
default => null, // non-simple value → bail out
97-
};
98-
99-
if ($node === null) {
100-
return null;
84+
foreach ($data as $v) {
85+
if (--$budget < 0) {
86+
return false;
10187
}
102-
103-
$entry = ['n' => $node];
104-
if (!$isIndexed) {
105-
$entry['k'] = $k;
88+
if (is_array($v)) {
89+
if (!$this->isSimpleArray($v, $budget)) {
90+
return false;
91+
}
92+
} elseif (!$this->isSimpleValue($v)) {
93+
return false;
10694
}
107-
$children[] = $entry;
108-
$count++;
10995
}
110-
111-
$cut = max(0, count($data) - $count);
112-
113-
$result = [
114-
't' => 'h',
115-
'ht' => $isIndexed ? 2 : 1,
116-
];
117-
118-
if ($count > 0) {
119-
$result['cls'] = (string) ($count + $cut);
120-
}
121-
122-
if ($children !== []) {
123-
$result['c'] = $children;
124-
}
125-
126-
if ($cut > 0) {
127-
$result['cut'] = $cut;
128-
}
129-
130-
return $result;
131-
}
132-
133-
private static function truncateString(string $v, int $maxString): array
134-
{
135-
$totalLen = mb_strlen($v, 'UTF-8');
136-
return ['t' => 'r', 'v' => mb_substr($v, 0, $maxString, 'UTF-8'), 'cut' => $totalLen - $maxString, 'len' => $totalLen];
96+
return true;
13797
}
13898

13999
/**

src/DataFormatter/VarDumper/ReverseJsonDumper.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,39 @@
55
namespace DebugBar\DataFormatter\VarDumper;
66

77
use Symfony\Component\VarDumper\Cloner\Cursor;
8+
use Symfony\Component\VarDumper\Cloner\Data;
9+
use Symfony\Component\VarDumper\Cloner\VarCloner;
810

911
class ReverseJsonDumper
1012
{
13+
public function toCloneVarData(mixed $data): Data
14+
{
15+
$result = $this->wrapJsonDumps($data);
16+
17+
$cloner = new VarCloner();
18+
$cloner->addCasters(DebugBarJsonCaster::getCasters());
19+
20+
return $cloner->cloneVar($result);
21+
}
22+
23+
private function wrapJsonDumps(mixed $data): mixed
24+
{
25+
if (!is_array($data)) {
26+
return $data;
27+
}
28+
29+
// Wrap the data in a special format that the DebugBarJsonCaster can understand
30+
if (array_key_exists('_sd', $data)) {
31+
return new DebugBarJsonVar($data);
32+
}
33+
34+
foreach ($data as $key => $value) {
35+
$data[$key] = $this->wrapJsonDumps($value);
36+
}
37+
38+
return $data;
39+
}
40+
1141
public function reverseFormatVar(array $node): string
1242
{
1343
return $this->jsonToText($node, 0);

0 commit comments

Comments
 (0)