Skip to content
Permalink
Browse files

feature #30301 [VarDumper] add link to source next to class names (ni…

…colas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[VarDumper] add link to source next to class names

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR adds a `^` next to language identifiers (class and callback names) both on the Web and on the CLI. Clicking it opens the IDE to the target source code:

Eg in the profiler:
![image](https://user-images.githubusercontent.com/243674/53021031-900c4380-3458-11e9-9439-260ff11f0910.png)

And in the CLI:
![image](https://user-images.githubusercontent.com/243674/53021092-b16d2f80-3458-11e9-9f03-cdab59da4585.png)

Commits
-------

5fcd6b1 [VarDumper] add link to source next to class names
  • Loading branch information...
fabpot committed Feb 21, 2019
2 parents 8c3dc82 + 5fcd6b1 commit cbe8cff8826f93741f6f2589f474d45d54cd28d7
@@ -43,6 +43,11 @@ public function load(array $configs, ContainerBuilder $container)
->addMethodCall('setMinDepth', [$config['min_depth']])
->addMethodCall('setMaxString', [$config['max_string_length']]);
if (method_exists(ReflectionClass::class, 'unsetClosureFileInfo')) {
$container->getDefinition('var_dumper.cloner')
->addMethodCall('addCasters', ReflectionClass::UNSET_CLOSURE_FILE_INFO);
}
if (method_exists(HtmlDumper::class, 'setTheme') && 'dark' !== $config['theme']) {
$container->getDefinition('var_dumper.html_dumper')
->addMethodCall('setTheme', [$config['theme']]);
@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
@@ -79,7 +80,7 @@ protected function cloneVar($var)
*/
protected function getCasters()
{
return [
$casters = [
'*' => function ($v, array $a, Stub $s, $isNested) {
if (!$v instanceof Stub) {
foreach ($a as $k => $v) {
@@ -92,5 +93,11 @@ protected function getCasters()
return $a;
},
];
if (method_exists(ReflectionCaster::class, 'unsetClosureFileInfo')) {
$casters += ReflectionCaster::UNSET_CLOSURE_FILE_INFO;
}
return $casters;
}
}
@@ -87,21 +87,17 @@ public static function generateUrlFormat(UrlGeneratorInterface $router, $routeNa
private function getFileLinkFormat()
{
if ($this->fileLinkFormat) {
return $this->fileLinkFormat;
}
if ($this->requestStack && $this->baseDir && $this->urlFormat) {
$request = $this->requestStack->getMasterRequest();
if ($request instanceof Request) {
if ($this->urlFormat instanceof \Closure && !$this->urlFormat = ($this->urlFormat)()) {
return;
}
return [
if ($request instanceof Request && (!$this->urlFormat instanceof \Closure || $this->urlFormat = ($this->urlFormat)())) {
$this->fileLinkFormat = [
$request->getSchemeAndHttpHost().$request->getBasePath().$this->urlFormat,
$this->baseDir.\DIRECTORY_SEPARATOR, '',
];
}
}
return $this->fileLinkFormat;
}
}
@@ -34,18 +34,6 @@ public function testWhenFileLinkFormatAndNoRequest()
$this->assertSame("debug://open?url=file://$file&line=3", $sut->format($file, 3));
}
public function testWhenFileLinkFormatAndRequest()
{
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
$requestStack = new RequestStack();
$request = new Request();
$requestStack->push($request);
$sut = new FileLinkFormatter('debug://open?url=file://%f&line=%l', $requestStack, __DIR__, '/_profiler/open?file=%f&line=%l#line%l');
$this->assertSame("debug://open?url=file://$file&line=3", $sut->format($file, 3));
}
public function testWhenNoFileLinkFormatAndRequest()
{
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
@@ -20,6 +20,8 @@
*/
class ReflectionCaster
{
const UNSET_CLOSURE_FILE_INFO = ['Closure' => __CLASS__.'::unsetClosureFileInfo'];
private static $extraMap = [
'docComment' => 'getDocComment',
'extension' => 'getExtensionName',
@@ -46,22 +48,34 @@ public static function castClosure(\Closure $c, array $a, Stub $stub, $isNested,
$stub->class .= self::getSignature($a);
if ($f = $c->getFileName()) {
$stub->attr['file'] = $f;
$stub->attr['line'] = $c->getStartLine();
}
unset($a[$prefix.'parameters']);
if ($filter & Caster::EXCLUDE_VERBOSE) {
$stub->cut += ($c->getFileName() ? 2 : 0) + \count($a);
return [];
}
unset($a[$prefix.'parameters']);
if ($f = $c->getFileName()) {
if ($f) {
$a[$prefix.'file'] = new LinkStub($f, $c->getStartLine());
$a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine();
}
return $a;
}
public static function unsetClosureFileInfo(\Closure $c, array $a)
{
unset($a[Caster::PREFIX_VIRTUAL.'file'], $a[Caster::PREFIX_VIRTUAL.'line']);
return $a;
}
public static function castGenerator(\Generator $c, array $a, Stub $stub, $isNested)
{
if (!class_exists('ReflectionGenerator', false)) {
@@ -279,7 +279,7 @@ protected function castObject(Stub $stub, $isNested)
$stub->class = get_parent_class($class).'@anonymous';
}
if (isset($this->classInfo[$class])) {
list($i, $parents, $hasDebugInfo) = $this->classInfo[$class];
list($i, $parents, $hasDebugInfo, $fileInfo) = $this->classInfo[$class];
} else {
$i = 2;
$parents = [$class];
@@ -295,9 +295,16 @@ protected function castObject(Stub $stub, $isNested)
}
$parents[] = '*';
$this->classInfo[$class] = [$i, $parents, $hasDebugInfo];
$r = new \ReflectionClass($class);
$fileInfo = $r->isInternal() || $r->isSubclassOf(Stub::class) ? [] : [
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
$this->classInfo[$class] = [$i, $parents, $hasDebugInfo, $fileInfo];
}
$stub->attr += $fileInfo;
$a = Caster::castObject($obj, $class, $hasDebugInfo);
try {
@@ -274,6 +274,7 @@ public function dumpString(Cursor $cursor, $str, $bin, $cut)
public function enterHash(Cursor $cursor, $type, $class, $hasChild)
{
$this->dumpKey($cursor);
$attr = $cursor->attr;
if ($this->collapseNextHash) {
$cursor->skipChildren = true;
@@ -282,11 +283,11 @@ public function enterHash(Cursor $cursor, $type, $class, $hasChild)
$class = $this->utf8Encode($class);
if (Cursor::HASH_OBJECT === $type) {
$prefix = $class && 'stdClass' !== $class ? $this->style('note', $class).' {' : '{';
$prefix = $class && 'stdClass' !== $class ? $this->style('note', $class, $attr).' {' : '{';
} elseif (Cursor::HASH_RESOURCE === $type) {
$prefix = $this->style('note', $class.' resource').($hasChild ? ' {' : ' ');
$prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' ');
} else {
$prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '[';
$prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class, $attr).' [' : '[';
}
if ($cursor->softRefCount || 0 < $cursor->softRefHandle) {
@@ -454,11 +455,9 @@ protected function style($style, $value, $attr = [])
goto href;
}
$style = $this->styles[$style];
$map = static::$controlCharsMap;
$startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : '';
$endCchr = $this->colors ? "\033[m\033[{$style}m" : '';
$endCchr = $this->colors ? "\033[m\033[{$this->styles[$style]}m" : '';
$value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) {
$s = $startCchr;
$c = $c[$i = 0];
@@ -473,7 +472,7 @@ protected function style($style, $value, $attr = [])
if ($cchrCount && "\033" === $value[0]) {
$value = substr($value, \strlen($startCchr));
} else {
$value = "\033[{$style}m".$value;
$value = "\033[{$this->styles[$style]}m".$value;
}
if ($cchrCount && $endCchr === substr($value, -\strlen($endCchr))) {
$value = substr($value, 0, -\strlen($endCchr));
@@ -485,7 +484,11 @@ protected function style($style, $value, $attr = [])
href:
if ($this->colors && $this->handlesHrefGracefully) {
if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
$attr['href'] = $href;
if ('note' === $style) {
$value .= "\033]8;;{$href}\033\\^\033]8;;\033\\";
} else {
$attr['href'] = $href;
}
}
if (isset($attr['href'])) {
$value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\";
@@ -632,7 +635,7 @@ private function isWindowsTrueColor()
private function getSourceLink($file, $line)
{
if ($fmt = $this->displayOptions['fileLinkFormat']) {
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file);
}
return false;
@@ -12,6 +12,7 @@
namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
@@ -29,6 +30,7 @@ public function __construct(RequestStack $requestStack)
$this->requestStack = $requestStack;
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(0);
$this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
}
public function getContext(): ?array
@@ -314,13 +314,17 @@ function resetHighlightedNodes(root) {
}
function a(e, f) {
addEventListener(root, e, function (e) {
addEventListener(root, e, function (e, n) {
if ('A' == e.target.tagName) {
f(e.target, e);
} else if ('A' == e.target.parentNode.tagName) {
f(e.target.parentNode, e);
} else if (e.target.nextElementSibling && 'A' == e.target.nextElementSibling.tagName) {
f(e.target.nextElementSibling, e, true);
} else if ((n = e.target.nextElementSibling) && 'A' == n.tagName) {
if (!/\bsf-dump-toggle\b/.test(n.className)) {
n = n.nextElementSibling;
}
f(n, e, true);
}
});
};
@@ -852,7 +856,13 @@ protected function style($style, $value, $attr = [])
} elseif ('str' === $style && 1 < $attr['length']) {
$style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : '');
} elseif ('note' === $style && false !== $c = strrpos($v, '\\')) {
return sprintf('<abbr title="%s" class=sf-dump-%s>%s</abbr>', $v, $style, substr($v, $c + 1));
if (isset($attr['file']) && $link = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
$link = sprintf('<a href="%s" rel="noopener noreferrer">^</a>', esc($this->utf8Encode($link)));
} else {
$link = '';
}
return sprintf('<abbr title="%s" class=sf-dump-%s>%s</abbr>%s', $v, $style, substr($v, $c + 1), $link);
} elseif ('protected' === $style) {
$style .= ' title="Protected property"';
} elseif ('meta' === $style && isset($attr['title'])) {
@@ -114,7 +114,7 @@ public function testClosureCasterExcludingVerbosity()
{
$var = function &($a = 5) {};
$this->assertDumpEquals('Closure&($a = 5) { …6}', $var, Caster::EXCLUDE_VERBOSE);
$this->assertDumpEquals('Closure&($a = 5) { …5}', $var, Caster::EXCLUDE_VERBOSE);
}
public function testReflectionParameter()
@@ -411,6 +411,8 @@ public function testCaster()
[position] => 1
[attr] => Array
(
[file] => %a%eVarClonerTest.php
[line] => 20
)
)
@@ -11,6 +11,7 @@
namespace Symfony\Component\VarDumper;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
@@ -29,6 +30,7 @@ public static function dump($var)
{
if (null === self::$handler) {
$cloner = new VarCloner();
$cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
if (isset($_SERVER['VAR_DUMPER_FORMAT'])) {
$dumper = 'html' === $_SERVER['VAR_DUMPER_FORMAT'] ? new HtmlDumper() : new CliDumper();

0 comments on commit cbe8cff

Please sign in to comment.
You can’t perform that action at this time.