From ce3ef6a96ef37f46b34452d447c15643383dacb5 Mon Sep 17 00:00:00 2001 From: Thomas Perez Date: Tue, 11 Apr 2017 23:22:30 +0200 Subject: [PATCH 1/2] Persist app bootstrapping logs for logger datacollector --- .../Resources/config/collectors.xml | 1 + .../views/Collector/logger.html.twig | 58 +++++++++++++++++-- .../Twig/WebProfilerExtension.php | 7 ++- .../DependencyInjection/Compiler/Compiler.php | 4 ++ .../DataCollector/LoggerDataCollector.php | 56 +++++++++++++++++- src/Symfony/Component/HttpKernel/Kernel.php | 24 +++++++- 6 files changed, 136 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index afebe6da98fd8..340c7e1972801 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -32,6 +32,7 @@ + %kernel.cache_dir%/%kernel.container_class% diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index 554e1e61dd09e..8843a95ecd6c0 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -124,6 +124,52 @@ + {% set compilerLogTotal = 0 %} + {% for logs in collector.compilerLogs %} + {% set compilerLogTotal = compilerLogTotal + logs|length %} + {% endfor %} +
+

Container Compilation{{ compilerLogTotal }}

+ +
+ {% if collector.compilerLogs is empty %} +
+

There are no compiler log messages.

+
+ {% else %} + + + + + + + + + + {% for class, logs in collector.compilerLogs %} + + + + + {% endfor %} + +
ClassMessages
+ {% set context_id = 'context-compiler-' ~ loop.index %} + + {{ class }} + +
+
    + {% for log in logs %} +
  • {{ profiler_dump_log(log.message) }}
  • + {% endfor %} +
