Skip to content

Commit 2cd6c63

Browse files
committed
feature #19797 [VarDumper] Handle attributes in Data clones for more semantic dumps (nicolas-grekas)
This PR was merged into the 3.2-dev branch. Discussion ---------- [VarDumper] Handle attributes in Data clones for more semantic dumps | Q | A | ------------- | --- | Branch? | master | New feature? | yes | Tests pass? | yes | License | MIT Casters can now add attributes to the stub they create and to virtual properties so that e.g. the HtmlDumper knows more about the structure it is dumping. This allow for fine tuned HTML representations. The ExceptionCaster uses this feature to make traces more useful, by telling the HtmlDumper that a which keys/values are files, lines or code excerpt (and which language). Thus, code excerpts can now be opened directly in the IDE. Commits ------- 2937ffa [VarDumper] Handle attributes in Data clones for more semantic dumps
2 parents ec9cbab + 2937ffa commit 2cd6c63

File tree

12 files changed

+177
-86
lines changed

12 files changed

+177
-86
lines changed

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,11 @@ table.logs .sf-call-stack abbr {
912912
#collector-content .sf-dump .trace li.selected {
913913
background: rgba(255, 255, 153, 0.5);
914914
}
915+
#collector-content .sf-dump-expanded code { color: #222; }
916+
#collector-content .sf-dump-expanded code .sf-dump-const {
917+
background: rgba(255, 255, 153, 0.5);
918+
font-weight: normal;
919+
}
915920

916921
{# Search Results page
917922
========================================================================= #}

src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is
8989
$stub->handle = 0;
9090
$frames = $trace->value;
9191
$prefix = Caster::PREFIX_VIRTUAL;
92-
$format = "\0~Stack level %s.\0%s";
9392

9493
$a = array();
9594
$j = count($frames);
@@ -99,14 +98,14 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is
9998
if (!isset($trace->value[$i])) {
10099
return array();
101100
}
102-
$lastCall = isset($frames[$i]['function']) ? ' ==> '.(isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '';
101+
$lastCall = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '';
103102
$frames[] = array('function' => '');
104103

105104
for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) {
106105
$f = $frames[$i];
107106
$call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'].'()' : '???';
108107

109-
$label = $call.$lastCall;
108+
$label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall;
110109
$frame = new FrameStub(
111110
array(
112111
'object' => isset($f['object']) ? $f['object'] : null,
@@ -120,14 +119,15 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is
120119
$f = self::castFrameStub($frame, array(), $frame, true);
121120
if (isset($f[$prefix.'src'])) {
122121
foreach ($f[$prefix.'src']->value as $label => $frame) {
122+
$label = substr_replace($label, "title=Stack level $j.&", 2, 0);
123123
}
124124
if (isset($f[$prefix.'args']) && $frame instanceof EnumStub) {
125125
$frame->value['args'] = $f[$prefix.'args'];
126126
}
127127
}
128-
$a[sprintf($format, $j, $label)] = $frame;
128+
$a[$label] = $frame;
129129

130-
$lastCall = ' ==> '.$call;
130+
$lastCall = $call;
131131
}
132132
if (null !== $trace->sliceLength) {
133133
$a = array_slice($a, 0, $trace->sliceLength, true);
@@ -149,28 +149,38 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $is
149149
$f['file'] = substr($f['file'], 0, -strlen($match[0]));
150150
$f['line'] = (int) $match[1];
151151
}
152-
$src = array();
152+
$caller = isset($f['function']) ? sprintf('in %s() on line %d', (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'], $f['line']) : null;
153+
$src = $f['line'];
154+
$srcKey = $f['file'];
155+
$ellipsis = explode(DIRECTORY_SEPARATOR, $srcKey);
156+
$ellipsis = 3 < count($ellipsis) ? 2 + strlen(implode(array_slice($ellipsis, -2))) : 0;
157+
153158
if (file_exists($f['file']) && 0 <= self::$srcContext) {
154159
if (!empty($f['class']) && is_subclass_of($f['class'], 'Twig_Template') && method_exists($f['class'], 'getDebugInfo')) {
155160
$template = isset($f['object']) ? $f['object'] : new $f['class'](new \Twig_Environment(new \Twig_Loader_Filesystem()));
156161

157162
try {
163+
$ellipsis = 0;
158164
$templateName = $template->getTemplateName();
159165
$templateSrc = explode("\n", method_exists($template, 'getSource') ? $template->getSource() : $template->getEnvironment()->getLoader()->getSource($templateName));
160166
$templateInfo = $template->getDebugInfo();
161167
if (isset($templateInfo[$f['line']])) {
162-
$src[$templateName.':'.$templateInfo[$f['line']]] = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext);
168+
$src = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext, $caller, 'twig');
169+
$srcKey = $templateName.':'.$templateInfo[$f['line']];
163170
}
164171
} catch (\Twig_Error_Loader $e) {
165172
}
166173
}
167-
if (!$src) {
168-
$src[$f['file'].':'.$f['line']] = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], self::$srcContext);
174+
if ($srcKey == $f['file']) {
175+
$src = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], self::$srcContext, $caller, 'php', $f['file']);
176+
$srcKey .= ':'.$f['line'];
177+
if ($ellipsis) {
178+
$ellipsis += 1 + strlen($f['line']);
179+
}
169180
}
170-
} else {
171-
$src[$f['file']] = $f['line'];
172181
}
173-
$a[$prefix.'src'] = new EnumStub($src);
182+
$srcAttr = $ellipsis ? 'ellipsis='.$ellipsis : '';
183+
$a[$prefix.'src'] = new EnumStub(array("\0~$srcAttr\0$srcKey" => $src));
174184
}
175185

