Skip to content

Commit b3bd92a

Browse files
[VarDumper] Add an options builder to configure VarDumper's behaviour at runtime
1 parent 99b25a1 commit b3bd92a

File tree

9 files changed

+299
-27
lines changed

9 files changed

+299
-27
lines changed

src/Symfony/Bundle/DebugBundle/Resources/config/services.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor;
2323
use Symfony\Component\VarDumper\Command\ServerDumpCommand;
2424
use Symfony\Component\VarDumper\Dumper\CliDumper;
25+
use Symfony\Component\VarDumper\Dumper\ContextProvider\BacktraceContextProvider;
2526
use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider;
2627
use Symfony\Component\VarDumper\Dumper\ContextProvider\RequestContextProvider;
2728
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
@@ -87,6 +88,10 @@
8788
param('kernel.project_dir'),
8889
service('debug.file_link_formatter')->nullOnInvalid(),
8990
]),
91+
'backtrace' => inline_service(BacktraceContextProvider::class)->args([
92+
0,
93+
service('var_dump.cloner')->nullOnInvalid(),
94+
]),
9095
],
9196
])
9297

@@ -111,6 +116,10 @@
111116
]),
112117
'request' => inline_service(RequestContextProvider::class)->args([service('request_stack')]),
113118
'cli' => inline_service(CliContextProvider::class),
119+
'backtrace' => inline_service(BacktraceContextProvider::class)->args([
120+
0,
121+
service('var_dump.cloner')->nullOnInvalid(),
122+
]),
114123
],
115124
])
116125

src/Symfony/Component/VarDumper/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for `FORCE_COLOR` environment variable
8+
* Add support of options when using `dd()` and `dump()`
89

910
7.1
1011
---

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\VarDumper\Cloner;
1313

1414
use Symfony\Component\VarDumper\Caster\Caster;
15+
use Symfony\Component\VarDumper\Dumper\ContextProvider\BacktraceContextProvider;
1516
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
1617

1718
/**
@@ -270,12 +271,17 @@ public function dump(DumperInterface $dumper): void
270271
$cursor->hashType = -1;
271272
$cursor->attr = $this->context[SourceContextProvider::class] ?? [];
272273
$label = $this->context['label'] ?? '';
274+
$options = $this->context['options'] ?? [];
273275

274276
if ($cursor->attr || '' !== $label) {
275277
$dumper->dumpScalar($cursor, 'label', $label);
276278
}
277279
$cursor->hashType = 0;
278280
$this->dumpItem($dumper, $cursor, $refs, $this->data[$this->position][$this->key]);
281+
282+
if (false !== ($options['_trace'] ?? false) && $cursor->attr = $this->context[BacktraceContextProvider::class] ?? []) {
283+
$this->dumpDebugBacktrace($dumper, $cursor);
284+
}
279285
}
280286

281287
/**
@@ -426,4 +432,14 @@ private function getStub(mixed $item): mixed
426432

427433
return $stub;
428434
}
435+
436+
private function dumpDebugBacktrace(DumperInterface $dumper, Cursor $cursor): void
437+
{
438+
$backtrace = $cursor->attr['backtrace'];
439+
440+
$dumper->dumpScalar($cursor, 'default', '');
441+
$dumper->dumpScalar($cursor, 'default', '**DEBUG BACKTRACE**');
442+
443+
$backtrace->dump($dumper);
444+
}
429445
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\Dumper\ContextProvider;
13+
14+
use Symfony\Component\VarDumper\Caster\TraceStub;
15+
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
16+
use Symfony\Component\VarDumper\Cloner\VarCloner;
17+
18+
/**
19+
* Provides the debug stacktrace of the VarDumper call.
20+
*
21+
* @author Alexandre Daubois <alex.daubois@gmail.com>
22+
*/
23+
final class BacktraceContextProvider implements ContextProviderInterface
24+
{
25+
private const BACKTRACE_CONTEXT_PROVIDER_DEPTH = 4;
26+
27+
public function __construct(
28+
private readonly bool|int $limit,
29+
private ?ClonerInterface $cloner,
30+
) {
31+
$this->cloner ??= new VarCloner();
32+
}
33+
34+
public function getContext(): ?array
35+
{
36+
if (false === $this->limit) {
37+
return [];
38+
}
39+
40+
$context = [];
41+
$traces = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
42+
43+
for ($i = self::BACKTRACE_CONTEXT_PROVIDER_DEPTH; $i < \count($traces); ++$i) {
44+
$context[] = $traces[$i];
45+
46+
if ($this->limit === \count($context)) {
47+
break;
48+
}
49+
}
50+
51+
$stub = new TraceStub($context);
52+
53+
return ['backtrace' => $this->cloner->cloneVar($stub->value)];
54+
}
55+
}

src/Symfony/Component/VarDumper/Resources/functions/dump.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,33 @@
1919
*/
2020
function dump(mixed ...$vars): mixed
2121
{
22-
if (!$vars) {
23-
VarDumper::dump(new ScalarStub('🐛'));
22+
$options = array_filter(
23+
$vars,
24+
static fn (mixed $key): bool => in_array($key, VarDumper::AVAILABLE_OPTIONS, true),
25+
\ARRAY_FILTER_USE_KEY
26+
);
2427

25-
return null;
26-
}
28+
$trace = $options['_trace'] ?? null;
29+
unset($options['_trace']);
2730

2831
if (array_key_exists(0, $vars) && 1 === count($vars)) {
29-
VarDumper::dump($vars[0]);
32+
VarDumper::dump($vars[0], null, $options);
3033
$k = 0;
3134
} else {
35+
$vars = array_filter($vars, static fn (int|string $key) => !str_starts_with($key, '_'), \ARRAY_FILTER_USE_KEY);
36+
37+
if (!$vars) {
38+
VarDumper::dump(new ScalarStub('🐛'), null, $options);
39+
40+
return null;
41+
}
42+
3243
foreach ($vars as $k => $v) {
33-
VarDumper::dump($v, is_int($k) ? 1 + $k : $k);
44+
if (array_key_last($vars) === $k) {
45+
$options['_trace'] = $trace;
46+
}
47+
48+
VarDumper::dump($v, is_int($k) ? 1 + $k : $k, $options);
3449
}
3550
}
3651

@@ -45,7 +60,7 @@ function dump(mixed ...$vars): mixed
4560
if (!function_exists('dd')) {
4661
function dd(mixed ...$vars): never
4762
{
48-
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && !headers_sent()) {
63+
if (!in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && !headers_sent()) {
4964
header('HTTP/1.1 500 Internal Server Error');
5065
}
5166

@@ -55,11 +70,21 @@ function dd(mixed ...$vars): never
5570
exit(1);
5671
}
5772

73+
$options = array_filter(
74+
$vars,
75+
static fn (mixed $key): bool => in_array($key, VarDumper::AVAILABLE_OPTIONS, true),
76+
\ARRAY_FILTER_USE_KEY
77+
);
78+
5879
if (array_key_exists(0, $vars) && 1 === count($vars)) {
59-
VarDumper::dump($vars[0]);
80+
VarDumper::dump($vars[0], null, $options);
6081
} else {
6182
foreach ($vars as $k => $v) {
62-
VarDumper::dump($v, is_int($k) ? 1 + $k : $k);
83+
if (str_starts_with($k, '_')) {
84+
continue;
85+
}
86+
87+
VarDumper::dump($v, is_int($k) ? 1 + $k : $k, $options);
6388
}
6489
}
6590

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Tests\Dumper\ContextProvider;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\VarDumper\Cloner\VarCloner;
16+
use Symfony\Component\VarDumper\Dumper\ContextProvider\BacktraceContextProvider;
17+
18+
class BacktraceContextProviderTest extends TestCase
19+
{
20+
public function testFalseBacktraceLimit()
21+
{
22+
$provider = new BacktraceContextProvider(false, new VarCloner());
23+
$this->assertArrayNotHasKey('backtrace', $provider->getContext());
24+
}
25+
26+
public function testPositiveBacktraceLimit()
27+
{
28+
$provider = new BacktraceContextProvider(2, new VarCloner());
29+
$this->assertCount(2, $provider->getContext()['backtrace']);
30+
}
31+
}

src/Symfony/Component/VarDumper/Tests/Dumper/ContextualizedDumperTest.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
namespace Symfony\Component\VarDumper\Tests\Dumper;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
1516
use Symfony\Component\VarDumper\Cloner\VarCloner;
1617
use Symfony\Component\VarDumper\Dumper\CliDumper;
18+
use Symfony\Component\VarDumper\Dumper\ContextProvider\BacktraceContextProvider;
1719
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
1820
use Symfony\Component\VarDumper\Dumper\ContextualizedDumper;
1921

2022
/**
2123
* @author Kévin Thérage <therage.kevin@gmail.com>
24+
* @author Alexandre Daubois <alex.daubois@gmail.com>
2225
*/
2326
class ContextualizedDumperTest extends TestCase
2427
{
@@ -28,8 +31,8 @@ public function testContextualizedCliDumper()
2831
$wrappedDumper->setColors(true);
2932

3033
$var = 'example';
31-
$href = \sprintf('file://%s#L%s', __FILE__, 37);
32-
$dumper = new ContextualizedDumper($wrappedDumper, [new SourceContextProvider()]);
34+
$href = \sprintf('file://%s#L%s', __FILE__, 40);
35+
$dumper = new ContextualizedDumper($wrappedDumper, [new SourceContextProvider(fileLinkFormatter: new FileLinkFormatter())]);
3336
$cloner = new VarCloner();
3437
$data = $cloner->cloneVar($var);
3538

@@ -40,4 +43,22 @@ public function testContextualizedCliDumper()
4043
$this->assertStringContainsString("\e]8;;{$href}\e\\^\e]", $out);
4144
$this->assertStringContainsString("m{$var}\e[", $out);
4245
}
46+
47+
public function testEnablingBacktraceDisplaysIt()
48+
{
49+
$wrappedDumper = new CliDumper('php://output');
50+
$cloner = new VarCloner();
51+
52+
$dumper = new ContextualizedDumper($wrappedDumper, [new SourceContextProvider(), new BacktraceContextProvider(0, $cloner)]);
53+
54+
ob_start();
55+
$dumper->dump(
56+
$cloner->cloneVar(123)->withContext([
57+
'options' => ['_trace' => true],
58+
])
59+
);
60+
$result = ob_get_clean();
61+
62+
$this->assertStringContainsString('**DEBUG BACKTRACE**', $result);
63+
}
4364
}

src/Symfony/Component/VarDumper/Tests/Dumper/FunctionsTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ public function testDumpReturnsAllNamedArgsInArray()
8585
$this->assertSame([$var1, 'second' => $var2, 'third' => $var3], $return);
8686
}
8787

88+
public function testDumpReturnsAllNamedArgsExceptSpecialOptionOnes()
89+
{
90+
$this->setupVarDumper();
91+
92+
$var1 = 'a';
93+
$var2 = 'b';
94+
95+
ob_start();
96+
$return = dump($var1, named: $var2, _trace: true, _flags: 0);
97+
ob_end_clean();
98+
99+
$this->assertSame([$var1, 'named' => $var2], $return);
100+
}
101+
88102
protected function setupVarDumper()
89103
{
90104
$cloner = new VarCloner();

0 commit comments

Comments
 (0)