feature #21502 Persist app bootstrapping logs for logger datacollector (ScullWM, nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

Persist app bootstrapping logs for logger datacollector

| Q             | A
| ------------- | ---
| Branch?       | 3.3
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | ?
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #21405
| License       | MIT

Logs generated during the container build are catched by the BufferingLogger with a special flag.

They are persist by the LoggerDataCollector and are available in the logger profiler.
In the profiler toolbar, the "container build" logs increment the current logs counter (even if the container was previously built).

<img width="540" alt="capture d ecran 2017-02-01 a 20 56 40" src="https://cloud.githubusercontent.com/assets/1017746/22523826/0bc12e4a-e8c1-11e6-830f-7f6238ea7423.png">

<img width="1022" alt="capture d ecran 2017-02-01 a 20 57 55" src="https://cloud.githubusercontent.com/assets/1017746/22523859/2c48a698-e8c1-11e6-9bdb-d85f3e692938.png">

The BufferingLogger now require the cachePath and the filesystem to persist a (unique) container build logs.
If the current workflow is ok, I will update the test coverage (actually they fail). Maybe we can display the appDevDebugProjectContainerCompiler.log content in that logger profile.

Commits
-------

2fd18b5503 [VarDumper] Fine tune dumping log messages
ce3ef6a96e Persist app bootstrapping logs for logger datacollector
This commit is contained in:
Fabien Potencier 2017-04-20 11:12:40 -06:00
commit ad86e2dff0
18 changed files with 289 additions and 63 deletions

View File

@ -32,6 +32,7 @@
<tag name="data_collector" template="@WebProfiler/Collector/logger.html.twig" id="logger" priority="300" />
<tag name="monolog.logger" channel="profiler" />
<argument type="service" id="logger" on-invalid="ignore" />
<argument>%kernel.cache_dir%/%kernel.container_class%</argument>
</service>
<service id="data_collector.time" class="Symfony\Component\HttpKernel\DataCollector\TimeDataCollector" public="false">

View File

@ -124,6 +124,52 @@
</div>
</div>
{% set compilerLogTotal = 0 %}
{% for logs in collector.compilerLogs %}
{% set compilerLogTotal = compilerLogTotal + logs|length %}
{% endfor %}
<div class="tab">
<h3 class="tab-title">Container Compilation<span class="badge">{{ compilerLogTotal }}</span></h3>
<div class="tab-content">
{% if collector.compilerLogs is empty %}
<div class="empty">
<p>There are no compiler log messages.</p>
</div>
{% else %}
<table class="logs">
<thead>
<tr>
<th class="full-width">Class</th>
<th>Messages</th>
</tr>
</thead>
<tbody>
{% for class, logs in collector.compilerLogs %}
<tr class="">
<td class="font-normal">
{% set context_id = 'context-compiler-' ~ loop.index %}
<a class="btn btn-link sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="{{ class }}">{{ class }}</a>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul>
{% for log in logs %}
<li>{{ profiler_dump_log(log.message) }}</li>
{% endfor %}
</ul>
</div>
</td>
<td class="font-normal text-right">{{ logs|length }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
@ -165,16 +211,16 @@
{% endif %}
<td class="font-normal">{{ helper.render_log_message(category, loop.index, log, is_deprecation) }}</td>
<td class="font-normal">{{ helper.render_log_message(category, loop.index, log) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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 @@
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context.exception['\0Exception\0trace'], maxDepth=2) }}
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
</div>
</span>
{% elseif log.context is defined and log.context is not empty %}
@ -198,6 +244,6 @@
</div>
</span>
{% else %}
{{ log.message }}
{{ profiler_dump_log(log.message) }}
{% endif %}
{% endmacro %}

View File

@ -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;

View File