176186
unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']);
@@ -214,7 +224,7 @@ private static function filterExceptionArray($xClass, array $a, $xPrefix, $filte
214224
return $a;
215225
}
216226

217-
private static function extractSource(array $srcArray, $line, $srcContext)
227+
private static function extractSource(array $srcArray, $line, $srcContext, $title, $lang, $file = null)
218228
{
219229
$src = array();
220230

@@ -239,20 +249,24 @@ private static function extractSource(array $srcArray, $line, $srcContext)
239249
} while (0 > $i && null !== $pad);
240250

241251
--$ltrim;
242-
243-
$pad = strlen($line + $srcContext);
244252
$srcArray = array();
245253

246254
foreach ($src as $i => $c) {
247255
if ($ltrim) {
248256
$c = isset($c[$ltrim]) && "\r" !== $c[$ltrim] ? substr($c, $ltrim) : ltrim($c, " \t");
249257
}
250258
$c = substr($c, 0, -1);
251-
$c = new ConstStub($c, $c);
252259
if ($i !== $srcContext) {
253-
$c->class = 'default';
260+
$c = new ConstStub('default', $c);
261+
} else {
262+
$c = new ConstStub($c, $title);
263+
if (null !== $file) {
264+
$c->attr['file'] = $file;
265+
$c->attr['line'] = $line;
266+
}
254267
}
255-
$srcArray[sprintf("% {$pad}d", $i + $line - $srcContext)] = $c;
268+
$c->attr['lang'] = $lang;
269+
$srcArray[sprintf("\0~%d\0", $i + $line - $srcContext)] = $c;
256270
}
257271

258272
return new EnumStub($srcArray);

src/Symfony/Component/VarDumper/Caster/StubCaster.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static function castStub(Stub $c, array $a, Stub $stub, $isNested)
2828
$stub->value = $c->value;
2929
$stub->handle = $c->handle;
3030
$stub->cut = $c->cut;
31+
$stub->attr = $c->attr;
3132

