diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 9c098a0996..a183e82cf8 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -43,6 +43,11 @@ class DebugExtension extends Extension ->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']]); diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php index efe1705179..3b15868ff5 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php @@ -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 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable */ 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 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable return $a; }, ]; + + if (method_exists(ReflectionCaster::class, 'unsetClosureFileInfo')) { + $casters += ReflectionCaster::UNSET_CLOSURE_FILE_INFO; + } + + return $casters; } } diff --git a/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php b/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php index d83f7920df..54defbf1d1 100644 --- a/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php +++ b/src/Symfony/Component/HttpKernel/Debug/FileLinkFormatter.php @@ -87,21 +87,17 @@ class FileLinkFormatter 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; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php index 5c93bd90e3..1f4d298bf3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/FileLinkFormatterTest.php @@ -34,18 +34,6 @@ class FileLinkFormatterTest extends TestCase $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'; diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index 5290fedfcf..c686962dbd 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -20,6 +20,8 @@ use Symfony\Component\VarDumper\Cloner\Stub; */ class ReflectionCaster { + const UNSET_CLOSURE_FILE_INFO = ['Closure' => __CLASS__.'::unsetClosureFileInfo']; + private static $extraMap = [ 'docComment' => 'getDocComment', 'extension' => 'getExtensionName', @@ -46,15 +48,20 @@ class ReflectionCaster $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(); } @@ -62,6 +69,13 @@ class ReflectionCaster 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)) { diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 80b5d548fb..7891edf2a4 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -279,7 +279,7 @@ abstract class AbstractCloner implements ClonerInterface $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 @@ abstract class AbstractCloner implements ClonerInterface } $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 { diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php index 3fb7ac619e..3ca3e33587 100644 --- a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php @@ -274,6 +274,7 @@ class CliDumper extends AbstractDumper public function enterHash(Cursor $cursor, $type, $class, $hasChild) { $this->dumpKey($cursor); + $attr = $cursor->attr; if ($this->collapseNextHash) { $cursor->skipChildren = true; @@ -282,11 +283,11 @@ class CliDumper extends AbstractDumper $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 @@ class CliDumper extends AbstractDumper 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 @@ class CliDumper extends AbstractDumper 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 @@ class CliDumper extends AbstractDumper 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 @@ class CliDumper extends AbstractDumper 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; diff --git a/src/Symfony/Component/VarDumper/Dumper/ContextProvider/RequestContextProvider.php b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/RequestContextProvider.php index bb3cc863fe..3684a47535 100644 --- a/src/Symfony/Component/VarDumper/Dumper/ContextProvider/RequestContextProvider.php +++ b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/RequestContextProvider.php @@ -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 @@ final class RequestContextProvider implements ContextProviderInterface $this->requestStack = $requestStack; $this->cloner = new VarCloner(); $this->cloner->setMaxItems(0); + $this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); } public function getContext(): ?array diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php index 37651816e6..0acf3b502b 100644 --- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php @@ -314,13 +314,17 @@ return function (root, x) { } 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 @@ EOHTML } 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('%s', $v, $style, substr($v, $c + 1)); + if (isset($attr['file']) && $link = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) { + $link = sprintf('^', esc($this->utf8Encode($link))); + } else { + $link = ''; + } + + return sprintf('%s%s', $v, $style, substr($v, $c + 1), $link); } elseif ('protected' === $style) { $style .= ' title="Protected property"'; } elseif ('meta' === $style && isset($attr['title'])) { diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index f53f06ab5f..ebe9ab4b94 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -114,7 +114,7 @@ EOTXT { $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() diff --git a/src/Symfony/Component/VarDumper/Tests/Cloner/VarClonerTest.php b/src/Symfony/Component/VarDumper/Tests/Cloner/VarClonerTest.php index 3b180af498..d3141c6eaf 100644 --- a/src/Symfony/Component/VarDumper/Tests/Cloner/VarClonerTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Cloner/VarClonerTest.php @@ -411,6 +411,8 @@ Symfony\Component\VarDumper\Cloner\Data Object [position] => 1 [attr] => Array ( + [file] => %a%eVarClonerTest.php + [line] => 20 ) ) diff --git a/src/Symfony/Component/VarDumper/VarDumper.php b/src/Symfony/Component/VarDumper/VarDumper.php index 4271e63965..009f662f3b 100644 --- a/src/Symfony/Component/VarDumper/VarDumper.php +++ b/src/Symfony/Component/VarDumper/VarDumper.php @@ -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 @@ class VarDumper { 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();