@ -89,18 +89,19 @@ class WebProfilerExtension extends \Twig_Extension_Profiler
return str_replace("\n</pre", '</pre', rtrim($dump));
}
public function dumpLog(\Twig_Environment $env, $message, Data $context)
public function dumpLog(\Twig_Environment $env, $message, Data $context = null)
{
$message = twig_escape_filter($env, $message);
$message = preg_replace('/&quot;(.*?)&quot;/', '&quot;<b>$1</b>&quot;', $message);
if (false === strpos($message, '{')) {
if (null === $context || false === strpos($message, '{')) {
return '<span class="dump-inline">'.$message.'</span>';
}
$replacements = array();
foreach ($context as $k => $v) {
$k = '{'.twig_escape_filter($env, $k).'}';
$replacements['&quot;'.$k.'&quot;'] = $replacements[$k] = $this->dumpData($env, $v);
$replacements['&quot;<b>'.$k.'</b>&quot;'] = $replacements['&quot;'.$k.'&quot;'] = $replacements[$k] = $this->dumpData($env, $v);
}
return '<span class="dump-inline">'.strtr($message, $replacements).'</span>';

View File

@ -162,7 +162,7 @@ class DebugClassLoader
$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 @@ class DebugClassLoader
}
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 @@ class DebugClassLoader
}
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 @@ class DebugClassLoader
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 @@ class DebugClassLoader
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 @@ class DebugClassLoader
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 @@ class DebugClassLoader
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)));
}
}

View File

@ -185,7 +185,7 @@ class DebugClassLoaderTest extends TestCase
$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 DebugClassLoaderTest extends TestCase
$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 DebugClassLoaderTest extends TestCase
$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 DebugClassLoaderTest extends TestCase
$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);

View File

@ -114,6 +114,10 @@ class Compiler
*/
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;
}

View File

@ -24,12 +24,15 @@ use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
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 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte
{
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 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte
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 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte
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 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte
}
$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'];

View File

@ -539,12 +539,32 @@ abstract class Kernel implements KernelInterface, TerminableInterface
$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());

View File

@ -30,6 +30,8 @@ class ClassStub extends ConstStub
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 @@ class ClassStub extends ConstStub
} 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);

View File

@ -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 @@ class ExceptionCaster
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 @@ class ExceptionCaster
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 @@ class ExceptionCaster
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 @@ class ExceptionCaster
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 @@ class ExceptionCaster
$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 @@ class ExceptionCaster
}
}
}
$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 @@ class ExceptionCaster
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']);

View File

@ -18,6 +18,9 @@ namespace Symfony\Component\VarDumper\Caster;
*/
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 @@ class LinkStub extends ConstStub
if (null === $href) {
$href = $label;
}
if (is_string($href)) {
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 (!is_string($href)) {
return;
}
if (0 === strpos($href, 'file://')) {
if ($href === $label) {
$label = substr($label, 7);
}
if (file_exists($href)) {
if ($line) {
$this->attr['line'] = $line;
}
$this->attr['file'] = realpath($href) ?: $href;
$href = substr($href, 7);
} elseif (false !== strpos($href, '://')) {
$this->attr['href'] = $href;
if ($this->attr['file'] === $label && 3 < count($ellipsis = explode(DIRECTORY_SEPARATOR, $href))) {
$this->attr['ellipsis'] = 2 + strlen(implode(array_slice($ellipsis, -2)));
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;
}
}
}
}
$inVendor = false;
if (isset(self::$composerRoots[$dir = dirname($file)])) {
return self::$composerRoots[$dir];
}
foreach (self::$vendorRoots as $root) {
if ($inVendor = 0 === strpos($file, $root)) {
return $root;
}
}
$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;
}
}

View File

@ -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'),

View File