+
+
{{ logs|length }}
+ {% endif %} +
+
+ {% endif %} {% endblock %} @@ -165,16 +211,16 @@ {% endif %} - {{ helper.render_log_message(category, loop.index, log, is_deprecation) }} + {{ helper.render_log_message(category, loop.index, log) }} {% endfor %} {% endmacro %} -{% macro render_log_message(category, log_index, log, is_deprecation = false) %} - {% if is_deprecation %} - {{ log.message }} +{% macro render_log_message(category, log_index, log) %} + {% if log.context.exception.trace is defined %} + {{ profiler_dump_log(log.message, log.context) }} {% set context_id = 'context-' ~ category ~ '-' ~ log_index %} @@ -182,7 +228,7 @@ Show trace
- {{ profiler_dump(log.context.exception['\0Exception\0trace'], maxDepth=2) }} + {{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
{% elseif log.context is defined and log.context is not empty %} @@ -198,6 +244,6 @@ {% else %} - {{ log.message }} + {{ profiler_dump_log(log.message) }} {% endif %} {% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php index 664763ea67a10..4662cbf56b82e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php @@ -89,18 +89,19 @@ public function dumpData(\Twig_Environment $env, Data $data, $maxDepth = 0) return str_replace("\n$1"', $message); - if (false === strpos($message, '{')) { + if (null === $context || false === strpos($message, '{')) { return ''.$message.''; } $replacements = array(); foreach ($context as $k => $v) { $k = '{'.twig_escape_filter($env, $k).'}'; - $replacements['"'.$k.'"'] = $replacements[$k] = $this->dumpData($env, $v); + $replacements['"'.$k.'"'] = $replacements['"'.$k.'"'] = $replacements[$k] = $this->dumpData($env, $v); } return ''.strtr($message, $replacements).''; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php index 25f352f63fdca..8a2ef34f5b8a8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php @@ -114,6 +114,10 @@ public function addLogMessage($string) */ public function log(CompilerPassInterface $pass, $message) { + if (false !== strpos($message, "\n")) { + $message = str_replace("\n", "\n".get_class($pass).': ', trim($message)); + } + $this->log[] = get_class($pass).': '.$message; } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 217290aa103d7..d32c581cb6b28 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -24,12 +24,15 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface { private $logger; + private $containerPathPrefix; - public function __construct($logger = null) + public function __construct($logger = null, $containerPathPrefix = null) { if (null !== $logger && $logger instanceof DebugLoggerInterface) { $this->logger = $logger; } + + $this->containerPathPrefix = $containerPathPrefix; } /** @@ -47,7 +50,11 @@ public function lateCollect() { if (null !== $this->logger) { $this->data = $this->computeErrorsCount(); - $this->data['logs'] = $this->sanitizeLogs($this->logger->getLogs()); + + $containerDeprecationLogs = $this->getContainerDeprecationLogs(); + $this->data['deprecation_count'] += count($containerDeprecationLogs); + $this->data['compiler_logs'] = $this->getContainerCompilerLogs(); + $this->data['logs'] = $this->sanitizeLogs(array_merge($this->logger->getLogs(), $containerDeprecationLogs)); $this->data = $this->cloneVar($this->data); } } @@ -87,6 +94,11 @@ public function countScreams() return isset($this->data['scream_count']) ? $this->data['scream_count'] : 0; } + public function getCompilerLogs() + { + return isset($this->data['compiler_logs']) ? $this->data['compiler_logs'] : array(); + } + /** * {@inheritdoc} */ @@ -95,6 +107,44 @@ public function getName() return 'logger'; } + private function getContainerDeprecationLogs() + { + if (null === $this->containerPathPrefix || !file_exists($file = $this->containerPathPrefix.'Deprecations.log')) { + return array(); + } + + $stubs = array(); + $bootTime = filemtime($file); + $logs = array(); + foreach (unserialize(file_get_contents($file)) as $log) { + $log['context'] = array('exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'])); + $log['timestamp'] = $bootTime; + $log['priority'] = 100; + $log['priorityName'] = 'DEBUG'; + $log['channel'] = '-'; + $log['scream'] = false; + $logs[] = $log; + } + + return $logs; + } + + private function getContainerCompilerLogs() + { + if (null === $this->containerPathPrefix || !file_exists($file = $this->containerPathPrefix.'Compiler.log')) { + return array(); + } + + $logs = array(); + foreach (file($file, FILE_IGNORE_NEW_LINES) as $log) { + $log = explode(': ', $log, 2); + + $logs[$log[0]][] = array('message' => $log[1]); + } + + return $logs; + } + private function sanitizeLogs($logs) { $sanitizedLogs = array(); @@ -107,7 +157,7 @@ private function sanitizeLogs($logs) } $exception = $log['context']['exception']; - $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}".($exception instanceof \Exception ? "\0".$exception->getMessage() : ''), true); + $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$log['message']}", true); if (isset($sanitizedLogs[$errorId])) { ++$sanitizedLogs[$errorId]['errorCount']; diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index d83b3532246f8..33950a08b2774 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -539,12 +539,32 @@ protected function initializeContainer() $cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug); $fresh = true; if (!$cache->isFresh()) { - $container = $this->buildContainer(); + if ($this->debug) { + $collectedLogs = array(); + $previousHandler = set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { + if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; + } + + $collectedLogs[] = array( + 'type' => $type, + 'message' => $message, + 'file' => $file, + 'line' => $line, + ); + }); + } + try { + $container = null; + $container = $this->buildContainer(); $container->compile(); } finally { if ($this->debug) { - file_put_contents($this->getCacheDir().'/'.$class.'Compiler.log', implode("\n", $container->getCompiler()->getLog())); + restore_error_handler(); + + file_put_contents($this->getCacheDir().'/'.$class.'Deprecations.log', serialize($collectedLogs)); + file_put_contents($this->getCacheDir().'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); } } $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); From 2fd18b55031e0a7f7e5a59dbaaee1ed714ee12aa Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 17 Apr 2017 15:29:15 +0200 Subject: [PATCH 2/2] [VarDumper] Fine tune dumping log messages --- .../views/Profiler/profiler.css.twig | 1 + .../Component/Debug/DebugClassLoader.php | 16 ++-- .../Debug/Tests/DebugClassLoaderTest.php | 8 +- .../Component/VarDumper/Caster/ClassStub.php | 4 +- .../VarDumper/Caster/ExceptionCaster.php | 51 ++++++++++-- .../Component/VarDumper/Caster/LinkStub.php | 83 +++++++++++++++---- .../VarDumper/Cloner/AbstractCloner.php | 1 + .../Component/VarDumper/Dumper/HtmlDumper.php | 16 +++- .../Tests/Caster/ExceptionCasterTest.php | 12 +-- .../VarDumper/Tests/Caster/StubCasterTest.php | 2 +- .../VarDumper/Tests/Dumper/CliDumperTest.php | 2 +- .../VarDumper/Tests/Dumper/HtmlDumperTest.php | 4 +- 12 files changed, 152 insertions(+), 48 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 83f9ea3c8b8d4..0a4a808713ad5 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -913,6 +913,7 @@ table.logs .metadata { #collector-content .sf-dump-key { color: #789339; } #collector-content .sf-dump-ref { color: #6E6E6E; } #collector-content .sf-dump-ellipsis { color: #CC7832; max-width: 100em; } +#collector-content .sf-dump-ellipsis-path { max-width: 5em; } #collector-content .sf-dump { margin: 0; diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 3b76eb4887661..2e1d71808e132 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -162,7 +162,7 @@ public function loadClass($class) $name = $refl->getName(); if ($name !== $class && 0 === strcasecmp($name, $class)) { - throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name)); + throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); } $parent = get_parent_class($class); @@ -174,7 +174,7 @@ public function loadClass($class) } if ($parent && isset(self::$final[$parent])) { - @trigger_error(sprintf('The %s class is considered final%s. It may change without further notice as of its next major version. You should not extend it from %s.', $parent, self::$final[$parent], $name), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $name), E_USER_DEPRECATED); } // Inherit @final annotations @@ -186,7 +186,7 @@ public function loadClass($class) } if ($parent && isset(self::$finalMethods[$parent][$method->name])) { - @trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from %s.', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED); + @trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from "%s".', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED); } $doc = $method->getDocComment(); @@ -196,13 +196,13 @@ public function loadClass($class) if (preg_match('#\n\s+\* @final(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { $message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; - self::$finalMethods[$name][$method->name] = sprintf('The %s::%s() method is considered final%s.', $name, $method->name, $message); + self::$finalMethods[$name][$method->name] = sprintf('The "%s::%s()" method is considered final%s.', $name, $method->name, $message); } } } if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) { - @trigger_error(sprintf('%s uses a reserved class name (%s) that will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class uses the reserved name "%s", it will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED); } elseif (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]); } else { @@ -223,7 +223,7 @@ public function loadClass($class) if (!$parent || strncmp($ns, $parent, $len)) { if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) { - @trigger_error(sprintf('The %s class extends %s that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class extends "%s" that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED); } $parentInterfaces = array(); @@ -245,7 +245,7 @@ public function loadClass($class) foreach ($deprecatedInterfaces as $interface) { if (!isset($parentInterfaces[$interface])) { - @trigger_error(sprintf('The %s %s %s that is deprecated %s', $name, $refl->isInterface() ? 'interface extends' : 'class implements', $interface, self::$deprecated[$interface]), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" %s "%s" that is deprecated %s', $name, $refl->isInterface() ? 'interface extends' : 'class implements', $interface, self::$deprecated[$interface]), E_USER_DEPRECATED); } } } @@ -342,7 +342,7 @@ public function loadClass($class) if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true) && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false) ) { - throw new \RuntimeException(sprintf('Case mismatch between class and real file names: %s vs %s in %s', substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1))); + throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1))); } } diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 8602579237f05..765e8d9b86bfd 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -185,7 +185,7 @@ class_exists('Test\\'.__NAMESPACE__.'\\'.$class, true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'The Test\Symfony\Component\Debug\Tests\\'.$class.' class '.$type.' Symfony\Component\Debug\Tests\Fixtures\\'.$super.' that is deprecated but this is a test deprecation notice.', + 'message' => 'The "Test\Symfony\Component\Debug\Tests\\'.$class.'" class '.$type.' "Symfony\Component\Debug\Tests\Fixtures\\'.$super.'" that is deprecated but this is a test deprecation notice.', ); $this->assertSame($xError, $lastError); @@ -263,7 +263,7 @@ class_exists('Test\\'.__NAMESPACE__.'\\Float', true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'Test\Symfony\Component\Debug\Tests\Float uses a reserved class name (Float) that will break on PHP 7 and higher', + 'message' => 'The "Test\Symfony\Component\Debug\Tests\Float" class uses the reserved name "Float", it will break on PHP 7 and higher', ); $this->assertSame($xError, $lastError); @@ -285,7 +285,7 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'The Symfony\Component\Debug\Tests\Fixtures\FinalClass class is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from Test\Symfony\Component\Debug\Tests\ExtendsFinalClass.', + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\FinalClass" class is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsFinalClass".', ); $this->assertSame($xError, $lastError); @@ -307,7 +307,7 @@ class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'The Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod() method is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod.', + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod".', ); $this->assertSame($xError, $lastError); diff --git a/src/Symfony/Component/VarDumper/Caster/ClassStub.php b/src/Symfony/Component/VarDumper/Caster/ClassStub.php index 2b3e9dbd2dcaf..51990bc356c0d 100644 --- a/src/Symfony/Component/VarDumper/Caster/ClassStub.php +++ b/src/Symfony/Component/VarDumper/Caster/ClassStub.php @@ -30,6 +30,8 @@ public function __construct($identifier, $callable = null) if (0 < $i = strrpos($identifier, '\\')) { $this->attr['ellipsis'] = strlen($identifier) - $i; + $this->attr['ellipsis-type'] = 'class'; + $this->attr['ellipsis-tail'] = 1; } try { @@ -49,7 +51,7 @@ public function __construct($identifier, $callable = null) } else { $r = new \ReflectionFunction($callable); } - } elseif (false !== $i = strpos($identifier, '::')) { + } elseif (0 < $i = strpos($identifier, '::') ?: strpos($identifier, '->')) { $r = array(substr($identifier, 0, $i), substr($identifier, 2 + $i)); } else { $r = new \ReflectionClass($identifier); diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index 9a47e7789acf0..e2a4200c52503 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; +use Symfony\Component\Debug\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; use Symfony\Component\VarDumper\Cloner\Stub; @@ -64,13 +65,14 @@ public static function castErrorException(\ErrorException $e, array $a, Stub $st public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, $isNested) { + $trace = Caster::PREFIX_VIRTUAL.'trace'; $prefix = Caster::PREFIX_PROTECTED; $xPrefix = "\0Exception\0"; - if (isset($a[$xPrefix.'previous'], $a[$xPrefix.'trace']) && $a[$xPrefix.'previous'] instanceof \Exception) { + if (isset($a[$xPrefix.'previous'], $a[$trace]) && $a[$xPrefix.'previous'] instanceof \Exception) { $b = (array) $a[$xPrefix.'previous']; self::traceUnshift($b[$xPrefix.'trace'], get_class($a[$xPrefix.'previous']), $b[$prefix.'file'], $b[$prefix.'line']); - $a[$xPrefix.'trace'] = new TraceStub($b[$xPrefix.'trace'], false, 0, -count($a[$xPrefix.'trace']->value)); + $a[$trace] = new TraceStub($b[$xPrefix.'trace'], false, 0, -count($a[$trace]->value)); } unset($a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']); @@ -78,6 +80,29 @@ public static function castThrowingCasterException(ThrowingCasterException $e, a return $a; } + public static function castSilencedErrorContext(SilencedErrorContext $e, array $a, Stub $stub, $isNested) + { + $sPrefix = "\0".SilencedErrorContext::class."\0"; + $xPrefix = "\0Exception\0"; + + if (!isset($a[$s = $sPrefix.'severity'])) { + return $a; + } + + if (isset(self::$errorTypes[$a[$s]])) { + $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]); + } + + $trace = array( + 'file' => $a[$sPrefix.'file'], + 'line' => $a[$sPrefix.'line'], + ); + unset($a[$sPrefix.'file'], $a[$sPrefix.'line']); + $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub(array($trace)); + + return $a; + } + public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $isNested) { if (!$isNested) { @@ -101,9 +126,8 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) { $f = $frames[$i]; - $call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'].'()' : '???'; + $call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'] : '???'; - $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall; $frame = new FrameStub( array( 'object' => isset($f['object']) ? $f['object'] : null, @@ -123,6 +147,16 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is if ($trace->keepArgs && !empty($f['args']) && $frame instanceof EnumStub) { $frame->value['arguments'] = new ArgsStub($f['args'], isset($f['function']) ? $f['function'] : null, isset($f['class']) ? $f['class'] : null); } + } elseif ('???' !== $lastCall) { + $label = new ClassStub($lastCall); + if (isset($label->attr['ellipsis'])) { + $label->attr['ellipsis'] += 2; + $label = substr_replace($prefix, "ellipsis-type=class&ellipsis={$label->attr['ellipsis']}&ellipsis-tail=1&title=Stack level $j.", 2, 0).$label->value.'()'; + } else { + $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$label->value.'()'; + } + } else { + $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall; } $a[$label] = $frame; @@ -159,8 +193,9 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $is $caller = isset($f['function']) ? sprintf('in %s() on line %d', (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'], $f['line']) : null; $src = $f['line']; $srcKey = $f['file']; - $ellipsis = explode(DIRECTORY_SEPARATOR, $srcKey); - $ellipsis = 3 < count($ellipsis) ? 2 + strlen(implode(array_slice($ellipsis, -2))) : 0; + $ellipsis = (new LinkStub($srcKey, 0))->attr; + $ellipsisTail = isset($ellipsis['ellipsis-tail']) ? $ellipsis['ellipsis-tail'] : 0; + $ellipsis = isset($ellipsis['ellipsis']) ? $ellipsis['ellipsis'] : 0; if (file_exists($f['file']) && 0 <= self::$srcContext) { if (!empty($f['class']) && is_subclass_of($f['class'], 'Twig_Template') && method_exists($f['class'], 'getDebugInfo')) { @@ -187,7 +222,7 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $is } } } - $srcAttr = $ellipsis ? 'ellipsis='.$ellipsis : ''; + $srcAttr = $ellipsis ? 'ellipsis-type=path&ellipsis='.$ellipsis.'&ellipsis-tail='.$ellipsisTail : ''; self::$framesCache[$cacheKey] = $a[$prefix.'src'] = new EnumStub(array("\0~$srcAttr\0$srcKey" => $src)); } } @@ -221,7 +256,7 @@ private static function filterExceptionArray($xClass, array $a, $xPrefix, $filte if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) { self::traceUnshift($trace, $xClass, $a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']); } - $a[$xPrefix.'trace'] = new TraceStub($trace, self::$traceArgs); + $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs); } if (empty($a[$xPrefix.'previous'])) { unset($a[$xPrefix.'previous']); diff --git a/src/Symfony/Component/VarDumper/Caster/LinkStub.php b/src/Symfony/Component/VarDumper/Caster/LinkStub.php index ea39c5b08eef7..1a9aa419d148f 100644 --- a/src/Symfony/Component/VarDumper/Caster/LinkStub.php +++ b/src/Symfony/Component/VarDumper/Caster/LinkStub.php @@ -18,6 +18,9 @@ */ class LinkStub extends ConstStub { + private static $vendorRoots; + private static $composerRoots; + public function __construct($label, $line = 0, $href = null) { $this->value = $label; @@ -25,27 +28,75 @@ public function __construct($label, $line = 0, $href = null) if (null === $href) { $href = $label; } - if (is_string($href)) { - if (0 === strpos($href, 'file://')) { - if ($href === $label) { - $label = substr($label, 7); + if (!is_string($href)) { + return; + } + if (0 === strpos($href, 'file://')) { + if ($href === $label) { + $label = substr($label, 7); + } + $href = substr($href, 7); + } elseif (false !== strpos($href, '://')) { + $this->attr['href'] = $href; + + return; + } + if (!file_exists($href)) { + return; + } + if ($line) { + $this->attr['line'] = $line; + } + if ($label !== $this->attr['file'] = realpath($href) ?: $href) { + return; + } + if ($composerRoot = $this->getComposerRoot($href, $inVendor)) { + $this->attr['ellipsis'] = strlen($href) - strlen($composerRoot) + 1; + $this->attr['ellipsis-type'] = 'path'; + $this->attr['ellipsis-tail'] = 1 + ($inVendor ? 2 + strlen(implode(array_slice(explode(DIRECTORY_SEPARATOR, substr($href, 1 - $this->attr['ellipsis'])), 0, 2))) : 0); + } elseif (3 < count($ellipsis = explode(DIRECTORY_SEPARATOR, $href))) { + $this->attr['ellipsis'] = 2 + strlen(implode(array_slice($ellipsis, -2))); + $this->attr['ellipsis-type'] = 'path'; + $this->attr['ellipsis-tail'] = 1; + } + } + + private function getComposerRoot($file, &$inVendor) + { + if (null === self::$vendorRoots) { + self::$vendorRoots = array(); + + foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $v = dirname(dirname($r->getFileName())); + if (file_exists($v.'/composer/installed.json')) { + self::$vendorRoots[] = $v.DIRECTORY_SEPARATOR; + } } - $href = substr($href, 7); - } elseif (false !== strpos($href, '://')) { - $this->attr['href'] = $href; + } + } + $inVendor = false; + + if (isset(self::$composerRoots[$dir = dirname($file)])) { + return self::$composerRoots[$dir]; + } - return; + foreach (self::$vendorRoots as $root) { + if ($inVendor = 0 === strpos($file, $root)) { + return $root; } - if (file_exists($href)) { - if ($line) { - $this->attr['line'] = $line; - } - $this->attr['file'] = realpath($href) ?: $href; + } - if ($this->attr['file'] === $label && 3 < count($ellipsis = explode(DIRECTORY_SEPARATOR, $href))) { - $this->attr['ellipsis'] = 2 + strlen(implode(array_slice($ellipsis, -2))); - } + $parent = $dir; + while (!file_exists($parent.'/composer.json')) { + if ($parent === dirname($parent)) { + return self::$composerRoots[$dir] = false; } + + $parent = dirname($parent); } + + return self::$composerRoots[$dir] = $parent.DIRECTORY_SEPARATOR; } } diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 103df2db018c6..c07ae491c5dc0 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -79,6 +79,7 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => array('Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castThrowingCasterException'), 'Symfony\Component\VarDumper\Caster\TraceStub' => array('Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'), 'Symfony\Component\VarDumper\Caster\FrameStub' => array('Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'), + 'Symfony\Component\Debug\Exception\SilencedErrorContext' => array('Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'), 'PHPUnit_Framework_MockObject_MockObject' => array('Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'), 'Prophecy\Prophecy\ProphecySubjectInterface' => array('Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'), diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php index c118fbc3986e7..a05307bab0e1e 100644 --- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php @@ -623,6 +623,9 @@ function showCurrent(state) overflow: hidden; vertical-align: top; } +pre.sf-dump .sf-dump-ellipsis+.sf-dump-ellipsis { + max-width: none; +} pre.sf-dump code { display:inline; padding:0; @@ -788,9 +791,20 @@ protected function style($style, $value, $attr = array()) $map = static::$controlCharsMap; if (isset($attr['ellipsis'])) { + $class = 'sf-dump-ellipsis'; + if (isset($attr['ellipsis-type'])) { + $class = sprintf('"%s sf-dump-ellipsis-%s"', $class, $attr['ellipsis-type']); + } $label = esc(substr($value, -$attr['ellipsis'])); $style = str_replace(' title="', " title=\"$v\n", $style); - $v = sprintf('%s%s', substr($v, 0, -strlen($label)), $label); + $v = sprintf('%s', $class, substr($v, 0, -strlen($label))); + + if (!empty($attr['ellipsis-tail'])) { + $tail = strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']))); + $v .= sprintf('%s%s', substr($label, 0, $tail), substr($label, $tail)); + } else { + $v .= $label; + } } $v = "".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) { diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php index e4a72ed7cf271..94ad5fe0997ff 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php @@ -44,7 +44,7 @@ public function testDefaultSettings() #code: 0 #file: "%sExceptionCasterTest.php" #line: 27 - -trace: { + trace: { %sExceptionCasterTest.php:27: { : { : return new \Exception(''.$msg); @@ -102,7 +102,7 @@ public function testNoArgs() #code: 0 #file: "%sExceptionCasterTest.php" #line: 27 - -trace: { + trace: { %sExceptionCasterTest.php:27: { : { : return new \Exception(''.$msg); @@ -130,7 +130,7 @@ public function testNoSrcContext() #code: 0 #file: "%sExceptionCasterTest.php" #line: 27 - -trace: { + trace: { %sExceptionCasterTest.php: 27 %sExceptionCasterTest.php: %d %A @@ -156,11 +156,11 @@ public function testHtmlDump() #message: "1" #code: 0 #file: "%sTests%eCaster%eExceptionCasterTest.php" +%d characters">%s%eVarDumper%eTests%eCaster%eExceptionCasterTest.php" #line: 27 - -trace: { + trace: { %sVarDumper%eTests%eCaster%eExceptionCasterTest.php: 27 +Stack level %d.">%s%eVarDumper%eTests%eCaster%eExceptionCasterTest.php: 27 …%d } } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php index b1d1c85be6a13..aae7965fd6a69 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/StubCasterTest.php @@ -141,7 +141,7 @@ public function testClassStubWithNotExistingClass() $expectedDump = <<<'EODUMP' array:1 [ 0 => "Symfony\Component\VarDumper\Tests\Caster\NotExisting" +52 characters">Symfony\Component\VarDumper\Tests\Caster\NotExisting" ] EODUMP; diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php index a8ae1e6c7e1e5..6226012fabf15 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/CliDumperTest.php @@ -332,7 +332,7 @@ public function testThrowingCaster() stream resource {@{$ref} ⚠: Symfony\Component\VarDumper\Exception\ThrowingCasterException {{$r} #message: "Unexpected Exception thrown from a caster: Foobar" - -trace: { + trace: { %sTwig.php:2: { : foo bar : twig source diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php index 37a310e7ce65c..b852f27476d0a 100644 --- a/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/HtmlDumperTest.php @@ -77,7 +77,7 @@ public function testGet() } "closure" => Closure {{$r} class: "Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" +55 characters">Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" this: HtmlDumperTest {{$r} &%s;} parameters: { \$a: {} @@ -87,7 +87,7 @@ public function testGet() } } file: "%sTests%eFixtures%edumb-var.php" +%d characters">%s%eVarDumper%eTests%eFixtures%edumb-var.php" line: "{$var['line']} to {$var['line']}" } "line" => {$var['line']}