Skip to content

Commit aecc2a1

Browse files
authored
Merge pull request #134 from anabeto93/feature/dd-source
Adds the source file and line number to `dd()` output
2 parents 394d5fd + f455e52 commit aecc2a1

File tree

9 files changed

+1015
-0
lines changed

9 files changed

+1015
-0
lines changed

.php-cs-fixer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
->exclude('bin')
9595
->exclude('overrides')
9696
->exclude('vendor')
97+
->notPath('tests/Foundation/fixtures/fake-compiled-view.php')
9798
->in(__DIR__)
9899
)
99100
->setUsingCache(false);
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Foundation\Concerns;
6+
7+
use Throwable;
8+
9+
trait ResolvesDumpSource
10+
{
11+
/**
12+
* All of the href formats for common editors.
13+
*
14+
* @var array<string, string>
15+
*/
16+
protected array $editorHrefs = [
17+
'atom' => 'atom://core/open/file?filename={file}&line={line}',
18+
'cursor' => 'cursor://file/{file}:{line}',
19+
'emacs' => 'emacs://open?url=file://{file}&line={line}',
20+
'idea' => 'idea://open?file={file}&line={line}',
21+
'macvim' => 'mvim://open/?url=file://{file}&line={line}',
22+
'netbeans' => 'netbeans://open/?f={file}:{line}',
23+
'nova' => 'nova://core/open/file?filename={file}&line={line}',
24+
'phpstorm' => 'phpstorm://open?file={file}&line={line}',
25+
'sublime' => 'subl://open?url=file://{file}&line={line}',
26+
'textmate' => 'txmt://open?url=file://{file}&line={line}',
27+
'vscode' => 'vscode://file/{file}:{line}',
28+
'vscode-insiders' => 'vscode-insiders://file/{file}:{line}',
29+
'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/{file}:{line}',
30+
'vscode-remote' => 'vscode://vscode-remote/{file}:{line}',
31+
'vscodium' => 'vscodium://file/{file}:{line}',
32+
'xdebug' => 'xdebug://{file}@{line}',
33+
];
34+
35+
/**
36+
* Files that require special trace handling and their levels.
37+
*
38+
* @var array<string, int>
39+
*/
40+
protected static array $adjustableTraces = [
41+
'symfony/var-dumper/Resources/functions/dump.php' => 1,
42+
];
43+
44+
/**
45+
* The source resolver.
46+
*
47+
* @var null|(callable(): (null|array{0: string, 1: string, 2: null|int}))|false
48+
*/
49+
protected static $dumpSourceResolver;
50+
51+
/**
52+
* Resolve the source of the dump call.
53+
*
54+
* @return null|array{0: string, 1: string, 2: null|int}
55+
*/
56+
public function resolveDumpSource(): ?array
57+
{
58+
if (static::$dumpSourceResolver === false) {
59+
return null;
60+
}
61+
62+
if (static::$dumpSourceResolver) {
63+
return call_user_func(static::$dumpSourceResolver);
64+
}
65+
66+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);
67+
68+
$sourceKey = null;
69+
70+
foreach ($trace as $traceKey => $traceFile) {
71+
if (! isset($traceFile['file'])) {
72+
continue;
73+
}
74+
75+
foreach (self::$adjustableTraces as $name => $key) {
76+
if (str_ends_with(
77+
$traceFile['file'],
78+
str_replace('/', DIRECTORY_SEPARATOR, $name)
79+
)) {
80+
$sourceKey = $traceKey + $key;
81+
break;
82+
}
83+
}
84+
85+
if (! is_null($sourceKey)) {
86+
break;
87+
}
88+
}
89+
90+
if (is_null($sourceKey)) {
91+
return null;
92+
}
93+
94+
$file = $trace[$sourceKey]['file'] ?? null;
95+
$line = $trace[$sourceKey]['line'] ?? null;
96+
97+
if (is_null($file) || is_null($line)) {
98+
return null;
99+
}
100+
101+
$relativeFile = $file;
102+
103+
if ($this->isCompiledViewFile($file)) {
104+
$file = $this->getOriginalFileForCompiledView($file);
105+
$line = null;
106+
}
107+
108+
if (str_starts_with($file, $this->basePath)) {
109+
$relativeFile = substr($file, strlen($this->basePath) + 1);
110+
}
111+
112+
return [$file, $relativeFile, $line];
113+
}
114+
115+
/**
116+
* Determine if the given file is a view compiled.
117+
*/
118+
protected function isCompiledViewFile(string $file): bool
119+
{
120+
return str_starts_with($file, $this->compiledViewPath) && str_ends_with($file, '.php');
121+
}
122+
123+
/**
124+
* Get the original view compiled file by the given compiled file.
125+
*/
126+
protected function getOriginalFileForCompiledView(string $file): string
127+
{
128+
preg_match('/\/\*\*PATH\s(.*)\sENDPATH/', file_get_contents($file), $matches);
129+
130+
if (isset($matches[1])) {
131+
$file = $matches[1];
132+
}
133+
134+
return $file;
135+
}
136+
137+
/**
138+
* Resolve the source href, if possible.
139+
*
140+
* @return null|string|void
141+
*/
142+
protected function resolveSourceHref(string $file, ?int $line)
143+
{
144+
try {
145+
$editor = config('app.editor');
146+
} catch (Throwable) {
147+
// ..
148+
}
149+
150+
if (! isset($editor)) {
151+
return;
152+
}
153+
154+
$href = is_array($editor) && isset($editor['href'])
155+
? $editor['href']
156+
: ($this->editorHrefs[$editor['name'] ?? $editor] ?? sprintf('%s://open?file={file}&line={line}', $editor['name'] ?? $editor));
157+
158+
if ($basePath = $editor['base_path'] ?? false) {
159+
$file = str_replace($this->basePath, $basePath, $file);
160+
}
161+
162+
return str_replace(
163+
['{file}', '{line}'],
164+
[$file, is_null($line) ? 1 : $line],
165+
$href,
166+
);
167+
}
168+
169+
/**
170+
* Set the resolver that resolves the source of the dump call.
171+
*
172+
* @param null|(callable(): (null|array{0: string, 1: string, 2: null|int})) $callable
173+
*/
174+
public static function resolveDumpSourceUsing(?callable $callable): void
175+
{
176+
static::$dumpSourceResolver = $callable;
177+
}
178+
179+
/**
180+
* Don't include the location / file of the dump in dumps.
181+
*/
182+
public static function dontIncludeSource(): void
183+
{
184+
static::$dumpSourceResolver = false;
185+
}
186+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Foundation\Console;
6+
7+
use Hypervel\Foundation\Concerns\ResolvesDumpSource;
8+
use Symfony\Component\Console\Output\ConsoleOutput;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
11+
use Symfony\Component\VarDumper\Cloner\Data;
12+
use Symfony\Component\VarDumper\Cloner\VarCloner;
13+
use Symfony\Component\VarDumper\Dumper\CliDumper as BaseCliDumper;
14+
use Symfony\Component\VarDumper\VarDumper;
15+
16+
class CliDumper extends BaseCliDumper
17+
{
18+
use ResolvesDumpSource;
19+
20+
/**
21+
* If the dumper is currently dumping.
22+
*/
23+
protected bool $dumping = false;
24+
25+
/**
26+
* Create a new CLI dumper instance.
27+
*
28+
* @param OutputInterface $output
29+
*/
30+
public function __construct(
31+
protected mixed $output,
32+
protected string $basePath,
33+
protected ?string $compiledViewPath,
34+
) {
35+
parent::__construct();
36+
37+
$this->setColors($this->supportsColors());
38+
}
39+
40+
/**
41+
* Create a new CLI dumper instance and register it as the default dumper.
42+
*
43+
* @param string $basePath
44+
* @param string $compiledViewPath
45+
*/
46+
public static function register($basePath, $compiledViewPath): void
47+
{
48+
$cloner = tap(new VarCloner())->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
49+
50+
$dumper = new static(new ConsoleOutput(), $basePath, $compiledViewPath);
51+
52+
VarDumper::setHandler(fn ($value) => $dumper->dumpWithSource($cloner->cloneVar($value)));
53+
}
54+
55+
/**
56+
* Dump a variable with its source file / line.
57+
*/
58+
public function dumpWithSource(Data $data): void
59+
{
60+
if ($this->dumping) {
61+
$this->dump($data);
62+
63+
return;
64+
}
65+
66+
$this->dumping = true;
67+
68+
$output = (string) $this->dump($data, true);
69+
$lines = explode("\n", $output);
70+
71+
$lines[array_key_last($lines) - 1] .= $this->getDumpSourceContent();
72+
73+
$this->output->write(implode("\n", $lines));
74+
75+
$this->dumping = false;
76+
}
77+
78+
/**
79+
* Get the dump's source console content.
80+
*/
81+
protected function getDumpSourceContent(): string
82+
{
83+
if (is_null($dumpSource = $this->resolveDumpSource())) {
84+
return '';
85+
}
86+
87+
[$file, $relativeFile, $line] = $dumpSource;
88+
89+
$href = $this->resolveSourceHref($file, $line);
90+
91+
return sprintf(
92+
' <fg=gray>// <fg=gray%s>%s%s</></>',
93+
is_null($href) ? '' : ";href={$href}",
94+
$relativeFile,
95+
is_null($line) ? '' : ":{$line}"
96+
);
97+
}
98+
99+
protected function supportsColors(): bool
100+
{
101+
return $this->output->isDecorated();
102+
}
103+
}

0 commit comments

Comments
 (0)