@ -623,6 +623,9 @@ pre.sf-dump .sf-dump-ellipsis {
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 @@ EOHTML
$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('<span class=sf-dump-ellipsis>%s</span>%s', substr($v, 0, -strlen($label)), $label);
$v = sprintf('<span class=%s>%s</span>', $class, substr($v, 0, -strlen($label)));
if (!empty($attr['ellipsis-tail'])) {
$tail = strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail'])));
$v .= sprintf('<span class=sf-dump-ellipsis>%s</span>%s', substr($label, 0, $tail), substr($label, $tail));
} else {
$v .= $label;
}
}
$v = "<span class=sf-dump-{$style}>".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) {

View File

@ -44,7 +44,7 @@ Exception {
#code: 0
#file: "%sExceptionCasterTest.php"
#line: 27
-trace: {
trace: {
%sExceptionCasterTest.php:27: {
: {
: return new \Exception(''.$msg);
@ -102,7 +102,7 @@ Exception {
#code: 0
#file: "%sExceptionCasterTest.php"
#line: 27
-trace: {
trace: {
%sExceptionCasterTest.php:27: {
: {
: return new \Exception(''.$msg);
@ -130,7 +130,7 @@ Exception {
#code: 0
#file: "%sExceptionCasterTest.php"
#line: 27
-trace: {
trace: {
%sExceptionCasterTest.php: 27
%sExceptionCasterTest.php: %d
%A
@ -156,11 +156,11 @@ EODUMP;
#<span class=sf-dump-protected title="Protected property">message</span>: "<span class=sf-dump-str>1</span>"
#<span class=sf-dump-protected title="Protected property">code</span>: <span class=sf-dump-num>0</span>
#<span class=sf-dump-protected title="Protected property">file</span>: "<span class=sf-dump-str title="%sExceptionCasterTest.php
%d characters"><span class=sf-dump-ellipsis>%sTests</span>%eCaster%eExceptionCasterTest.php</span>"
%d characters"><span class="sf-dump-ellipsis sf-dump-ellipsis-path">%s%eVarDumper</span><span class=sf-dump-ellipsis>%e</span>Tests%eCaster%eExceptionCasterTest.php</span>"
#<span class=sf-dump-protected title="Protected property">line</span>: <span class=sf-dump-num>27</span>
-<span class=sf-dump-private title="Private property defined in class:&#10;`Exception`">trace</span>: {<samp>
<span class=sf-dump-meta>trace</span>: {<samp>
<span class=sf-dump-meta title="%sExceptionCasterTest.php
Stack level %d."><span class=sf-dump-ellipsis>%sVarDumper%eTests</span>%eCaster%eExceptionCasterTest.php</span>: <span class=sf-dump-num>27</span>
Stack level %d."><span class="sf-dump-ellipsis sf-dump-ellipsis-path">%s%eVarDumper</span><span class=sf-dump-ellipsis>%e</span>Tests%eCaster%eExceptionCasterTest.php</span>: <span class=sf-dump-num>27</span>
&hellip;%d
</samp>}
</samp>}

View File

@ -141,7 +141,7 @@ EODUMP;
$expectedDump = <<<'EODUMP'
<foo></foo><bar><span class=sf-dump-note>array:1</span> [<samp>
<span class=sf-dump-index>0</span> => "<span class=sf-dump-str title="Symfony\Component\VarDumper\Tests\Caster\NotExisting
52 characters"><span class=sf-dump-ellipsis>Symfony\Component\VarDumper\Tests\Caster</span>\NotExisting</span>"
52 characters"><span class="sf-dump-ellipsis sf-dump-ellipsis-class">Symfony\Component\VarDumper\Tests\Caster</span><span class=sf-dump-ellipsis>\</span>NotExisting</span>"
</samp>]
</bar>
EODUMP;

View File

@ -332,7 +332,7 @@ EOTXT
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

View File

@ -77,7 +77,7 @@ class HtmlDumperTest extends TestCase
</samp>}
"<span class=sf-dump-key>closure</span>" => <span class=sf-dump-note>Closure</span> {{$r}<samp>
<span class=sf-dump-meta>class</span>: "<span class=sf-dump-str title="Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest
55 characters"><span class=sf-dump-ellipsis>Symfony\Component\VarDumper\Tests\Dumper</span>\HtmlDumperTest</span>"
55 characters"><span class="sf-dump-ellipsis sf-dump-ellipsis-class">Symfony\Component\VarDumper\Tests\Dumper</span><span class=sf-dump-ellipsis>\</span>HtmlDumperTest</span>"
<span class=sf-dump-meta>this</span>: <abbr title="Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" class=sf-dump-note>HtmlDumperTest</abbr> {{$r} &%s;}
<span class=sf-dump-meta>parameters</span>: {<samp>
<span class=sf-dump-meta>\$a</span>: {}
@ -87,7 +87,7 @@ class HtmlDumperTest extends TestCase
</samp>}
</samp>}
<span class=sf-dump-meta>file</span>: "<span class=sf-dump-str title="{$var['file']}
%d characters"><span class=sf-dump-ellipsis>%sTests</span>%eFixtures%edumb-var.php</span>"
%d characters"><span class="sf-dump-ellipsis sf-dump-ellipsis-path">%s%eVarDumper</span><span class=sf-dump-ellipsis>%e</span>Tests%eFixtures%edumb-var.php</span>"
<span class=sf-dump-meta>line</span>: "<span class=sf-dump-str title="%d characters">{$var['line']} to {$var['line']}</span>"
</samp>}
"<span class=sf-dump-key>line</span>" => <span class=sf-dump-num>{$var['line']}</span>