3233
return array();
3334
}
@@ -56,6 +57,7 @@ public static function castEnum(EnumStub $c, array $a, Stub $stub, $isNested)
5657
$stub->handle = 0;
5758
$stub->value = null;
5859
$stub->cut = $c->cut;
60+
$stub->attr = $c->attr;
5961

6062
$a = array();
6163

src/Symfony/Component/VarDumper/Cloner/Cursor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ class Cursor
3838
public $hashLength = 0;
3939
public $hashCut = 0;
4040
public $stop = false;
41+
public $attr = array();
4142
}

src/Symfony/Component/VarDumper/Cloner/Data.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item)
155155
$firstSeen = true;
156156

157157
if (!$item instanceof Stub) {
158+
$cursor->attr = array();
158159
$type = gettype($item);
159160
} elseif (Stub::TYPE_REF === $item->type) {
160161
if ($item->handle) {
@@ -167,6 +168,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item)
167168
$cursor->hardRefHandle = $this->useRefHandles & $item->handle;
168169
$cursor->hardRefCount = $item->refCount;
169170
}
171+
$cursor->attr = $item->attr;
170172
$type = $item->class ?: gettype($item->value);
171173
$item = $item->value;
172174
}
@@ -181,6 +183,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item)
181183
}
182184
$cursor->softRefHandle = $this->useRefHandles & $item->handle;
183185
$cursor->softRefCount = $item->refCount;
186+
$cursor->attr = $item->attr;
184187
$cut = $item->cut;
185188

186189
if ($item->position && $firstSeen) {

src/Symfony/Component/VarDumper/Cloner/Stub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ class Stub
3737
public $handle = 0;
3838
public $refCount = 0;
3939
public $position = 0;
40+
public $attr = array();
4041
}

src/Symfony/Component/VarDumper/Dumper/CliDumper.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public function dumpScalar(Cursor $cursor, $type, $value)
112112
$this->dumpKey($cursor);
113113

114114
$style = 'const';
115-
$attr = array();
115+
$attr = $cursor->attr;
116116

