Skip to content

Commit 89578f1

Browse files
[VarDumper] Dump PHP+Twig code excerpts in backtraces
1 parent 74c24a5 commit 89578f1

File tree

7 files changed

+315
-26
lines changed

7 files changed

+315
-26
lines changed

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

+138-14
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ class ExceptionCaster
4242

4343
public static function castError(\Error $e, array $a, Stub $stub, $isNested, $filter = 0)
4444
{
45-
return self::filterExceptionArray($a, "\0Error\0", $filter);
45+
return self::filterExceptionArray($stub->class, $a, "\0Error\0", $filter);
4646
}
4747

4848
public static function castException(\Exception $e, array $a, Stub $stub, $isNested, $filter = 0)
4949
{
50-
return self::filterExceptionArray($a, "\0Exception\0", $filter);
50+
return self::filterExceptionArray($stub->class, $a, "\0Exception\0", $filter);
5151
}
5252

5353
public static function castErrorException(\ErrorException $e, array $a, Stub $stub, $isNested)
@@ -64,24 +64,133 @@ public static function castThrowingCasterException(ThrowingCasterException $e, a
6464
$prefix = Caster::PREFIX_PROTECTED;
6565
$xPrefix = "\0Exception\0";
6666

67-
if (isset($a[$xPrefix.'previous'], $a[$xPrefix.'trace'][0])) {
67+
if (isset($a[$xPrefix.'previous'], $a[$xPrefix.'trace'])) {
6868
$b = (array) $a[$xPrefix.'previous'];
69-
$b[$xPrefix.'trace'][0] += array(
69+
array_unshift($b[$xPrefix.'trace'], array(
70+
'function' => 'new '.get_class($a[$xPrefix.'previous']),
7071
'file' => $b[$prefix.'file'],
7172
'line' => $b[$prefix.'line'],
73+
));
74+
$a[$xPrefix.'trace'] = new TraceStub($b[$xPrefix.'trace'], 1, false, 0, -1 - count($a[$xPrefix.'trace']->value));
75+
}
76+
77+
unset($a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']);
78+
79+
return $a;
80+
}
81+
82+
public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $isNested)
83+
{
84+
if (!$isNested) {
85+
return $a;
86+
}
87+
$stub->class = '';
88+
$stub->handle = 0;
89+
$frames = $trace->value;
90+
91+
$a = array();
92+
$j = count($frames);
93+
if (0 > $i = $trace->offset) {
94+
$i = max(0, $j + $i);
95+
}
96+
if (!isset($trace->value[$i])) {
97+
return array();
98+
}
99+
$lastCall = isset($frames[$i]['function']) ? ' ==> '.(isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '';
100+
101+
for ($j -= $i++; isset($frames[$i]); ++$i, --$j) {
102+
$call = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[$i]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '???';
103+
104+
$a[Caster::PREFIX_VIRTUAL.$j.'. '.$call.$lastCall] = new FrameStub(
105+
array(
106+
'object' => isset($frames[$i]['object']) ? $frames[$i]['object'] : null,
107+
'class' => isset($frames[$i]['class']) ? $frames[$i]['class'] : null,
108+
'type' => isset($frames[$i]['type']) ? $frames[$i]['type'] : null,
109+
'function' => isset($frames[$i]['function']) ? $frames[$i]['function'] : null,
110+
) + $frames[$i - 1],
111+
$trace->srcContext,
112+
$trace->keepArgs,
113+
true
72114
);
73-
array_splice($b[$xPrefix.'trace'], -1 - count($a[$xPrefix.'trace']));
74-
static::filterTrace($b[$xPrefix.'trace'], false);
75-
$a[Caster::PREFIX_VIRTUAL.'trace'] = $b[$xPrefix.'trace'];
115+
116+
$lastCall = ' ==> '.$call;
76117
}
118+
$a[Caster::PREFIX_VIRTUAL.$j.'. {main}'.$lastCall] = new FrameStub(
119+
array(
120+
'object' => null,
121+
'class' => null,
122+
'type' => null,
123+
'function' => '{main}',
124+
) + $frames[$i - 1],
125+
$trace->srcContext,
126+
$trace->keepArgs,
127+
true
128+
);
129+
if (null !== $trace->length) {
130+
$a = array_slice($a, 0, $trace->length, true);
131+
}
132+
133+
return $a;
134+
}
77135

78-
unset($a[$xPrefix.'trace'], $a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']);
136+
public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $isNested)
137+
{
138+
if (!$isNested) {
139+
return $a;
140+
}
141+
$f = $frame->value;
142+
$prefix = Caster::PREFIX_VIRTUAL;
143+
144+
if (isset($f['file'], $f['line'])) {
145+
if (preg_match('/\((\d+)\)(?:\([\da-f]{32}\))? : (?:eval\(\)\'d code|runtime-created function)$/', $f['file'], $match)) {
146+
$f['file'] = substr($f['file'], 0, -strlen($match[0]));
147+
$f['line'] = (int) $match[1];
148+
}
149+
if (file_exists($f['file']) && 0 <= $frame->srcContext) {
150+
$src[$f['file'].':'.$f['line']] = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], $frame->srcContext);
151+
152+
if (!empty($f['class']) && is_subclass_of($f['class'], 'Twig_Template') && method_exists($f['class'], 'getDebugInfo')) {
153+
$template = isset($f['object']) ? $f['object'] : new $f['class'](new \Twig_Environment(new \Twig_Loader_Filesystem()));
154+
155+
try {
156+
$templateName = $template->getTemplateName();
157+
$templateSrc = explode("\n", method_exists($template, 'getSource') ? $template->getSource() : $template->getEnvironment()->getLoader()->getSource($templateName));
158+
$templateInfo = $template->getDebugInfo();
159+
if (isset($templateInfo[$f['line']])) {
160+
$src[$templateName.':'.$templateInfo[$f['line']]] = self::extractSource($templateSrc, $templateInfo[$f['line']], $frame->srcContext);
161+
}
162+
} catch (\Twig_Error_Loader $e) {
163+
}
164+
}
165+
} else {
166+
$src[$f['file']] = $f['line'];
167+
}
168+
$a[$prefix.'src'] = new EnumStub($src);
169+
}
170+
171+
unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']);
172+
if ($frame->inTraceStub) {
173+
unset($a[$prefix.'class'], $a[$prefix.'type'], $a[$prefix.'function']);
174+
}
175+
foreach ($a as $k => $v) {
176+
if (!$v) {
177+
unset($a[$k]);
178+
}
179+
}
180+
if ($frame->keepArgs && isset($f['args'])) {
181+
$a[$prefix.'args'] = $f['args'];
182+
}
79183

80184
return $a;
81185
}
82186

187+
/**
188+
* @deprecated since 2.8, to be removed in 3.0. Use the castTraceStub method instead.
189+
*/
83190
public static function filterTrace(&$trace, $dumpArgs, $offset = 0)
84191
{
192+
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the castTraceStub method instead.', E_USER_DEPRECATED);
193+
85194
if (0 > $offset || empty($trace[$offset])) {
86195
return $trace = null;
87196
}
@@ -111,7 +220,7 @@ public static function filterTrace(&$trace, $dumpArgs, $offset = 0)
111220
}
112221
}
113222

