Skip to content

Commit f051fbc

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

File tree

7 files changed

+316
-26
lines changed

7 files changed

+316
-26
lines changed

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

Lines changed: 140 additions & 14 deletions
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;
117+
}
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+
}
135+
136+
public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $isNested)
137+
{
138+
if (!$isNested) {
139+
return $a;
76140
}
141+
extract($frame->value);
142+
$prefix = Caster::PREFIX_VIRTUAL;
77143

78-
unset($a[$xPrefix.'trace'], $a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']);
144+
if (isset($file, $line)) {
145+
if (preg_match('/\((\d+)\)(?:\([\da-f]{32}\))? : (?:eval\(\)\'d code|runtime-created function)$/', $file, $match)) {
146+
$file = substr($file, 0, -strlen($match[0]));
147+
$line = (int) $match[1];
148+
}
149+
if (file_exists($file) && 0 <= $frame->srcContext) {
150+
$src[$file.':'.$line] = self::extractSource(explode("\n", file_get_contents($file)), $line, $frame->srcContext);
151+
152+
if (!empty($class) && is_subclass_of($class, 'Twig_Template') && method_exists($class, 'getDebugInfo')) {
153+
$twig = isset($object) ? $object : new $class(new \Twig_Environment(new \Twig_Loader_Filesystem()));
154+
155+
try {
156+
$twigName = $twig->getTemplateName();
157+
$twigSrc = explode("\n", method_exists($twig, 'getSource') ? $twig->getSource() : $twig->getEnvironment()->getLoader()->getSource($twigName));
158+
$twigInfo = $twig->getDebugInfo();
159+
if (isset($twigInfo[$line])) {
160+
$src[$twigName.':'.$twigInfo[$line]] = self::extractSource($twigSrc, $twigInfo[$line], $frame->srcContext);
161+
}
162+
} catch (\Twig_Error_Loader $e) {
163+
}
164+
}
165+
} else {
166+
$src[$file] = $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($args)) {
181+
$a[$prefix.'args'] = $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,20 @@ 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 = substr($src, 0, -1);
257+
} else {
258+
$src = trim($src);
259+
}
260+
261+
return $src;
262+
}
137263
}
Lines changed: 32 additions & 0 deletions
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+
}
Lines changed: 36 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 70 additions & 11 deletions
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,34 @@ 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+
"""
209+
210+
EOTXT;
211+
} else {
212+
$twig = '';
213+
}
214+
197215
$r = defined('HHVM_VERSION') ? '' : '#%d';
198216
$this->assertStringMatchesFormat(
199217
<<<EOTXT
@@ -210,12 +228,53 @@ public function testThrowingCaster()
210228
options: []
211229
⚠: Symfony\Component\VarDumper\Exception\ThrowingCasterException {{$r}
212230
#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-
]
231+
-trace: {
232+
%d. __TwigTemplate_VarDumperFixture_u75a09->doDisplay() ==> new Exception(): {
233+
src: {
234+
%sTwig.php:19: """
235+
// line 2\\n
236+
throw new \Exception('Foobar');\\n
237+
}
238+
"""
239+
{$twig} }
240+
}
241+
%d. Twig_Template->displayWithErrorHandling() ==> __TwigTemplate_VarDumperFixture_u75a09->doDisplay(): {
242+
src: {
243+
%sTemplate.php:%d: """
244+
try {\\n
245+
\$this->doDisplay(\$context, \$blocks);\\n
246+
} catch (Twig_Error \$e) {
247+
"""
248+
}
249+
}
250+
%d. Twig_Template->display() ==> Twig_Template->displayWithErrorHandling(): {
251+
src: {
252+
%sTemplate.php:%d: """
253+
{\\n
254+
\$this->displayWithErrorHandling(\$this->env->mergeGlobals(\$context), array_merge(\$this->blocks, \$blocks));\\n
255+
}
256+
"""
257+
}
258+
}
259+
%d. Twig_Template->render() ==> Twig_Template->display(): {
260+
src: {
261+
%sTemplate.php:%d: """
262+
try {\\n
263+
\$this->display(\$context);\\n
264+
} catch (Exception \$e) {
265+
"""
266+
}
267+
}
268+
%d. %slosure%s() ==> Twig_Template->render(): {
269+
src: {
270+
%sCliDumperTest.php:{$line}: """
271+
}\\n
272+
};'),\\n
273+
));
274+
"""
275+
}
276+
}
277+
}
219278
}
220279
}
221280

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy