[TwigBridge] add Twig dump() function + tests and fixes

This commit is contained in:
Nicolas Grekas 2014-09-17 10:28:09 +02:00
parent 0f8d30fd89
commit 2e167ba351
19 changed files with 373 additions and 137 deletions

View File

@ -85,10 +85,7 @@
"src/Symfony/Component/HttpFoundation/Resources/stubs",
"src/Symfony/Component/Intl/Resources/stubs"
],
"files": [
"src/Symfony/Component/Intl/Resources/stubs/functions.php",
"src/Symfony/Component/VarDumper/Resources/functions/dump.php"
]
"files": [ "src/Symfony/Component/Intl/Resources/stubs/functions.php" ]
},
"minimum-stability": "dev",
"extra": {

View File

@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\DumpTokenParser;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
/**
* Provides integration of the dump() function with Twig.
@ -20,6 +22,18 @@ use Symfony\Bridge\Twig\TokenParser\DumpTokenParser;
*/
class DumpExtension extends \Twig_Extension
{
public function __construct(ClonerInterface $cloner = null)
{
$this->cloner = $cloner;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('dump', array($this, 'dump'), array('is_safe' => array('html'), 'needs_context' => true, 'needs_environment' => true)),
);
}
public function getTokenParsers()
{
return array(new DumpTokenParser());
@ -29,4 +43,38 @@ class DumpExtension extends \Twig_Extension
{
return 'dump';
}
public function dump(\Twig_Environment $env, $context)
{
if (!$env->isDebug() || !$this->cloner) {
return;
}
if (2 === func_num_args()) {
$vars = array();
foreach ($context as $key => $value) {
if (!$value instanceof \Twig_Template) {
$vars[$key] = $value;
}
}
$vars = array($vars);
} else {
$vars = func_get_args();
unset($vars[0], $vars[1]);
}
$html = '';
$dumper = new HtmlDumper(function ($line, $depth) use (&$html) {
if (-1 !== $depth) {
$html .= str_repeat(' ', $depth).$line."\n";
}
});
foreach ($vars as $value) {
$dumper->dump($this->cloner->cloneVar($value));
}
return $html;
}
}

View File

@ -13,20 +13,22 @@ namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\DumpExtension;
use Symfony\Component\VarDumper\VarDumper;
use Symfony\Component\VarDumper\Cloner\PhpCloner;
class DumpExtensionTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getDumpParams
* @dataProvider getDumpTags
*/
public function testDebugDump($template, $debug, $expectedOutput, $expectedDumped)
public function testDumpTag($template, $debug, $expectedOutput, $expectedDumped)
{
$extension = new DumpExtension(new PhpCloner());
$twig = new \Twig_Environment(new \Twig_Loader_String(), array(
'debug' => $debug,
'cache' => false,
'optimizations' => 0,
));
$twig->addExtension(new DumpExtension());
$twig->addExtension($extension);
$dumped = null;
$exception = null;
@ -46,7 +48,7 @@ class DumpExtensionTest extends \PHPUnit_Framework_TestCase
$this->assertSame($expectedDumped, $dumped);
}
public function getDumpParams()
public function getDumpTags()
{
return array(
array('A{% dump %}B', true, 'AB', array()),
@ -54,4 +56,50 @@ class DumpExtensionTest extends \PHPUnit_Framework_TestCase
array('A{% dump %}B', false, 'AB', null),
);
}
/**
* @dataProvider getDumpArgs
*/
public function testDump($context, $args, $expectedOutput, $debug = true)
{
$extension = new DumpExtension(new PhpCloner());
$twig = new \Twig_Environment(new \Twig_Loader_String(), array(
'debug' => $debug,
'cache' => false,
'optimizations' => 0,
));
array_unshift($args, $context);
array_unshift($args, $twig);
$dump = call_user_func_array(array($extension, 'dump'), $args);
if ($debug) {
$this->assertStringStartsWith('<script>', $dump);
$dump = preg_replace('/^.*?<pre/', '<pre', $dump);
}
$this->assertEquals($expectedOutput, $dump);
}
public function getDumpArgs()
{
return array(
array(array(), array(), '', false),
array(array(), array(), "<pre id=sf-dump><span class=sf-dump-0>[]\n</span></pre><script>Sfjs.dump.instrument()</script>\n"),
array(
array(),
array(123, 456),
"<pre id=sf-dump><span class=sf-dump-0><span class=sf-dump-num>123</span>\n</span></pre><script>Sfjs.dump.instrument()</script>\n"
."<pre id=sf-dump><span class=sf-dump-0><span class=sf-dump-num>456</span>\n</span></pre><script>Sfjs.dump.instrument()</script>\n",
),
array(
array('foo' => 'bar'),
array(),
"<pre id=sf-dump><span class=sf-dump-0><span class=sf-dump-note>array:1</span> [<span name=sf-dump-child>\n"
." <span class=sf-dump-1>\"<span class=sf-dump-meta>foo</span>\" => \"<span class=sf-dump-str>bar</span>\"\n"
."</span></span>]\n"
."</span></pre><script>Sfjs.dump.instrument()</script>\n",
),
);
}
}