114-
private static function filterExceptionArray(array $a, $xPrefix, $filter)
223+
private static function filterExceptionArray($xClass, array $a, $xPrefix, $filter)
115224
{
116225
if (isset($a[$xPrefix.'trace'])) {
117226
$trace = $a[$xPrefix.'trace'];
@@ -121,11 +230,12 @@ private static function filterExceptionArray(array $a, $xPrefix, $filter)
121230
}
122231

123232
if (!($filter & Caster::EXCLUDE_VERBOSE)) {
124-
static::filterTrace($trace, static::$traceArgs);
125-
126-
if (null !== $trace) {
127-
$a[$xPrefix.'trace'] = $trace;
128-
}
233+
array_unshift($trace, array(
234+
'function' => $xClass ? 'new '.$xClass : null,
235+
'file' => $a[Caster::PREFIX_PROTECTED.'file'],
236+
'line' => $a[Caster::PREFIX_PROTECTED.'line'],
237+
));
238+
$a[$xPrefix.'trace'] = new TraceStub($trace);
129239
}
130240
if (empty($a[$xPrefix.'previous'])) {
131241
unset($a[$xPrefix.'previous']);
@@ -134,4 +244,18 @@ private static function filterExceptionArray(array $a, $xPrefix, $filter)
134244

135245
return $a;
136246
}
247+
248+
private static function extractSource(array $srcArray, $line, $srcContext)
249+
{
250+
$src = '';
251+
252+
for ($i = $line - 1 - $srcContext; $i <= $line - 1 + $srcContext; ++$i) {
253+
$src .= (isset($srcArray[$i]) ? $srcArray[$i] : '')."\n";
254+
}
255+
if (!$srcContext) {
256+
$src = trim($src);
257+
}
258+
259+
return $src;
260+
}
137261
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\VarDumper\Caster;
13+
14+
/**
15+
* Represents a single backtrace frame as returned by debug_backtrace() or Exception->getTrace().
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class FrameStub extends EnumStub
20+
{
21+
public $srcContext;
22+
public $keepArgs;
23+
public $inTraceStub;
24+
25+
public function __construct(array $trace, $srcContext = 1, $keepArgs = true, $inTraceStub = false)
26+
{
27+
$this->value = $trace;
28+
$this->srcContext = $srcContext;
29+
$this->keepArgs = $keepArgs;
30+
$this->inTraceStub = $inTraceStub;
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\VarDumper\Caster;
13+
14+
use Symfony\Component\VarDumper\Cloner\Stub;
15+
16+
/**
17+
* Represents a backtrace as returned by debug_backtrace() or Exception->getTrace().
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class TraceStub extends Stub
22+
{
23+
public $srcContext;
24+
public $keepArgs;
25+
public $offset;
26+
public $length;
27+
28+
public function __construct(array $trace, $srcContext = 1, $keepArgs = true, $offset = 0, $length = null)
29+
{
30+
$this->value = $trace;
31+
$this->srcContext = $srcContext;
32+
$this->keepArgs = $keepArgs;
33+
$this->offset = $offset;
34+
$this->length = $length;
35+
}
36+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ abstract class AbstractCloner implements ClonerInterface
6969
'Error' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castError',
7070
'Symfony\Component\DependencyInjection\ContainerInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals',
7171
'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castThrowingCasterException',
72+
'Symfony\Component\VarDumper\Caster\TraceStub' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castTraceStub',
73+
'Symfony\Component\VarDumper\Caster\FrameStub' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castFrameStub',
7274

7375
'PHPUnit_Framework_MockObject_MockObject' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals',
7476
'Prophecy\Prophecy\ProphecySubjectInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals',

src/Symfony/Component/VarDumper/Tests/CliDumperTest.php

+71-11
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ public function testThrowingCaster()
170170
{
171171
$out = fopen('php://memory', 'r+b');
172172

173+
require_once __DIR__.'/Fixtures/Twig.php';
174+
$twig = new \__TwigTemplate_VarDumperFixture_u75a09(new \Twig_Environment(new \Twig_Loader_Filesystem()));
175+
173176
$dumper = new CliDumper();
174177
$dumper->setColors(false);
175178
$cloner = new VarCloner();
@@ -181,19 +184,35 @@ public function testThrowingCaster()
181184
},
182185
));
183186
$cloner->addCasters(array(
184-
':stream' => function () {
185-
throw new \Exception('Foobar');
186-
},
187+
':stream' => eval('return function () use ($twig) {
188+
try {
189+
$twig->render(array());
190+
} catch (\Twig_Error_Runtime $e) {
191+
throw $e->getPrevious();
192+
}
193+
};'),
187194
));
188-
$line = __LINE__ - 3;
189-
$file = __FILE__;
195+
$line = __LINE__ - 2;
190196
$ref = (int) $out;
191197

192198
$data = $cloner->cloneVar($out);
193199
$dumper->dump($data, $out);
194200
rewind($out);
195201
$out = stream_get_contents($out);
196202

203+
if (method_exists($twig, 'getSource')) {
204+
$twig = <<<EOTXT
205+
foo.twig:2: """
206+
foo bar\\n
207+
twig source\\n
208+
\\n
209+
"""
210+
211+
EOTXT;
212+
} else {
213+
$twig = '';
214+
}
215+
197216
$r = defined('HHVM_VERSION') ? '' : '#%d';
198217
$this->assertStringMatchesFormat(
199218
<<<EOTXT
@@ -210,12 +229,53 @@ public function testThrowingCaster()
210229
options: []
211230
⚠: Symfony\Component\VarDumper\Exception\ThrowingCasterException {{$r}
212231
#message: "Unexpected Exception thrown from a caster: Foobar"
213-
trace: array:1 [
214-
0 => array:2 [
215-
"call" => "%slosure%s()"
216-
"file" => "{$file}:{$line}"
217-
]
218-
]
232+
-trace: {
233+
%d. __TwigTemplate_VarDumperFixture_u75a09->doDisplay() ==> new Exception(): {
234+
src: {
235+
%sTwig.php:19: """
236+
// line 2\\n
237+
throw new \Exception('Foobar');\\n
238+
}\\n
239+
"""
240+
{$twig} }
241+
}
242+
%d. Twig_Template->displayWithErrorHandling() ==> __TwigTemplate_VarDumperFixture_u75a09->doDisplay(): {
243+
src: {
244+
%sTemplate.php:%d: """
245+
try {\\n
246+
\$this->doDisplay(\$context, \$blocks);\\n
247+
} catch (Twig_Error \$e) {\\n
248+
"""
249+
}
250+
}
251+
%d. Twig_Template->display() ==> Twig_Template->displayWithErrorHandling(): {
252+
src: {
253+
%sTemplate.php:%d: """
254+
{\\n
255+
\$this->displayWithErrorHandling(\$this->env->mergeGlobals(\$context), array_merge(\$this->blocks, \$blocks));\\n
256+
}\\n
257+
"""
258+
}
259+
}
260+
%d. Twig_Template->render() ==> Twig_Template->display(): {
261+
src: {
262+
%sTemplate.php:%d: """
263+
try {\\n
264+
\$this->display(\$context);\\n
265+
} catch (Exception \$e) {\\n
266+
"""
267+
}
268+
}
269+
%d. %slosure%s() ==> Twig_Template->render(): {
270+
src: {
271+
%sCliDumperTest.php:{$line}: """
272+
}\\n
273+
};'),\\n
274+
));\\n
275+
"""
276+
}
277+
}
278+
}
219279
}
220280
}
221281

0 commit comments

Comments
 (0)