117117
switch ($type) {
118118
case 'default':
@@ -148,7 +148,7 @@ public function dumpScalar(Cursor $cursor, $type, $value)
148148
break;
149149

150150
default:
151-
$attr['value'] = isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value;
151+
$attr += array('value' => isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value);
152152
$value = isset($type[0]) && !preg_match('//u', $type) ? $this->utf8Encode($type) : $type;
153153
break;
154154
}
@@ -164,6 +164,7 @@ public function dumpScalar(Cursor $cursor, $type, $value)
164164
public function dumpString(Cursor $cursor, $str, $bin, $cut)
165165
{
166166
$this->dumpKey($cursor);
167+
$attr = $cursor->attr;
167168

168169
if ($bin) {
169170
$str = $this->utf8Encode($str);
@@ -172,7 +173,7 @@ public function dumpString(Cursor $cursor, $str, $bin, $cut)
172173
$this->line .= '""';
173174
$this->dumpLine($cursor->depth, true);
174175
} else {
175-
$attr = array(
176+
$attr += array(
176177
'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
177178
'binary' => $bin,
178179
);
@@ -350,7 +351,8 @@ protected function dumpKey(Cursor $cursor)
350351
case '~':
351352
$style = 'meta';
352353
if (isset($key[0][1])) {
353-
$attr['title'] = substr($key[0], 1);
354+
parse_str(substr($key[0], 1), $attr);
355+
$attr += array('binary' => $cursor->hashKeyIsBinary);
354356
}
355357
break;
356358
case '*':

src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ class HtmlDumper extends CliDumper
4343
'meta' => 'color:#B729D9',
4444
'key' => 'color:#56DB3A',
4545
'index' => 'color:#1299DA',
46+
'expanded code.hljs' => 'display:inline; padding:0; background:none',
4647
);
4748

4849
private $displayOptions = array(
4950
'maxDepth' => 1,
5051
'maxStringLength' => 160,
52+
'fileLinkFormat' => null,
5153
);
5254
private $extraDisplayOptions = array();
5355

@@ -184,6 +186,19 @@ function toggle(a, recursive) {
184186
return function (root, x) {
185187
root = doc.getElementById(root);
186188
189+
var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'),
190+
options = {$options},
191+
elt = root.getElementsByTagName('A'),
192+
len = elt.length,
193+
i = 0, s, h,
194+
t = [];
195+
196+
while (i < len) t.push(elt[i++]);
197+
198+
for (i in x) {
199+
options[i] = x[i];
200+
}
201+
187202
function a(e, f) {
188203
addEventListener(root, e, function (e) {
189204
if ('A' == e.target.tagName) {
@@ -201,6 +216,20 @@ function isCtrlKey(e) {
201216
refStyle.innerHTML = '';
202217
}
203218
});
219+
if (options.fileLinkFormat) {
220+
addEventListener(root, 'click', function (e) {
221+
e = e.target;
222+
while (root != e && 'CODE' != e.tagName) {
223+
e = e.parentNode;
224+
}
225+
if ('CODE' == e.tagName) {
226+
var f = e.getAttribute('data-file'), l = e.getAttribute('data-line');
227+
if (f && l) {
228+
location.href = options.fileLinkFormat.replace('%f', f).replace('%l', l);
229+
}
230+
}
231+
});
232+
}
204233
a('mouseover', function (a) {
205234
if (a = idRx.exec(a.className)) {
206235
try {
@@ -246,19 +275,6 @@ function isCtrlKey(e) {
246275
}
247276
});
248277
249-
var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'),
250-
options = {$options},
251-
elt = root.getElementsByTagName('A'),
252-
len = elt.length,
253-
i = 0, s, h,
254-
t = [];
255-
256-
while (i < len) t.push(elt[i++]);
257-
258-
for (i in x) {
259-
options[i] = x[i];
260-
}
261-
262278
elt = root.getElementsByTagName('SAMP');
263279
len = elt.length;
264280
i = 0;
@@ -369,6 +385,15 @@ function isCtrlKey(e) {
369385
border: 0;
370386
outline: none;
371387
}
388+
pre.sf-dump .sf-dump-ellipsis {
389+
display: inline-block;
390+
overflow: visible;
391+
text-overflow: ellipsis;
392+
width: 50px;
393+
white-space: nowrap;
394+
overflow: hidden;
395+
vertical-align: top;
396+
}
372397
.sf-dump-str-collapse .sf-dump-str-collapse {
373398
display: none;
374399
}
@@ -452,6 +477,11 @@ protected function style($style, $value, $attr = array())
452477
} elseif ('private' === $style) {
453478
$style .= sprintf(' title="Private property defined in class:&#10;`%s`"', esc($attr['class']));
454479
}
480+
if (isset($attr['ellipsis'])) {
481+
$label = esc(substr($value, -$attr['ellipsis']));
482+
483+
return sprintf('<span class=sf-dump-%s><abbr title="%s" class=sf-dump-ellipsis>%2$s</abbr>%s</span>', $style, substr($v, 0, -strlen($label)), $label);
484+
}
455485

456486
$map = static::$controlCharsMap;
457487
$style = "<span class=sf-dump-{$style}>";
@@ -475,6 +505,13 @@ protected function style($style, $value, $attr = array())
475505
} else {
476506
$v .= '</span>';
477507
}
508+
if (isset($attr['lang'])) {
509+
if (isset($attr['file'], $attr['line'])) {
510+
$v = sprintf('<code class="%s" data-file="%s" data-line="%d">%s</code>', esc($attr['lang']), esc($attr['file']), $attr['line'], $v);
511+
} else {
512+
$v = sprintf('<code class="%s">%s</code>', esc($attr['lang']), $v);
513+
}
514+
}
478515

479516
return $v;
480517
}

0 commit comments

Comments
 (0)