View File

@ -17,12 +17,9 @@
<argument type="service" id="debug.stopwatch" />
</service>
<service id="twig.extension.debug" class="Twig_Extension_Debug" public="false">
<tag name="twig.extension" />
</service>
<service id="twig.extension.dump" class="%twig.extension.dump.class%" public="false">
<tag name="twig.extension" />
<argument type="service" id="var_dumper.cloner" on-invalid="null" />
</service>
</services>
</container>

View File

@ -169,7 +169,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
$dumper->dump(
$dump['data']->getLimitedClone($maxDepthLimit, $maxItemsPerDepth),
function ($line, $depth) use (&$data) {
if (false !==$depth) {
if (-1 !== $depth) {
$data .= str_repeat(' ', $depth).$line."\n";
}
}

View File

@ -34,14 +34,17 @@ class DumpDataCollectorTest extends \PHPUnit_Framework_TestCase
$xDump = array(
array(
'data' => "<!DOCTYPE html><style> pre.sf-dump { background-color: #300a24; white-space: pre; line-height: 1.2em; color: #eee8d5; font-family: monospace, sans-serif; padding: 5px; } .sf-dump span { display: inline; }a.sf-dump-ref {color:#444444}span.sf-dump-num {font-weight:bold;color:#0087FF}span.sf-dump-const {font-weight:bold;color:#0087FF}span.sf-dump-str {font-weight:bold;color:#00D7FF}span.sf-dump-cchr {font-style: italic}span.sf-dump-note {color:#D7AF00}span.sf-dump-ref {color:#444444}span.sf-dump-public {color:#008700}span.sf-dump-protected {color:#D75F00}span.sf-dump-private {color:#D70000}span.sf-dump-meta {color:#005FFF}</style><pre class=sf-dump><span class=sf-dump-0><span class=sf-dump-num>123</span>\n</pre>\n",
'data' => "<pre id=sf-dump><span class=sf-dump-0><span class=sf-dump-num>123</span>\n</span></pre><script>Sfjs.dump.instrument()</script>\n",
'name' => 'DumpDataCollectorTest.php',
'file' => __FILE__,
'line' => $line,
'fileExcerpt' => false,
),
);
$this->assertSame($xDump, $collector->getDumps('html'));
$dump = $collector->getDumps('html');
$this->assertTrue(isset($dump[0]['data']));
$dump[0]['data'] = preg_replace('/^.*?<pre/', '<pre', $dump[0]['data']);
$this->assertSame($xDump, $dump);
$this->assertStringStartsWith(
'a:1:{i:0;a:5:{s:4:"data";O:39:"Symfony\Component\VarDumper\Cloner\Data":3:{s:45:"Symfony\Component\VarDumper\Cloner\Datadata";a:1:{i:0;a:1:{i:0;i:123;}}s:49:"Symfony\Component\VarDumper\Cloner\DatamaxDepth";i:-1;s:57:"Symfony\Component\VarDumper\Cloner\DatamaxItemsPerDepth";i:-1;}s:4:"name";s:25:"DumpDataCollectorTest.php";s:4:"file";s:',

View File

@ -29,8 +29,9 @@ class ReflectionCaster
public static function castClosure(\Closure $c, array $a, Stub $stub, $isNested)
{
$stub->class = 'Closure'; // HHVM generates unique class names for closures
$a = static::castReflector(new \ReflectionFunction($c), $a, $stub, $isNested);
unset($a["\0+\0000"], $a['name']);
unset($a["\0+\0000"], $a['name'], $a["\0+\0this"], $a["\0+\0parameter"]);
return $a;
}

View File

@ -28,7 +28,7 @@ class ResourceCaster
public static function castDba($dba, array $a, Stub $stub, $isNested)
{
$list = dba_list();
$a['file'] = $list[substr((string) $dba, 13)];
$a['file'] = $list[(int) $dba];
return $a;
}

View File

@ -21,64 +21,64 @@ use Symfony\Component\VarDumper\Exception\ThrowingCasterException;
abstract class AbstractCloner implements ClonerInterface
{
public static $defaultCasters = array(
'o:Symfony\Component\VarDumper\Caster\CasterStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub',
'Symfony\Component\VarDumper\Caster\CasterStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub',
'o:Closure' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClosure',
'o:Reflector' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castReflector',
'Closure' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClosure',
'Reflector' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castReflector',
'o:Doctrine\Common\Persistence\ObjectManager' => 'Symfony\Component\VarDumper\Caster\StubCaster::castNestedFat',
'o:Doctrine\Common\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castCommonProxy',
'o:Doctrine\ORM\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castOrmProxy',
'o:Doctrine\ORM\PersistentCollection' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castPersistentCollection',
'Doctrine\Common\Persistence\ObjectManager' => 'Symfony\Component\VarDumper\Caster\StubCaster::castNestedFat',
'Doctrine\Common\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castCommonProxy',
'Doctrine\ORM\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castOrmProxy',
'Doctrine\ORM\PersistentCollection' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castPersistentCollection',
'o:DOMException' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castException',
'o:DOMStringList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'o:DOMNameList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'o:DOMImplementation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castImplementation',
'o:DOMImplementationList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'o:DOMNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNode',
'o:DOMNameSpaceNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNameSpaceNode',
'o:DOMDocument' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocument',
'o:DOMNodeList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'o:DOMNamedNodeMap' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'o:DOMCharacterData' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castCharacterData',
'o:DOMAttr' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castAttr',
'o:DOMElement' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castElement',
'o:DOMText' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castText',
'o:DOMTypeinfo' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castTypeinfo',
'o:DOMDomError' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDomError',
'o:DOMLocator' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLocator',
'o:DOMDocumentType' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocumentType',
'o:DOMNotation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNotation',
'o:DOMEntity' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castEntity',
'o:DOMProcessingInstruction' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castProcessingInstruction',
'o:DOMXPath' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castXPath',
'DOMException' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castException',
'DOMStringList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'DOMNameList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'DOMImplementation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castImplementation',
'DOMImplementationList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'DOMNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNode',
'DOMNameSpaceNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNameSpaceNode',
'DOMDocument' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocument',
'DOMNodeList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'DOMNamedNodeMap' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength',
'DOMCharacterData' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castCharacterData',
'DOMAttr' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castAttr',
'DOMElement' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castElement',
'DOMText' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castText',
'DOMTypeinfo' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castTypeinfo',
'DOMDomError' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDomError',
'DOMLocator' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLocator',
'DOMDocumentType' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocumentType',
'DOMNotation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNotation',
'DOMEntity' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castEntity',
'DOMProcessingInstruction' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castProcessingInstruction',
'DOMXPath' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castXPath',
'o:ErrorException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castErrorException',
'o:Exception' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castException',
'o:Symfony\Component\DependencyInjection\ContainerInterface'
'ErrorException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castErrorException',
'Exception' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castException',
'Symfony\Component\DependencyInjection\ContainerInterface'
=> 'Symfony\Component\VarDumper\Caster\StubCaster::castNestedFat',
'o:Symfony\Component\VarDumper\Exception\ThrowingCasterException'
'Symfony\Component\VarDumper\Exception\ThrowingCasterException'
=> 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castThrowingCasterException',
'o:PDO' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdo',
'o:PDOStatement' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdoStatement',
'PDO' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdo',
'PDOStatement' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdoStatement',
'o:ArrayObject' => 'Symfony\Component\VarDumper\Caster\SplCaster::castArrayObject',
'o:SplDoublyLinkedList' => 'Symfony\Component\VarDumper\Caster\SplCaster::castDoublyLinkedList',
'o:SplFixedArray' => 'Symfony\Component\VarDumper\Caster\SplCaster::castFixedArray',
'o:SplHeap' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap',
'o:SplObjectStorage' => 'Symfony\Component\VarDumper\Caster\SplCaster::castObjectStorage',
'o:SplPriorityQueue' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap',
'ArrayObject' => 'Symfony\Component\VarDumper\Caster\SplCaster::castArrayObject',
'SplDoublyLinkedList' => 'Symfony\Component\VarDumper\Caster\SplCaster::castDoublyLinkedList',
'SplFixedArray' => 'Symfony\Component\VarDumper\Caster\SplCaster::castFixedArray',
'SplHeap' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap',
'SplObjectStorage' => 'Symfony\Component\VarDumper\Caster\SplCaster::castObjectStorage',
'SplPriorityQueue' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap',
'r:curl' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castCurl',
'r:dba' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba',
'r:dba persistent' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba',
'r:gd' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castGd',
'r:mysql link' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castMysqlLink',
'r:process' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castProcess',
'r:stream' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStream',
'r:stream-context' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStreamContext',
':curl' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castCurl',
':dba' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba',
':dba persistent' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba',
':gd' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castGd',
':mysql link' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castMysqlLink',
':process' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castProcess',
':stream' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStream',
':stream-context' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStreamContext',
);
protected $maxItems = 250;
@ -107,8 +107,7 @@ abstract class AbstractCloner implements ClonerInterface
*
* Maps resources or objects types to a callback.
* Types are in the key, with a callable caster for value.
* Objects class are to be prefixed with a `o:`,
* resources type are to be prefixed with a `r:`,
* Resource types are to be prefixed with a `:`,
* see e.g. static::$defaultCasters.
*
* @param callable[] $casters A map of casters.
@ -193,7 +192,7 @@ abstract class AbstractCloner implements ClonerInterface
$class,
method_exists($class, '__debugInfo'),
new \ReflectionClass($class),
array_reverse(array($class => $class) + class_parents($class) + class_implements($class) + array('*' => '*')),
array_reverse(array($class => $class) + class_parents($class) + class_implements($class)),
);
$this->classInfo[$class] = $classInfo;
@ -213,7 +212,7 @@ abstract class AbstractCloner implements ClonerInterface
}
foreach ($classInfo[3] as $p) {
if (!empty($this->casters[$p = 'o:'.strtolower($p)])) {
if (!empty($this->casters[$p = strtolower($p)])) {
foreach ($this->casters[$p] as $p) {
$a = $this->callCaster($p, $obj, $a, $stub, $isNested);
}
@ -237,8 +236,8 @@ abstract class AbstractCloner implements ClonerInterface
$a = array();
$type = $stub->class;
if (!empty($this->casters['r:'.$type])) {
foreach ($this->casters['r:'.$type] as $c) {
if (!empty($this->casters[':'.$type])) {
foreach ($this->casters[':'.$type] as $c) {
$a = $this->callCaster($c, $res, $a, $stub, $isNested);
}
}

View File

@ -9,9 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\VarDumper\Dumper;
use Symfony\Component\VarDumper\Cloner\Stub;
namespace Symfony\Component\VarDumper\Cloner;
/**
* Represents the current state of a dumper while dumping.

View File

@ -11,9 +11,6 @@
namespace Symfony\Component\VarDumper\Cloner;
use Symfony\Component\VarDumper\Dumper\DumperInternalsInterface;
use Symfony\Component\VarDumper\Dumper\Cursor;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
@ -57,21 +54,21 @@ class Data
}
/**
* Dumps data with a DumperInternalsInterface dumper.
* Dumps data with a DumperInterface dumper.
*/
public function dump(DumperInternalsInterface $dumper)
public function dump(DumperInterface $dumper)
{
$refs = array(0);
$this->dumpItem($dumper, new Cursor, $refs, $this->data[0][0]);
}
/**
* Breadth-first dumping of items.
* Depth-first dumping of items.
*
* @param DumperInternalsInterface $dumper The dumper being used for dumping.
* @param Cursor $cursor A cursor used for tracking dumper state position.
* @param array &$refs A map of all references discovered while dumping.
* @param mixed $item A Stub object or the original value being dumped.
* @param DumperInterface $dumper The dumper being used for dumping.
* @param Cursor $cursor A cursor used for tracking dumper state position.
* @param array &$refs A map of all references discovered while dumping.
* @param mixed $item A Stub object or the original value being dumped.
*/
private function dumpItem($dumper, $cursor, &$refs, $item)
{
@ -157,12 +154,12 @@ class Data
/**
* Dumps children of hash structures.
*
* @param DumperInternalsInterface $dumper
* @param Cursor $parentCursor The cursor of the parent hash.
* @param array &$refs A map of all references discovered while dumping.
* @param array $children The children to dump.
* @param int $hashCut The number of items removed from the original hash.
* @param string $hashType A Cursor::HASH_* const.
* @param DumperInterface $dumper
* @param Cursor $parentCursor The cursor of the parent hash.
* @param array &$refs A map of all references discovered while dumping.
* @param array $children The children to dump.
* @param int $hashCut The number of items removed from the original hash.
* @param string $hashType A Cursor::HASH_* const.
*
* @return int The final number of removed items.
*/

View File

@ -9,14 +9,14 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\VarDumper\Dumper;
namespace Symfony\Component\VarDumper\Cloner;
/**
* DumperInterface used by Data objects.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface DumperInternalsInterface
interface DumperInterface
{
/**
* Dumps a scalar value.

View File

@ -12,13 +12,14 @@
namespace Symfony\Component\VarDumper\Dumper;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\DumperInterface;
/**
* Abstract mechanism for dumping a Data object.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractDumper implements DataDumperInterface, DumperInternalsInterface
abstract class AbstractDumper implements DataDumperInterface, DumperInterface
{
public static $defaultOutputStream = 'php://output';
@ -87,14 +88,22 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInternalsInt
*/
public function dump(Data $data, $lineDumper = null)
{
$this->decimalPoint = (string) 0.5;
$this->decimalPoint = $this->decimalPoint[1];
$dumper = clone $this;
$exception = null;
if ($lineDumper) {
$dumper->setLineDumper($lineDumper);
$prevLineDumper = $this->setLineDumper($lineDumper);
}
try {
$data->dump($this);
$this->dumpLine(-1);
} catch (\Exception $exception) {
// Re-thrown below
}
if ($lineDumper) {
$this->setLineDumper($prevLineDumper);
}
if (null !== $exception) {
throw $exception;
}
$data->dump($dumper);
$dumper->dumpLine(false);
}
/**
@ -116,7 +125,7 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInternalsInt
*/
protected function echoLine($line, $depth)
{
if (false !== $depth) {
if (-1 !== $depth) {
fwrite($this->outputStream, str_repeat($this->indentPad, $depth).$line."\n");
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\VarDumper\Dumper;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Cursor;
/**
* CliDumper dumps variables for command line output.

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\VarDumper\Dumper;
use Symfony\Component\VarDumper\Cloner\Cursor;
/**
* HtmlDumper dumps variables as HTML.
*
@ -21,8 +23,8 @@ class HtmlDumper extends CliDumper
public static $defaultOutputStream = 'php://output';
protected $dumpHeader;
protected $dumpPrefix = '<pre class=sf-dump>';
protected $dumpSuffix = '</pre>';
protected $dumpPrefix = '<pre id=sf-dump>';
protected $dumpSuffix = '</pre><script>Sfjs.dump.instrument()</script>';
protected $colors = true;
protected $headerIsDumped = false;
protected $lastDepth = -1;
@ -74,7 +76,7 @@ class HtmlDumper extends CliDumper
* @param string $prefix The prepended HTML string.
* @param string $suffix The appended HTML string.
*/
public function setDumpBoudaries($prefix, $suffix)
public function setDumpBoundaries($prefix, $suffix)
{
$this->dumpPrefix = $prefix;
$this->dumpSuffix = $suffix;
@ -87,28 +89,104 @@ class HtmlDumper extends CliDumper
{
$this->headerIsDumped = true;
$p = 'sf-dump';
$line = <<<EOHTML
<!DOCTYPE html><style>
pre.sf-dump {
if (null !== $this->dumpHeader) {
return $this->dumpHeader;
}
$line = <<<'EOHTML'
<script>
Sfjs = window.Sfjs || {};
Sfjs.dump = Sfjs.dump || {};
Sfjs.dump.childElts = document.getElementsByName('sf-dump-child');
Sfjs.dump.childLen = 0;
Sfjs.dump.instrument = function () {
var elt,
i = this.childLen,
aCompact = '▶</a><span class="sf-dump-compact">',
aExpanded = '▼</a><span class="sf-dump-expanded">';
this.childLen= this.childElts.length;
while (i < this.childLen) {
elt = this.childElts[i];
if ("" == elt.className) {
elt.className = "sf-dump-child";
elt.innerHTML = '<a class=sf-dump-ref onclick="Sfjs.dump.toggle(this)">'+('sf-dump-0' == elt.parentNode.className ? aExpanded : aCompact)+elt.innerHTML+'</span>';
}
++i;
}
};
Sfjs.dump.toggle = function(a) {
var s = a.nextElementSibling;
if ('sf-dump-compact' == s.className) {
a.innerHTML = '▼';
s.className = 'sf-dump-expanded';
} else {
a.innerHTML = '▶';
s.className = 'sf-dump-compact';
}
};
</script>
<style>
#sf-dump {
display: block;
background-color: #300a24;
white-space: pre;
line-height: 1.2em;
color: #eee8d5;
font-family: monospace, sans-serif;
font: 12px monospace, sans-serif;
padding: 5px;
}
.sf-dump span {
#sf-dump span {
display: inline;
}
#sf-dump .sf-dump-compact {
display: none;
}
#sf-dump abbr {
text-decoration: none;
border: none;
cursor: help;
}
#sf-dump a {
text-decoration: none;
cursor: pointer;
}
#sf-dump a:hover {
text-decoration: underline;
}
EOHTML;
$line .= "a.$p-ref {{$this->styles['ref']}}";
foreach ($this->styles as $class => $style) {
$line .= "span.$p-$class {{$style}}";
$line .= "#sf-dump .sf-dump-$class {{$style}}";
}
return preg_replace('/\s+/', ' ', $line).'</style>'.$this->dumpHeader;
return $this->dumpHeader = preg_replace('/\s+/', ' ', $line).'</style>'.$this->dumpHeader;
}
/**
* {@inheritdoc}
*/
protected function enterHash(Cursor $cursor, $prefix, $hasChild)
{
if ($hasChild) {
$prefix .= '<span name=sf-dump-child>';
}
return parent::enterHash($cursor, $prefix, $hasChild);
}
/**
* {@inheritdoc}
*/
protected function leaveHash(Cursor $cursor, $suffix, $hasChild, $cut)
{
if ($hasChild) {
$suffix = '</span>'.$suffix;
}
return parent::leaveHash($cursor, $suffix, $hasChild, $cut);
}
/**
@ -138,6 +216,10 @@ EOHTML;
$val = str_replace($c, "<span class=sf-dump-cchr>$r</span>", $val);
}
}
} elseif ('note' === $style) {
if (false !== $c = strrpos($val, '\\')) {
$val = sprintf('<abbr title="%s" class=sf-dump-%s>%s</abbr>', $val, $style, substr($val, $c+1));
}
}
return "<span class=sf-dump-$style>$val</span>";
@ -148,7 +230,6 @@ EOHTML;
*/
protected function dumpLine($depth)
{
switch ($this->lastDepth - $depth) {
case +1: $this->line = '</span>'.$this->line; break;
case -1: $this->line = "<span class=sf-dump-$depth>$this->line"; break;
@ -161,13 +242,11 @@ EOHTML;
$this->line = $this->getDumpHeader().$this->line;
}
if (false === $depth) {
$this->lastDepth = -1;
if (-1 === $depth) {
$this->line .= $this->dumpSuffix;
parent::dumpLine(0);
} else {
$this->lastDepth = $depth;
}
$this->lastDepth = $depth;
parent::dumpLine($depth);
}

View File

@ -1,10 +1,10 @@
Symfony mechanim for exploring and dumping PHP variables
========================================================
Symfony mechanism for exploring and dumping PHP variables
=========================================================
This component provides a mechanism that allows exploring then dumping
any PHP variable.
It handles scalar, objects and resources properly, taking hard and soft
It handles scalars, objects and resources properly, taking hard and soft
references into account. More than being immune to inifinite recursion
problems, it allows dumping where references link to each other.
It explores recursive structures using a breadth-first algorithm.

View File

@ -26,6 +26,13 @@ class CliDumperTest extends \PHPUnit_Framework_TestCase
$dumper = new CliDumper('php://output');
$dumper->setColors(false);
$cloner = new PhpCloner();
$cloner->addCasters(array(
':stream' => function ($res, $a) {
unset($a['uri']);
return $a;
}
));
$data = $cloner->cloneVar($var);
ob_start();
@ -51,7 +58,7 @@ array:25 [
"[]" => []
"res" => resource:stream {
wrapper_type: "plainfile"
stream_type: "dir"
stream_type: "STDIO"
mode: "r"
unread_bytes: 0
seekable: true

View File

@ -0,0 +1,43 @@
<?php
namespace Symfony\Component\VarDumper\Tests\Fixture;
if (!class_exists('Symfony\Component\VarDumper\Tests\Fixture\DumbFoo')) {
class DumbFoo
{
public $foo = 'foo';
}
}
$foo = new DumbFoo();
$foo->bar = 'bar';
$g = fopen(__FILE__, 'r');
$h = fopen(__FILE__, 'r');
fclose($h);
$var = array(
'number' => 1, null,
'const' => 1.1, true, false, NAN, INF, -INF, PHP_INT_MAX,
'str' => "déjà", "\xE9",
'[]' => array(),
'res' => $g,
$h,
'obj' => $foo,
'closure' => function ($a, \PDO &$b = null) {},
'line' => __LINE__ - 1,
'nobj' => array((object) array()),
);
$r = array();
$r[] =& $r;
$var['recurs'] =& $r;
$var[] =& $var[0];
$var['sobj'] = $var['obj'];
$var['snobj'] =& $var['nobj'][0];
$var['snobj2'] = $var['nobj'][0];
$var['file'] = __FILE__;
$var["bin-key-\xE9"] = "";
unset($g, $h, $r);

View File

@ -25,7 +25,16 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase
$dumper = new HtmlDumper('php://output');
$dumper->setColors(false);
$dumper->setDumpHeader('<foo></foo>');
$dumper->setDumpBoundaries('<bar>', '</bar>');
$cloner = new PhpCloner();
$cloner->addCasters(array(
':stream' => function ($res, $a) {
unset($a['uri']);
return $a;
}
));
$data = $cloner->cloneVar($var);
ob_start();
@ -37,7 +46,7 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase
$this->assertSame(
<<<EOTXT
<!DOCTYPE html><style> pre.sf-dump { background-color: #300a24; white-space: pre; line-height: 1.2em; color: #eee8d5; font-family: monospace, sans-serif; padding: 5px; } .sf-dump span { display: inline; }a.sf-dump-ref {color:#444444}span.sf-dump-num {font-weight:bold;color:#0087FF}span.sf-dump-const {font-weight:bold;color:#0087FF}span.sf-dump-str {font-weight:bold;color:#00D7FF}span.sf-dump-cchr {font-style: italic}span.sf-dump-note {color:#D7AF00}span.sf-dump-ref {color:#444444}span.sf-dump-public {color:#008700}span.sf-dump-protected {color:#D75F00}span.sf-dump-private {color:#D70000}span.sf-dump-meta {color:#005FFF}</style><pre class=sf-dump><span class=sf-dump-0><span class=sf-dump-note>array:25</span> [
<foo></foo><bar><span class=sf-dump-0><span class=sf-dump-note>array:25</span> [<span name=sf-dump-child>
<span class=sf-dump-1>"<span class=sf-dump-meta>number</span>" => <span class=sf-dump-num>1</span>
<span class=sf-dump-meta>0</span> => <span class=sf-dump-const>null</span> <a class=sf-dump-ref name="sf-dump-ref1">#1</a>
"<span class=sf-dump-meta>const</span>" => <span class=sf-dump-num>1.1</span>
@ -50,9 +59,9 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase
"<span class=sf-dump-meta>str</span>" => "<span class=sf-dump-str>déjà</span>"
<span class=sf-dump-meta>7</span> => b"<span class=sf-dump-str>é</span>"
"<span class=sf-dump-meta>[]</span>" => []
"<span class=sf-dump-meta>res</span>" => resource:<span class=sf-dump-note>stream</span> {
"<span class=sf-dump-meta>res</span>" => resource:<span class=sf-dump-note>stream</span> {<span name=sf-dump-child>
<span class=sf-dump-2><span class=sf-dump-meta>wrapper_type</span>: "<span class=sf-dump-str>plainfile</span>"
<span class=sf-dump-meta>stream_type</span>: "<span class=sf-dump-str>dir</span>"
<span class=sf-dump-meta>stream_type</span>: "<span class=sf-dump-str>STDIO</span>"
<span class=sf-dump-meta>mode</span>: "<span class=sf-dump-str>r</span>"
<span class=sf-dump-meta>unread_bytes</span>: <span class=sf-dump-num>0</span>
<span class=sf-dump-meta>seekable</span>: <span class=sf-dump-const>true</span>
@ -60,13 +69,13 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase
<span class=sf-dump-meta>blocked</span>: <span class=sf-dump-const>true</span>
<span class=sf-dump-meta>eof</span>: <span class=sf-dump-const>false</span>
<span class=sf-dump-meta>options</span>: []
</span>}
</span></span>}
<span class=sf-dump-meta>8</span> => resource:<span class=sf-dump-note>Unknown</span> {}
"<span class=sf-dump-meta>obj</span>" => <span class=sf-dump-note>Symfony\Component\VarDumper\Tests\Fixture\DumbFoo</span> { <a class=sf-dump-ref name="sf-dump-ref2">#2</a>
"<span class=sf-dump-meta>obj</span>" => <span class=sf-dump-note><abbr title="Symfony\Component\VarDumper\Tests\Fixture\DumbFoo" class=sf-dump-note>DumbFoo</abbr></span> {<span name=sf-dump-child> <a class=sf-dump-ref name="sf-dump-ref2">#2</a>
<span class=sf-dump-2><span class=sf-dump-public>foo</span>: "<span class=sf-dump-str>foo</span>"
"<span class=sf-dump-public>bar</span>": "<span class=sf-dump-str>bar</span>"
</span>}
"<span class=sf-dump-meta>closure</span>" => <span class=sf-dump-note>Closure</span> {
</span></span>}
"<span class=sf-dump-meta>closure</span>" => <span class=sf-dump-note>Closure</span> {<span name=sf-dump-child>
<span class=sf-dump-2><span class=sf-dump-meta>reflection</span>: """
<span class=sf-dump-str>Closure [ &lt;user&gt; {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] {</span>
<span class=sf-dump-str> @@ {$var['file']} {$var['line']} - {$var['line']}</span>
@ -77,22 +86,22 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase
<span class=sf-dump-str> }</span>
<span class=sf-dump-str>}</span>
"""
</span>}
</span></span>}
"<span class=sf-dump-meta>line</span>" => <span class=sf-dump-num>{$var['line']}</span>
"<span class=sf-dump-meta>nobj</span>" => <span class=sf-dump-note>array:1</span> [
"<span class=sf-dump-meta>nobj</span>" => <span class=sf-dump-note>array:1</span> [<span name=sf-dump-child>
<span class=sf-dump-2><span class=sf-dump-meta>0</span> => {} <a class=sf-dump-ref name="sf-dump-ref3">#3</a>
</span>]
"<span class=sf-dump-meta>recurs</span>" => <span class=sf-dump-note>array:1</span> [ <a class=sf-dump-ref name="sf-dump-ref4">#4</a>
</span></span>]
"<span class=sf-dump-meta>recurs</span>" => <span class=sf-dump-note>array:1</span> [<span name=sf-dump-child> <a class=sf-dump-ref name="sf-dump-ref4">#4</a>
<span class=sf-dump-2><span class=sf-dump-meta>0</span> => <a class=sf-dump-ref href="#sf-dump-ref4">&4</a> <span class=sf-dump-note>array:1</span> [<a class=sf-dump-ref href="#sf-dump-ref4">@4</a>]
</span>]
</span></span>]
<span class=sf-dump-meta>9</span> => <a class=sf-dump-ref href="#sf-dump-ref1">&1</a> <span class=sf-dump-const>null</span>
"<span class=sf-dump-meta>sobj</span>" => <span class=sf-dump-note>Symfony\Component\VarDumper\Tests\Fixture\DumbFoo</span> {<a class=sf-dump-ref href="#sf-dump-ref2">@2</a>}
"<span class=sf-dump-meta>sobj</span>" => <span class=sf-dump-note><abbr title="Symfony\Component\VarDumper\Tests\Fixture\DumbFoo" class=sf-dump-note>DumbFoo</abbr></span> {<a class=sf-dump-ref href="#sf-dump-ref2">@2</a>}
"<span class=sf-dump-meta>snobj</span>" => <a class=sf-dump-ref href="#sf-dump-ref3">&3</a> {<a class=sf-dump-ref href="#sf-dump-ref3">@3</a>}
"<span class=sf-dump-meta>snobj2</span>" => {<a class=sf-dump-ref href="#sf-dump-ref3">@3</a>}
"<span class=sf-dump-meta>file</span>" => "<span class=sf-dump-str>{$var['file']}</span>"
b"<span class=sf-dump-meta>bin-key-é</span>" => ""
</span>]
</pre>
</span></span>]
</span></bar>
EOTXT
,