diff --git a/composer.json b/composer.json index fe40170481c7b..07aae60f9730d 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "symfony/css-selector": "self.version", "symfony/dependency-injection": "self.version", "symfony/debug": "self.version", + "symfony/debug-bundle": "self.version", "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", "symfony/event-dispatcher": "self.version", @@ -63,6 +64,7 @@ "symfony/twig-bridge": "self.version", "symfony/twig-bundle": "self.version", "symfony/validator": "self.version", + "symfony/var-dumper": "self.version", "symfony/web-profiler-bundle": "self.version", "symfony/yaml": "self.version" }, diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php new file mode 100644 index 0000000000000..2fc02f3302e67 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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. + * + * @author Nicolas Grekas + */ +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()); + } + + public function getName() + { + 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; + } +} diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php new file mode 100644 index 0000000000000..654b03aecf62e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Node; + +/** + * @author Julien Galenski + */ +class DumpNode extends \Twig_Node +{ + private $varPrefix; + + public function __construct($varPrefix, \Twig_NodeInterface $values = null, $lineno, $tag = null) + { + parent::__construct(array('values' => $values), array(), $lineno, $tag); + $this->varPrefix = $varPrefix; + } + + /** + * {@inheritdoc} + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->write("if (\$this->env->isDebug()) {\n") + ->indent(); + + $values = $this->getNode('values'); + + if (null === $values) { + // remove embedded templates (macros) from the context + $compiler + ->write(sprintf('$%svars = array();'."\n", $this->varPrefix)) + ->write(sprintf('foreach ($context as $%1$skey => $%1$sval) {'."\n", $this->varPrefix)) + ->indent() + ->write(sprintf('if (!$%sval instanceof \Twig_Template) {'."\n", $this->varPrefix)) + ->indent() + ->write(sprintf('$%1$svars[$%1$skey] = $%1$sval;'."\n", $this->varPrefix)) + ->outdent() + ->write("}\n") + ->outdent() + ->write("}\n") + ->addDebugInfo($this) + ->write(sprintf('\Symfony\Component\VarDumper\VarDumper::dump($%svars);'."\n", $this->varPrefix)); + } elseif (1 === $values->count()) { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\VarDumper\VarDumper::dump(') + ->subcompile($values->getNode(0)) + ->raw(");\n"); + } else { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\VarDumper\VarDumper::dump(array('."\n") + ->indent(); + foreach ($values as $node) { + $compiler->addIndentation(); + if ($node->hasAttribute('name')) { + $compiler + ->string($node->getAttribute('name')) + ->raw(' => '); + } + $compiler + ->subcompile($node) + ->raw(",\n"); + } + $compiler + ->outdent() + ->write("));\n"); + } + + $compiler + ->outdent() + ->raw("}\n"); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php new file mode 100644 index 0000000000000..3de9a0e91eee7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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 getDumpTags + */ + 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($extension); + + $dumped = null; + $exception = null; + $prevDumper = VarDumper::setHandler(function ($var) use (&$dumped) {$dumped = $var;}); + + try { + $this->assertEquals($expectedOutput, $twig->render($template)); + } catch (\Exception $exception) { + } + + VarDumper::setHandler($prevDumper); + + if (null !== $exception) { + throw $exception; + } + + $this->assertSame($expectedDumped, $dumped); + } + + public function getDumpTags() + { + return array( + array('A{% dump %}B', true, 'AB', array()), + array('A{% set foo="bar"%}B{% dump %}C', true, 'ABC', array('foo' => 'bar')), + 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('\n"), + array( + array(), + array(123, 456), + "
123\n
\n" + ."
456\n
\n", + ), + array( + array('foo' => 'bar'), + array(), + "
array:1 [\n"
+                ."  \"foo\" => \"bar\"\n"
+                ."]\n"
+                ."
\n", + ), + ); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php new file mode 100644 index 0000000000000..1efe52d126045 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Node; + +use Symfony\Bridge\Twig\Node\DumpNode; + +class DumpNodeTest extends \PHPUnit_Framework_TestCase +{ + public function testNoVar() + { + $node = new DumpNode('bar', null, 7); + + $env = new \Twig_Environment(); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + $barvars = array(); + foreach ($context as $barkey => $barval) { + if (!$barval instanceof \Twig_Template) { + $barvars[$barkey] = $barval; + } + } + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump($barvars); +} + +EOTXT; + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } + + public function testOneVar() + { + $vars = new \Twig_Node(array( + new \Twig_Node_Expression_Name('foo', 7), + )); + $node = new DumpNode('bar', $vars, 7); + + $env = new \Twig_Environment(); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump(%foo%); +} + +EOTXT; + $expected = preg_replace('/%(.*?)%/', version_compare(PHP_VERSION, '5.4.0') >= 0 ? '(isset($context["$1"]) ? $context["$1"] : null)' : '$this->getContext($context, "$1")', $expected); + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } + + public function testMultiVars() + { + $vars = new \Twig_Node(array( + new \Twig_Node_Expression_Name('foo', 7), + new \Twig_Node_Expression_Name('bar', 7), + )); + $node = new DumpNode('bar', $vars, 7); + + $env = new \Twig_Environment(); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump(array( + "foo" => %foo%, + "bar" => %bar%, + )); +} + +EOTXT; + $expected = preg_replace('/%(.*?)%/', version_compare(PHP_VERSION, '5.4.0') >= 0 ? '(isset($context["$1"]) ? $context["$1"] : null)' : '$this->getContext($context, "$1")', $expected); + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } +} diff --git a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php new file mode 100644 index 0000000000000..269ead64f5d40 --- /dev/null +++ b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\TokenParser; + +use Symfony\Bridge\Twig\Node\DumpNode; + +/** + * Token Parser for the 'dump' tag. + * + * Dump variables with: + *
+ *  {% dump %}
+ *  {% dump foo %}
+ *  {% dump foo, bar %}
+ * 
+ * + * @author Julien Galenski + */ +class DumpTokenParser extends \Twig_TokenParser +{ + /** + * {@inheritdoc} + */ + public function parse(\Twig_Token $token) + { + $values = null; + if (!$this->parser->getStream()->test(\Twig_Token::BLOCK_END_TYPE)) { + $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + } + $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + + return new DumpNode($this->parser->getVarName(), $values, $token->getLine(), $this->getTag()); + } + + /** + * {@inheritdoc} + */ + public function getTag() + { + return 'dump'; + } +} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 59db37b44f68d..d9a864b919641 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -30,6 +30,7 @@ "symfony/security": "~2.4", "symfony/stopwatch": "~2.2", "symfony/console": "~2.2", + "symfony/var-dumper": "~2.6", "symfony/expression-language": "~2.4" }, "suggest": { @@ -41,6 +42,7 @@ "symfony/yaml": "For using the YamlExtension", "symfony/security": "For using the SecurityExtension", "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/var-dumper": "For using the DumpExtension", "symfony/expression-language": "For using the ExpressionExtension" }, "autoload": { diff --git a/src/Symfony/Bundle/DebugBundle/.gitignore b/src/Symfony/Bundle/DebugBundle/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php new file mode 100644 index 0000000000000..0399e63a9badf --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\VarDumper; + +/** + * @author Nicolas Grekas + */ +class DebugBundle extends Bundle +{ + public function boot() + { + if ($this->container->getParameter('kernel.debug')) { + $container = $this->container; + + // This code is here to lazy load the dump stack. This default + // configuration for CLI mode is overridden in HTTP mode on + // 'kernel.request' event + VarDumper::setHandler(function ($var) use ($container) { + $dumper = new CliDumper(); + $cloner = $container->get('var_dumper.cloner'); + $handler = function ($var) use ($dumper, $cloner) { + $dumper->dump($cloner->cloneVar($var)); + }; + VarDumper::setHandler($handler); + $handler($var); + }); + } + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000000000..176211ad70940 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * DebugExtension configuration structure. + * + * @author Nicolas Grekas + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('debug'); + + $rootNode + ->children() + ->integerNode('max_items') + ->info('Max number of displayed items past the first level, -1 means no limit') + ->min(-1) + ->defaultValue(250) + ->end() + ->integerNode('max_string_length') + ->info('Max length of displayed strings, -1 means no limit') + ->min(-1) + ->defaultValue(-1) + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php new file mode 100644 index 0000000000000..4b136c89454b8 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +/** + * DebugExtension. + * + * @author Nicolas Grekas + */ +class DebugExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.xml'); + + $container->setParameter( + 'var_dumper.cloner.class', + 'Symfony\Component\VarDumper\Cloner\\'.(function_exists('symfony_zval_info') ? 'Ext' : 'Php').'Cloner' + ); + + $container->getDefinition('var_dumper.cloner') + ->addMethodCall('setMaxItems', array($config['max_items'])) + ->addMethodCall('setMaxString', array($config['max_string_length'])); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/README.md b/src/Symfony/Bundle/DebugBundle/README.md new file mode 100644 index 0000000000000..36935ba33ed5c --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/README.md @@ -0,0 +1,22 @@ +dump() function +================ + +This bundle provides a better `dump()` function, that you can use instead of +`var_dump()`, *better* meaning: + +- per object and resource types specialized view: e.g. filter out Doctrine noise + while dumping a single proxy entity, or get more insight on opened files with + `stream_get_meta_data()`. Add your own dedicated `Dumper\Caster` and get the + view *you* need. +- configurable output format: HTML, command line with colors or [a dedicated high + accuracy JSON format](Resource/doc/json-spec.md). +- ability to dump internal references, either soft ones (objects or resources) + or hard ones (`=&` on arrays or objects properties). Repeated occurrences of + the same object/array/resource won't appear again and again anymore. Moreover, + you'll be able to inspect the reference structure of your data. +- ability to operate in the context of an output buffering handler. +- full exposure of the internal mechanisms used for walking through an arbitrary + PHP data structure. + +Calling `dump($myVvar)` works in all PHP code and `{% dump myVar %}` or +`{{ dump(myVar) }}` in Twig templates. diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml new file mode 100644 index 0000000000000..788a63cb4c222 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -0,0 +1,27 @@ + + + + + + Symfony\Component\HttpKernel\DataCollector\DumpDataCollector + Symfony\Component\HttpKernel\EventListener\DumpListener + + + + + + + + + + + + data_collector.dump + + + + + + diff --git a/src/Symfony/Bundle/DebugBundle/Resources/meta/LICENSE b/src/Symfony/Bundle/DebugBundle/Resources/meta/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/meta/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig b/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig new file mode 100644 index 0000000000000..d024ee1d062b3 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig @@ -0,0 +1,78 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set dumps_count = collector.dumpsCount %} + + {% if dumps_count %} + {% set icon %} + dump() + {{ dumps_count }} + {% endset %} + + {% set text %} +
+
+ dump() +
+ {% for dump in collector.getDumps('html') %} +
+ in {% if dump.file %}{{ dump.name }}{% else %}{{ dump.name }}{% endif %} + line {{ dump.line }}: + {{ dump.data|raw }} +
+ {% endfor %} + +
+ {% endset %} + + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} + {% endif %} +{% endblock %} + +{% block menu %} + + + {{- "" -}} + dump() + {{- "" -}} + + dump() + + {{ collector.dumpsCount }} + + +{% endblock %} + +{% block panel %} +

dump()

+ + + +
    + {% for dump in collector.getDumps('html') %} +
  • + in {% if dump.file %}{{ dump.name }}{% else %}{{ dump.name }}{% endif %} + line {{ dump.line }}: + + + {% if dump.fileExcerpt %}{{ dump.fileExcerpt|raw }}{% else %}{{ dump.file|file_excerpt(dump.line) }}{% endif %} + + + {{ dump.data|raw }} +
  • + {% endfor %} +
+{% endblock %} diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php new file mode 100644 index 0000000000000..4d22f59ce9fa8 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\Tests\DependencyInjection; + +use Symfony\Bundle\DebugBundle\DependencyInjection\DebugExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + +class DebugExtensionTest extends \PHPUnit_Framework_TestCase +{ + public function testLoadWithoutConfiguration() + { + $container = $this->createContainer(); + $container->registerExtension(new DebugExtension()); + $container->loadFromExtension('debug', array()); + $this->compileContainer($container); + + $expectedTags = array( + array( + "id" => "dump", + "template" => "@Debug/Profiler/dump.html.twig", + ), + ); + + $this->assertSame($expectedTags, $container->getDefinition('data_collector.dump')->getTag('data_collector')); + } + + private function createContainer() + { + $container = new ContainerBuilder(new ParameterBag(array( + 'kernel.cache_dir' => __DIR__, + 'kernel.root_dir' => __DIR__.'/Fixtures', + 'kernel.charset' => 'UTF-8', + 'kernel.debug' => true, + 'kernel.bundles' => array('DebugBundle' => 'Symfony\\Bundle\\DebugBundle\\DebugBundle'), + ))); + + return $container; + } + + private function compileContainer(ContainerBuilder $container) + { + $container->getCompilerPassConfig()->setOptimizationPasses(array()); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json new file mode 100644 index 0000000000000..6412fdeaf5c44 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/debug-bundle", + "type": "symfony-bundle", + "description": "Symfony DebugBundle", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3", + "symfony/http-kernel": "~2.6", + "symfony/twig-bridge": "~2.6", + "symfony/var-dumper": "~2.6" + }, + "suggest": { + "symfony/config": "For service container configuration", + "symfony/dependency-injection": "For using as a service from the container" + }, + "autoload": { + "psr-0": { "Symfony\\Bundle\\DebugBundle\\": "" } + }, + "target-dir": "Symfony/Bundle/DebugBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + } +} diff --git a/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist new file mode 100644 index 0000000000000..f33d475b2926e --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./Resources + ./vendor + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml index c44ae6312155e..36ddc2a33d82a 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml @@ -6,6 +6,7 @@ Symfony\Bundle\TwigBundle\Debug\TimedTwigEngine + Symfony\Bridge\Twig\Extension\DumpExtension @@ -16,8 +17,9 @@ - + + diff --git a/src/Symfony/Component/Debug/Resources/ext/README.rst b/src/Symfony/Component/Debug/Resources/ext/README.rst new file mode 100644 index 0000000000000..77be5648aaa2b --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/README.rst @@ -0,0 +1,71 @@ +Symfony Debug Extension +======================= + +This extension adds a ``symfony_zval_info($key, $array, $options = 0)`` function that: + +- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP, +- does work with references, preventing memory copying. + +Its behavior is about the same as: + +.. code-block:: php + + gettype($array[$key]), + 'zval_hash' => /* hashed memory address of $array[$key] */, + 'zval_refcount' => /* internal zval refcount of $array[$key] */, + 'zval_isref' => /* is_ref status of $array[$key] */, + ); + + switch ($info['type']) { + case 'object': + $info += array( + 'object_class' => get_class($array[$key]), + 'object_refcount' => /* internal object refcount of $array[$key] */, + 'object_hash' => spl_object_hash($array[$key]), + ); + break; + + case 'resource': + $info += array( + 'resource_id' => (int) substr((string) $array[$key], 13), + 'resource_type' => get_resource_type($array[$key]), + 'resource_refcount' => /* internal resource refcount of $array[$key] */, + ); + break; + + case 'array': + $info += array( + 'array_count' => count($array[$key]), + ); + break; + + case 'string': + $info += array( + 'strlen' => strlen($array[$key]), + ); + break; + } + + return $info; + } + +To enable the extension from source, run: + +.. code-block:: sh + + phpize + ./configure + make + sudo make install + diff --git a/src/Symfony/Component/Debug/Resources/ext/ext/config.m4 b/src/Symfony/Component/Debug/Resources/ext/ext/config.m4 new file mode 100644 index 0000000000000..3c56047150569 --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/ext/config.m4 @@ -0,0 +1,63 @@ +dnl $Id$ +dnl config.m4 for extension symfony_debug + +dnl Comments in this file start with the string 'dnl'. +dnl Remove where necessary. This file will not work +dnl without editing. + +dnl If your extension references something external, use with: + +dnl PHP_ARG_WITH(symfony_debug, for symfony_debug support, +dnl Make sure that the comment is aligned: +dnl [ --with-symfony_debug Include symfony_debug support]) + +dnl Otherwise use enable: + +PHP_ARG_ENABLE(symfony_debug, whether to enable symfony_debug support, +dnl Make sure that the comment is aligned: +[ --enable-symfony_debug Enable symfony_debug support]) + +if test "$PHP_SYMFONY_DEBUG" != "no"; then + dnl Write more examples of tests here... + + dnl # --with-symfony_debug -> check with-path + dnl SEARCH_PATH="/usr/local /usr" # you might want to change this + dnl SEARCH_FOR="/include/symfony_debug.h" # you most likely want to change this + dnl if test -r $PHP_SYMFONY_DEBUG/$SEARCH_FOR; then # path given as parameter + dnl SYMFONY_DEBUG_DIR=$PHP_SYMFONY_DEBUG + dnl else # search default path list + dnl AC_MSG_CHECKING([for symfony_debug files in default path]) + dnl for i in $SEARCH_PATH ; do + dnl if test -r $i/$SEARCH_FOR; then + dnl SYMFONY_DEBUG_DIR=$i + dnl AC_MSG_RESULT(found in $i) + dnl fi + dnl done + dnl fi + dnl + dnl if test -z "$SYMFONY_DEBUG_DIR"; then + dnl AC_MSG_RESULT([not found]) + dnl AC_MSG_ERROR([Please reinstall the symfony_debug distribution]) + dnl fi + + dnl # --with-symfony_debug -> add include path + dnl PHP_ADD_INCLUDE($SYMFONY_DEBUG_DIR/include) + + dnl # --with-symfony_debug -> check for lib and symbol presence + dnl LIBNAME=symfony_debug # you may want to change this + dnl LIBSYMBOL=symfony_debug # you most likely want to change this + + dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL, + dnl [ + dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $SYMFONY_DEBUG_DIR/lib, SYMFONY_DEBUG_SHARED_LIBADD) + dnl AC_DEFINE(HAVE_SYMFONY_DEBUGLIB,1,[ ]) + dnl ],[ + dnl AC_MSG_ERROR([wrong symfony_debug lib version or lib not found]) + dnl ],[ + dnl -L$SYMFONY_DEBUG_DIR/lib -lm + dnl ]) + dnl + dnl PHP_SUBST(SYMFONY_DEBUG_SHARED_LIBADD) + + PHP_NEW_EXTENSION(symfony_debug, symfony_debug.c, $ext_shared) +fi diff --git a/src/Symfony/Component/Debug/Resources/ext/ext/config.w32 b/src/Symfony/Component/Debug/Resources/ext/ext/config.w32 new file mode 100644 index 0000000000000..487e6913891cf --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/ext/config.w32 @@ -0,0 +1,13 @@ +// $Id$ +// vim:ft=javascript + +// If your extension references something external, use ARG_WITH +// ARG_WITH("symfony_debug", "for symfony_debug support", "no"); + +// Otherwise, use ARG_ENABLE +// ARG_ENABLE("symfony_debug", "enable symfony_debug support", "no"); + +if (PHP_SYMFONY_DEBUG != "no") { + EXTENSION("symfony_debug", "symfony_debug.c"); +} + diff --git a/src/Symfony/Component/Debug/Resources/ext/ext/php_symfony_debug.h b/src/Symfony/Component/Debug/Resources/ext/ext/php_symfony_debug.h new file mode 100644 index 0000000000000..c935f67019299 --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/ext/php_symfony_debug.h @@ -0,0 +1,55 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#ifndef PHP_SYMFONY_DEBUG_H +#define PHP_SYMFONY_DEBUG_H + +extern zend_module_entry symfony_debug_module_entry; +#define phpext_symfony_debug_ptr &symfony_debug_module_entry + +#define PHP_SYMFONY_DEBUG_VERSION "1.0" + +#ifdef PHP_WIN32 +# define PHP_SYMFONY_DEBUG_API __declspec(dllexport) +#elif defined(__GNUC__) && __GNUC__ >= 4 +# define PHP_SYMFONY_DEBUG_API __attribute__ ((visibility("default"))) +#else +# define PHP_SYMFONY_DEBUG_API +#endif + +#ifdef ZTS +#include "TSRM.h" +#endif + +ZEND_BEGIN_MODULE_GLOBALS(symfony_debug) + intptr_t req_rand_init; +ZEND_END_MODULE_GLOBALS(symfony_debug) + +PHP_MINIT_FUNCTION(symfony_debug); +PHP_MSHUTDOWN_FUNCTION(symfony_debug); +PHP_RINIT_FUNCTION(symfony_debug); +PHP_RSHUTDOWN_FUNCTION(symfony_debug); +PHP_MINFO_FUNCTION(symfony_debug); +PHP_GINIT_FUNCTION(symfony_debug); +PHP_GSHUTDOWN_FUNCTION(symfony_debug); + +PHP_FUNCTION(symfony_zval_info); + +static char *_symfony_debug_memory_address_hash(void *); +static const char *_symfony_debug_zval_type(zval *); +static const char* _symfony_debug_get_resource_type(long); +static int _symfony_debug_get_resource_refcount(long); + +#ifdef ZTS +#define SYMFONY_DEBUG_G(v) TSRMG(symfony_debug_globals_id, zend_symfony_debug_globals *, v) +#else +#define SYMFONY_DEBUG_G(v) (symfony_debug_globals.v) +#endif + +#endif /* PHP_SYMFONY_DEBUG_H */ diff --git a/src/Symfony/Component/Debug/Resources/ext/ext/symfony_debug.c b/src/Symfony/Component/Debug/Resources/ext/ext/symfony_debug.c new file mode 100644 index 0000000000000..2bcb922d5c98b --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/ext/symfony_debug.c @@ -0,0 +1,223 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" +#include "php_ini.h" +#include "ext/standard/info.h" +#include "php_symfony_debug.h" +#include "ext/standard/php_rand.h" +#include "ext/standard/php_lcg.h" +#include "ext/spl/php_spl.h" +#include "Zend/zend_gc.h" + +ZEND_DECLARE_MODULE_GLOBALS(symfony_debug) + +ZEND_BEGIN_ARG_INFO_EX(symfony_zval_arginfo, 0, 0, 2) + ZEND_ARG_INFO(0, key) + ZEND_ARG_ARRAY_INFO(0, array, 0) + ZEND_ARG_INFO(0, options) +ZEND_END_ARG_INFO() + +const zend_function_entry symfony_debug_functions[] = { + PHP_FE(symfony_zval_info, symfony_zval_arginfo) + PHP_FE_END +}; + +PHP_FUNCTION(symfony_zval_info) +{ + zval *key = NULL, *arg = NULL; + zval **data = NULL; + HashTable *array = NULL; + long options = 0; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "zh|l", &key, &array, &options) == FAILURE) { + return; + } + + switch (Z_TYPE_P(key)) { + case IS_STRING: + if (zend_symtable_find(array, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, (void **)&data) == FAILURE) { + return; + } + break; + case IS_LONG: + if (zend_hash_index_find(array, Z_LVAL_P(key), (void **)&data)) { + return; + } + break; + } + + arg = *data; + + array_init(return_value); + + add_assoc_string(return_value, "type", (char *)_symfony_debug_zval_type(arg), 1); + add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg), 16, 1); + add_assoc_long(return_value, "zval_refcount", Z_REFCOUNT_P(arg)); + add_assoc_bool(return_value, "zval_isref", (zend_bool)Z_ISREF_P(arg)); + + if (Z_TYPE_P(arg) == IS_OBJECT) { + static char hash[33] = {0}; + php_spl_object_hash(arg, (char *)hash); + add_assoc_stringl(return_value, "object_class", (char *)Z_OBJCE_P(arg)->name, Z_OBJCE_P(arg)->name_length, 1); + add_assoc_long(return_value, "object_refcount", EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(arg)].bucket.obj.refcount); + add_assoc_string(return_value, "object_hash", hash, 1); + } else if (Z_TYPE_P(arg) == IS_ARRAY) { + add_assoc_long(return_value, "array_count", zend_hash_num_elements(Z_ARRVAL_P(arg))); + } else if(Z_TYPE_P(arg) == IS_RESOURCE) { + add_assoc_long(return_value, "resource_id", Z_LVAL_P(arg)); + add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg)), 1); + add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg))); + } else if (Z_TYPE_P(arg) == IS_STRING) { + add_assoc_long(return_value, "strlen", Z_STRLEN_P(arg)); + } +} + +static const char* _symfony_debug_get_resource_type(long rsid) +{ + const char *res_type; + res_type = zend_rsrc_list_get_rsrc_type(rsid); + + if (!res_type) { + return "Unknown"; + } + + return res_type; +} + +static int _symfony_debug_get_resource_refcount(long rsid) +{ + zend_rsrc_list_entry *le; + + if (zend_hash_index_find(&EG(regular_list), rsid, (void **) &le)==SUCCESS) { + return le->refcount; + } + + return 0; +} + +static char *_symfony_debug_memory_address_hash(void *address) +{ + static char result[17] = {0}; + intptr_t address_rand; + + if (!SYMFONY_DEBUG_G(req_rand_init)) { + if (!BG(mt_rand_is_seeded)) { + php_mt_srand(GENERATE_SEED() TSRMLS_CC); + } + SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand(); + } + + address_rand = (intptr_t)address ^ SYMFONY_DEBUG_G(req_rand_init); + + snprintf(result, 17, "%016zx", address_rand); + + return result; +} + +static const char *_symfony_debug_zval_type(zval *zv) +{ + switch (Z_TYPE_P(zv)) { + case IS_NULL: + return "NULL"; + break; + + case IS_BOOL: + return "boolean"; + break; + + case IS_LONG: + return "integer"; + break; + + case IS_DOUBLE: + return "double"; + break; + + case IS_STRING: + return "string"; + break; + + case IS_ARRAY: + return "array"; + break; + + case IS_OBJECT: + return "object"; + + case IS_RESOURCE: + return "resource"; + + default: + return "unknown type"; + } +} + +zend_module_entry symfony_debug_module_entry = { + STANDARD_MODULE_HEADER, + "symfony_debug", + symfony_debug_functions, + PHP_MINIT(symfony_debug), + PHP_MSHUTDOWN(symfony_debug), + PHP_RINIT(symfony_debug), + PHP_RSHUTDOWN(symfony_debug), + PHP_MINFO(symfony_debug), + PHP_SYMFONY_DEBUG_VERSION, + PHP_MODULE_GLOBALS(symfony_debug), + PHP_GINIT(symfony_debug), + PHP_GSHUTDOWN(symfony_debug), + NULL, + STANDARD_MODULE_PROPERTIES_EX +}; + +#ifdef COMPILE_DL_SYMFONY_DEBUG +ZEND_GET_MODULE(symfony_debug) +#endif + +PHP_GINIT_FUNCTION(symfony_debug) +{ + symfony_debug_globals->req_rand_init = 0; +} + +PHP_GSHUTDOWN_FUNCTION(symfony_debug) +{ + +} + +PHP_MINIT_FUNCTION(symfony_debug) +{ + return SUCCESS; +} + +PHP_MSHUTDOWN_FUNCTION(symfony_debug) +{ + return SUCCESS; +} + +PHP_RINIT_FUNCTION(symfony_debug) +{ + return SUCCESS; +} + +PHP_RSHUTDOWN_FUNCTION(symfony_debug) +{ + return SUCCESS; +} + +PHP_MINFO_FUNCTION(symfony_debug) +{ + php_info_print_table_start(); + php_info_print_table_header(2, "Symfony Debug support", "enabled"); + php_info_print_table_header(2, "Symfony Debug version", PHP_SYMFONY_DEBUG_VERSION); + php_info_print_table_end(); +} diff --git a/src/Symfony/Component/Debug/Resources/ext/ext/tests/001.phpt b/src/Symfony/Component/Debug/Resources/ext/ext/tests/001.phpt new file mode 100644 index 0000000000000..a0015a939445c --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/ext/tests/001.phpt @@ -0,0 +1,149 @@ +--TEST-- +Test symfony_zval_info API +--SKIPIF-- + +--FILE-- + $int, + 'float' => $float, + 'str' => $str, + 'object' => $object, + 'array' => $array, + 'resource' => $resource, + 'null' => $null, + 'bool' => $bool, + 'refcount' => &$refcount2); + +var_dump(symfony_zval_info('int', $var)); +var_dump(symfony_zval_info('float', $var)); +var_dump(symfony_zval_info('str', $var)); +var_dump(symfony_zval_info('object', $var)); +var_dump(symfony_zval_info('array', $var)); +var_dump(symfony_zval_info('resource', $var)); +var_dump(symfony_zval_info('null', $var)); +var_dump(symfony_zval_info('bool', $var)); + +var_dump(symfony_zval_info('refcount', $var)); +var_dump(symfony_zval_info('not-exist', $var)); +?> +--EXPECTF-- +array(4) { + ["type"]=> + string(7) "integer" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) +} +array(4) { + ["type"]=> + string(6) "double" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) +} +array(5) { + ["type"]=> + string(6) "string" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) + ["strlen"]=> + int(6) +} +array(7) { + ["type"]=> + string(6) "object" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) + ["object_class"]=> + string(8) "stdClass" + ["object_refcount"]=> + int(1) + ["object_hash"]=> + string(32) "%s" +} +array(5) { + ["type"]=> + string(5) "array" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) + ["array_count"]=> + int(2) +} +array(7) { + ["type"]=> + string(8) "resource" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) + ["resource_id"]=> + int(4) + ["resource_type"]=> + string(6) "stream" + ["resource_refcount"]=> + int(1) +} +array(4) { + ["type"]=> + string(4) "NULL" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) +} +array(4) { + ["type"]=> + string(7) "boolean" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(2) + ["zval_isref"]=> + bool(false) +} +array(4) { + ["type"]=> + string(7) "integer" + ["zval_hash"]=> + string(16) "%s" + ["zval_refcount"]=> + int(3) + ["zval_isref"]=> + bool(true) +} +NULL \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php new file mode 100644 index 0000000000000..5b65de4961bc9 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php @@ -0,0 +1,229 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\JsonDumper; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Symfony\Component\VarDumper\Dumper\DataDumperInterface; + +/** + * @author Nicolas Grekas + */ +class DumpDataCollector extends DataCollector implements DataDumperInterface +{ + private $stopwatch; + private $dataCount = 0; + private $isCollected = true; + private $clonesCount = 0; + private $clonesIndex = 0; + private $rootRefs; + + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + + // All clones share these properties by reference: + $this->rootRefs = array( + &$this->data, + &$this->dataCount, + &$this->isCollected, + &$this->clonesCount, + ); + } + + public function __clone() + { + $this->clonesIndex = ++$this->clonesCount; + } + + public function dump(Data $data) + { + if ($this->stopwatch) { + $this->stopwatch->start('dump'); + } + if ($this->isCollected) { + $this->isCollected = false; + } + + $trace = PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS : true; + if (PHP_VERSION_ID >= 50400) { + $trace = debug_backtrace($trace, 7); + } else { + $trace = debug_backtrace($trace); + } + + $file = $trace[0]['file']; + $line = $trace[0]['line']; + $name = false; + $fileExcerpt = false; + + for ($i = 1; $i < 7; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dump' === $trace[$i]['function'] + && 'Symfony\Component\VarDumper\VarDumper' === $trace[$i]['class'] + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 7) { + if (isset($trace[$i]['function']) && empty($trace[$i]['class']) && 'call_user_func' !== $trace[$i]['function']) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + break; + } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof \Twig_Template) { + $info = $trace[$i]['object']; + $name = $info->getTemplateName(); + $src = $info->getEnvironment()->getLoader()->getSource($name); + $info = $info->getDebugInfo(); + if (isset($info[$trace[$i-1]['line']])) { + $file = false; + $line = $info[$trace[$i-1]['line']]; + $src = explode("\n", $src); + $fileExcerpt = array(); + + for ($i = max($line - 3, 1), $max = min($line + 3, count($src)); $i <= $max; ++$i) { + $fileExcerpt[] = ''.htmlspecialchars($src[$i - 1]).''; + } + + $fileExcerpt = '
    '.implode("\n", $fileExcerpt).'
'; + } + break; + } + } + break; + } + } + + if (false === $name) { + $name = strtr($file, '\\', '/'); + $name = substr($file, strrpos($file, '/') + 1); + } + + $this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt'); + ++$this->dataCount; + + if ($this->stopwatch) { + $this->stopwatch->stop('dump'); + } + } + + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + public function serialize() + { + if ($this->clonesCount !== $this->clonesIndex) { + return 'a:0:{}'; + } + + $ser = serialize($this->data); + $this->data = array(); + $this->dataCount = 0; + $this->isCollected = true; + + return $ser; + } + + public function unserialize($data) + { + parent::unserialize($data); + $this->dataCount = count($this->data); + self::__construct($this->stopwatch); + } + + public function getDumpsCount() + { + return $this->dataCount; + } + + public function getDumps($format, $maxDepthLimit = -1, $maxItemsPerDepth = -1) + { + if ('html' === $format) { + $dumper = new HtmlDumper(); + } elseif ('json' === $format) { + $dumper = new JsonDumper(); + } else { + throw new \InvalidArgumentException(sprintf('Invalid dump format: %s', $format)); + } + $dumps = array(); + + foreach ($this->data as $dump) { + $data = ''; + $dumper->dump( + $dump['data']->getLimitedClone($maxDepthLimit, $maxItemsPerDepth), + function ($line, $depth) use (&$data) { + if (-1 !== $depth) { + $data .= str_repeat(' ', $depth).$line."\n"; + } + } + ); + $dump['data'] = $data; + $dumps[] = $dump; + } + + return $dumps; + } + + public function getName() + { + return 'dump'; + } + + public function __destruct() + { + if (0 === $this->clonesCount-- && !$this->isCollected && $this->data) { + $this->clonesCount = 0; + $this->isCollected = true; + + $h = headers_list(); + $i = count($h); + array_unshift($h, 'Content-Type: '.ini_get('default_mimetype')); + while (0 !== stripos($h[$i], 'Content-Type:')) { + --$i; + } + + if ('cli' !== PHP_SAPI && stripos($h[$i], 'html')) { + echo ''; + $dumper = new HtmlDumper('php://output'); + } else { + $dumper = new CliDumper('php://output'); + $dumper->setColors(false); + } + + foreach ($this->data as $i => $dump) { + $this->data[$i] = null; + + if ($dumper instanceof HtmlDumper) { + $dump['name'] = htmlspecialchars($dump['name'], ENT_QUOTES, 'UTF-8'); + $dump['file'] = htmlspecialchars($dump['file'], ENT_QUOTES, 'UTF-8'); + if ('' !== $dump['file']) { + $dump['name'] = "{$dump['name']}"; + } + echo "\n{$dump['name']} on line {$dump['line']}:"; + } else { + echo "{$dump['name']} on line {$dump['line']}:\n"; + } + $dumper->dump($dump['data']); + } + + $this->data = array(); + $this->dataCount = 0; + } + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DumpListener.php b/src/Symfony/Component/HttpKernel/EventListener/DumpListener.php new file mode 100644 index 0000000000000..46833959b20dc --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/DumpListener.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\VarDumper\VarDumper; + +/** + * Configures dump() handler. + * + * @author Nicolas Grekas + */ +class DumpListener implements EventSubscriberInterface +{ + private $container; + private $dumper; + + /** + * @param ContainerInterface $container Service container, for lazy loading. + * @param string $dumper var_dumper dumper service to use. + */ + public function __construct(ContainerInterface $container, $dumper) + { + $this->container = $container; + $this->dumper = $dumper; + } + + public function configure() + { + if ($this->container) { + $container = $this->container; + $dumper = $this->dumper; + $this->container = null; + + VarDumper::setHandler(function ($var) use ($container, $dumper) { + $dumper = $container->get($dumper); + $cloner = $container->get('var_dumper.cloner'); + $handler = function ($var) use ($dumper, $cloner) {$dumper->dump($cloner->cloneVar($var));}; + VarDumper::setHandler($handler); + $handler($var); + }); + } + } + + public static function getSubscribedEvents() + { + // Register early to have a working dump() as early as possible + return array(KernelEvents::REQUEST => array('configure', 1024)); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php new file mode 100644 index 0000000000000..1218ef67e3ace --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\DataCollector; + +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * DumpDataCollectorTest + * + * @author Nicolas Grekas + */ +class DumpDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testDump() + { + $data = new Data(array(array(123))); + + $collector = new DumpDataCollector(); + + $this->assertSame('dump', $collector->getName()); + + $collector->dump($data); $line = __LINE__; + $this->assertSame(1, $collector->getDumpsCount()); + + $xDump = array( + array( + 'data' => "
123\n
\n", + 'name' => 'DumpDataCollectorTest.php', + 'file' => __FILE__, + 'line' => $line, + 'fileExcerpt' => false, + ), + ); + $dump = $collector->getDumps('html'); + $this->assertTrue(isset($dump[0]['data'])); + $dump[0]['data'] = preg_replace('/^.*?
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:',
+            str_replace("\0", '', $collector->serialize())
+        );
+
+        $this->assertSame(0, $collector->getDumpsCount());
+        $this->assertSame('a:0:{}', $collector->serialize());
+    }
+
+    public function testFlush()
+    {
+        $data = new Data(array(array(456)));
+        $collector = new DumpDataCollector();
+        $collector->dump($data); $line = __LINE__;
+
+        ob_start();
+        $collector = null;
+        $this->assertSame("DumpDataCollectorTest.php on line {$line}:\n456\n", ob_get_clean());
+    }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/DumpListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/DumpListenerTest.php
new file mode 100644
index 0000000000000..c665df563de60
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/DumpListenerTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Tests\EventListener;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\HttpKernel\EventListener\DumpListener;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\VarDumper\VarDumper;
+
+/**
+ * DumpListenerTest
+ *
+ * @author Nicolas Grekas 
+ */
+class DumpListenerTest extends \PHPUnit_Framework_TestCase
+{
+    public function testSubscribedEvents()
+    {
+        $this->assertSame(
+            array(KernelEvents::REQUEST => array('configure', 1024)),
+            DumpListener::getSubscribedEvents()
+        );
+    }
+
+    public function testConfigure()
+    {
+        $prevDumper = $this->getDumpHandler();
+
+        $container = new ContainerBuilder();
+        $container->setDefinition('var_dumper.cloner', new Definition('Symfony\Component\HttpKernel\Tests\EventListener\MockCloner'));
+        $container->setDefinition('mock_dumper', new Definition('Symfony\Component\HttpKernel\Tests\EventListener\MockDumper'));
+
+        ob_start();
+        $exception = null;
+        $listener = new DumpListener($container, 'mock_dumper');
+
+        try {
+            $listener->configure();
+
+            $lazyDumper = $this->getDumpHandler();
+            VarDumper::dump('foo');
+
+            $loadedDumper = $this->getDumpHandler();
+            VarDumper::dump('bar');
+
+            $this->assertSame('+foo-+bar-', ob_get_clean());
+
+            $listenerReflector = new \ReflectionClass($listener);
+            $lazyReflector = new \ReflectionFunction($lazyDumper);
+            $loadedReflector = new \ReflectionFunction($loadedDumper);
+
+            $this->assertSame($listenerReflector->getFilename(), $lazyReflector->getFilename());
+            $this->assertSame($listenerReflector->getFilename(), $loadedReflector->getFilename());
+            $this->assertGreaterThan($lazyReflector->getStartLine(), $loadedReflector->getStartLine());
+        } catch (\Exception $exception) {
+        }
+
+        VarDumper::setHandler($prevDumper);
+
+        if (null !== $exception) {
+            throw $exception;
+        }
+    }
+
+    private function getDumpHandler()
+    {
+        $prevDumper = VarDumper::setHandler('var_dump');
+        VarDumper::setHandler($prevDumper );
+
+        return $prevDumper;
+    }
+}
+
+class MockCloner
+{
+    public function cloneVar($var)
+    {
+        return $var.'-';
+    }
+}
+
+class MockDumper
+{
+    public function dump($var)
+    {
+        echo '+'.$var;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/CasterStub.php b/src/Symfony/Component/VarDumper/Caster/CasterStub.php
new file mode 100644
index 0000000000000..aaf8ed1035af1
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/CasterStub.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents the main properties of a PHP variable, pre-casted by a caster.
+ *
+ * @author Nicolas Grekas 
+ */
+class CasterStub extends Stub
+{
+    public function __construct($value, $class = '')
+    {
+        switch (gettype($value)) {
+            case 'object':
+                $this->type = self::TYPE_OBJECT;
+                $this->class = get_class($value);
+                $this->value = spl_object_hash($value);
+                $this->cut = -1;
+                break;
+
+            case 'array':
+                $this->type = self::TYPE_ARRAY;
+                $this->class = self::ARRAY_ASSOC;
+                $this->cut = $this->value = count($value);
+                break;
+
+            case 'resource':
+            case 'unknown type':
+                $this->type = self::TYPE_RESOURCE;
+                $this->class = @get_resource_type($value);
+                $this->value = (int) $value;
+                $this->cut = -1;
+                break;
+
+            case 'string':
+                if ('' === $class) {
+                    $this->type = self::TYPE_STRING;
+                    $this->class = preg_match('//u', $value) ? self::STRING_UTF8 : self::STRING_BINARY;
+                    $this->cut = self::STRING_BINARY === $this->class ? strlen($value) : (function_exists('iconv_strlen') ? iconv_strlen($value, 'UTF-8') : -1);
+                    break;
+                }
+                // No break;
+
+            default:
+                $this->class = $class;
+                $this->value = $value;
+                break;
+        }
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/DOMCaster.php b/src/Symfony/Component/VarDumper/Caster/DOMCaster.php
new file mode 100644
index 0000000000000..2a0dd2b9a61e8
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/DOMCaster.php
@@ -0,0 +1,302 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts DOM related classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class DOMCaster
+{
+    private static $errorCodes = array(
+        DOM_PHP_ERR => 'DOM_PHP_ERR',
+        DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR',
+        DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR',
+        DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR',
+        DOM_WRONG_DOCUMENT_ERR => 'DOM_WRONG_DOCUMENT_ERR',
+        DOM_INVALID_CHARACTER_ERR => 'DOM_INVALID_CHARACTER_ERR',
+        DOM_NO_DATA_ALLOWED_ERR => 'DOM_NO_DATA_ALLOWED_ERR',
+        DOM_NO_MODIFICATION_ALLOWED_ERR => 'DOM_NO_MODIFICATION_ALLOWED_ERR',
+        DOM_NOT_FOUND_ERR => 'DOM_NOT_FOUND_ERR',
+        DOM_NOT_SUPPORTED_ERR => 'DOM_NOT_SUPPORTED_ERR',
+        DOM_INUSE_ATTRIBUTE_ERR => 'DOM_INUSE_ATTRIBUTE_ERR',
+        DOM_INVALID_STATE_ERR => 'DOM_INVALID_STATE_ERR',
+        DOM_SYNTAX_ERR => 'DOM_SYNTAX_ERR',
+        DOM_INVALID_MODIFICATION_ERR => 'DOM_INVALID_MODIFICATION_ERR',
+        DOM_NAMESPACE_ERR => 'DOM_NAMESPACE_ERR',
+        DOM_INVALID_ACCESS_ERR => 'DOM_INVALID_ACCESS_ERR',
+        DOM_VALIDATION_ERR => 'DOM_VALIDATION_ERR',
+    );
+
+    private static $nodeTypes = array(
+        XML_ELEMENT_NODE => 'XML_ELEMENT_NODE',
+        XML_ATTRIBUTE_NODE => 'XML_ATTRIBUTE_NODE',
+        XML_TEXT_NODE => 'XML_TEXT_NODE',
+        XML_CDATA_SECTION_NODE => 'XML_CDATA_SECTION_NODE',
+        XML_ENTITY_REF_NODE => 'XML_ENTITY_REF_NODE',
+        XML_ENTITY_NODE => 'XML_ENTITY_NODE',
+        XML_PI_NODE => 'XML_PI_NODE',
+        XML_COMMENT_NODE => 'XML_COMMENT_NODE',
+        XML_DOCUMENT_NODE => 'XML_DOCUMENT_NODE',
+        XML_DOCUMENT_TYPE_NODE => 'XML_DOCUMENT_TYPE_NODE',
+        XML_DOCUMENT_FRAG_NODE => 'XML_DOCUMENT_FRAG_NODE',
+        XML_NOTATION_NODE => 'XML_NOTATION_NODE',
+        XML_HTML_DOCUMENT_NODE => 'XML_HTML_DOCUMENT_NODE',
+        XML_DTD_NODE => 'XML_DTD_NODE',
+        XML_ELEMENT_DECL_NODE => 'XML_ELEMENT_DECL_NODE',
+        XML_ATTRIBUTE_DECL_NODE => 'XML_ATTRIBUTE_DECL_NODE',
+        XML_ENTITY_DECL_NODE => 'XML_ENTITY_DECL_NODE',
+        XML_NAMESPACE_DECL_NODE => 'XML_NAMESPACE_DECL_NODE',
+    );
+
+    public static function castException(\DOMException $e, array $a, Stub $stub, $isNested)
+    {
+        if (isset($a["\0*\0code"], self::$errorCodes[$a["\0*\0code"]])) {
+            $a["\0*\0code"] = new CasterStub(self::$errorCodes[$a["\0*\0code"]], 'const');
+        }
+
+        return $a;
+    }
+
+    public static function castLength($dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'length' => $dom->length,
+        );
+
+        return $a;
+    }
+
+    public static function castImplementation($dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            "\0~\0Core" => '1.0',
+            "\0~\0XML" => '2.0',
+        );
+
+        return $a;
+    }
+
+    public static function castNode(\DOMNode $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'nodeName' => $dom->nodeName,
+            'nodeValue' => new CasterStub($dom->nodeValue),
+            'nodeType' => new CasterStub(self::$nodeTypes[$dom->nodeType], 'const'),
+            'parentNode' => new CasterStub($dom->parentNode),
+            'childNodes' => $dom->childNodes,
+            'firstChild' => new CasterStub($dom->firstChild),
+            'lastChild' => new CasterStub($dom->lastChild),
+            'previousSibling' => new CasterStub($dom->previousSibling),
+            'nextSibling' => new CasterStub($dom->nextSibling),
+            'attributes' => $dom->attributes,
+            'ownerDocument' => new CasterStub($dom->ownerDocument),
+            'namespaceURI' => $dom->namespaceURI,
+            'prefix' => $dom->prefix,
+            'localName' => $dom->localName,
+            'baseURI' => $dom->baseURI,
+            'textContent' => new CasterStub($dom->textContent),
+        );
+
+        return $a;
+    }
+
+    public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub $stub, $isNested)
+    {
+        // Commented lines denote properties that exist but are better not dumped for clarity.
+
+        $a += array(
+            'nodeName' => $dom->nodeName,
+            'nodeValue' => new CasterStub($dom->nodeValue),
+            'nodeType' => new CasterStub(self::$nodeTypes[$dom->nodeType], 'const'),
+            'prefix' => $dom->prefix,
+            'localName' => $dom->localName,
+            'namespaceURI' => $dom->namespaceURI,
+            'ownerDocument' => new CasterStub($dom->ownerDocument),
+            'parentNode' => new CasterStub($dom->parentNode),
+        );
+
+        return $a;
+    }
+
+    public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, $isNested)
+    {
+        $formatOutput = $dom->formatOutput;
+        $dom->formatOutput = true;
+
+        $a += array(
+            'doctype' => $dom->doctype,
+            'implementation' => $dom->implementation,
+            'documentElement' => new CasterStub($dom->documentElement),
+            'actualEncoding' => $dom->actualEncoding,
+            'encoding' => $dom->encoding,
+            'xmlEncoding' => $dom->xmlEncoding,
+            'standalone' => $dom->standalone,
+            'xmlStandalone' => $dom->xmlStandalone,
+            'version' => $dom->version,
+            'xmlVersion' => $dom->xmlVersion,
+            'strictErrorChecking' => $dom->strictErrorChecking,
+            'documentURI' => $dom->documentURI,
+            'config' => $dom->config,
+            'formatOutput' => $formatOutput,
+            'validateOnParse' => $dom->validateOnParse,
+            'resolveExternals' => $dom->resolveExternals,
+            'preserveWhiteSpace' => $dom->preserveWhiteSpace,
+            'recover' => $dom->recover,
+            'substituteEntities' => $dom->substituteEntities,
+            "\0~\0xml" => $dom->saveXML(),
+        );
+
+        $dom->formatOutput = $formatOutput;
+
+        return $a;
+    }
+
+    public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'data' => $dom->data,
+            'length' => $dom->length,
+        );
+
+        return $a;
+    }
+
+    public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'name' => $dom->name,
+            'specified' => $dom->specified,
+            'value' => $dom->value,
+            'ownerElement' => $dom->ownerElement,
+            'schemaTypeInfo' => $dom->schemaTypeInfo,
+        );
+
+        return $a;
+    }
+
+    public static function castElement(\DOMElement $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'tagName' => $dom->tagName,
+            'schemaTypeInfo' => $dom->schemaTypeInfo,
+        );
+
+        return $a;
+    }
+
+    public static function castText(\DOMText $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'wholeText' => $dom->wholeText,
+        );
+
+        return $a;
+    }
+
+    public static function castTypeinfo(\DOMTypeinfo $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'typeName' => $dom->typeName,
+            'typeNamespace' => $dom->typeNamespace,
+        );
+
+        return $a;
+    }
+
+    public static function castDomError(\DOMDomError $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'severity' => $dom->severity,
+            'message' => $dom->message,
+            'type' => $dom->type,
+            'relatedException' => $dom->relatedException,
+            'related_data' => $dom->related_data,
+            'location' => $dom->location,
+        );
+
+        return $a;
+    }
+
+    public static function castLocator(\DOMLocator $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'lineNumber' => $dom->lineNumber,
+            'columnNumber' => $dom->columnNumber,
+            'offset' => $dom->offset,
+            'relatedNode' => $dom->relatedNode,
+            'uri' => $dom->uri,
+        );
+
+        return $a;
+    }
+
+    public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'name' => $dom->name,
+            'entities' => $dom->entities,
+            'notations' => $dom->notations,
+            'publicId' => $dom->publicId,
+            'systemId' => $dom->systemId,
+            'internalSubset' => $dom->internalSubset,
+        );
+
+        return $a;
+    }
+
+    public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'publicId' => $dom->publicId,
+            'systemId' => $dom->systemId,
+        );
+
+        return $a;
+    }
+
+    public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'publicId' => $dom->publicId,
+            'systemId' => $dom->systemId,
+            'notationName' => $dom->notationName,
+            'actualEncoding' => $dom->actualEncoding,
+            'encoding' => $dom->encoding,
+            'version' => $dom->version,
+        );
+
+        return $a;
+    }
+
+    public static function castProcessingInstruction(\DOMProcessingInstruction $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'target' => $dom->target,
+            'data' => $dom->data,
+        );
+
+        return $a;
+    }
+
+    public static function castXPath(\DOMXPath $dom, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            'document' => $dom->document,
+        );
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/DoctrineCaster.php b/src/Symfony/Component/VarDumper/Caster/DoctrineCaster.php
new file mode 100644
index 0000000000000..30d7ca6ef1a92
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/DoctrineCaster.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Doctrine\Common\Proxy\Proxy as CommonProxy;
+use Doctrine\ORM\Proxy\Proxy as OrmProxy;
+use Doctrine\ORM\PersistentCollection;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Doctrine related classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class DoctrineCaster
+{
+    public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, $isNested)
+    {
+        unset(
+            $a['__cloner__'],
+            $a['__initializer__']
+        );
+        $stub->cut += 2;
+
+        return $a;
+    }
+
+    public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, $isNested)
+    {
+        $prefix = "\0Doctrine\\ORM\\Proxy\\Proxy\0";
+        unset(
+            $a[$prefix.'_entityPersister'],
+            $a[$prefix.'_identifier']
+        );
+        $stub->cut += 2;
+
+        return $a;
+    }
+
+    public static function castPersistentCollection(PersistentCollection $coll, array $a, Stub $stub, $isNested)
+    {
+        $prefix = "\0Doctrine\\ORM\\PersistentCollection\0";
+
+        $a[$prefix.'snapshot'] = new CasterStub($a[$prefix.'snapshot']);
+        $a[$prefix.'association'] = new CasterStub($a[$prefix.'association']);
+        $a[$prefix.'typeClass'] = new CasterStub($a[$prefix.'typeClass']);
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php
new file mode 100644
index 0000000000000..120eae1d2a030
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php
@@ -0,0 +1,133 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Exception\ThrowingCasterException;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts common Exception classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class ExceptionCaster
+{
+    public static $traceArgs = true;
+    public static $errorTypes = array(
+        E_DEPRECATED => 'E_DEPRECATED',
+        E_USER_DEPRECATED => 'E_USER_DEPRECATED',
+        E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
+        E_ERROR => 'E_ERROR',
+        E_WARNING => 'E_WARNING',
+        E_PARSE => 'E_PARSE',
+        E_NOTICE => 'E_NOTICE',
+        E_CORE_ERROR => 'E_CORE_ERROR',
+        E_CORE_WARNING => 'E_CORE_WARNING',
+        E_COMPILE_ERROR => 'E_COMPILE_ERROR',
+        E_COMPILE_WARNING => 'E_COMPILE_WARNING',
+        E_USER_ERROR => 'E_USER_ERROR',
+        E_USER_WARNING => 'E_USER_WARNING',
+        E_USER_NOTICE => 'E_USER_NOTICE',
+        E_STRICT => 'E_STRICT',
+    );
+
+    public static function castException(\Exception $e, array $a, Stub $stub, $isNested)
+    {
+        $trace = $a["\0Exception\0trace"];
+        unset($a["\0Exception\0trace"]); // Ensures the trace is always last
+
+        static::filterTrace($trace, static::$traceArgs);
+
+        if (null !== $trace) {
+            $a["\0Exception\0trace"] = $trace;
+        }
+        if (empty($a["\0Exception\0previous"])) {
+            unset($a["\0Exception\0previous"]);
+        }
+        unset($a["\0Exception\0string"], $a["\0+\0xdebug_message"], $a["\0+\0__destructorException"]);
+
+        return $a;
+    }
+
+    public static function castErrorException(\ErrorException $e, array $a, Stub $stub, $isNested)
+    {
+        if (isset($a[$s = "\0*\0severity"], self::$errorTypes[$a[$s]])) {
+            $a[$s] = new CasterStub(self::$errorTypes[$a[$s]], 'const');
+        }
+
+        return $a;
+    }
+
+    public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, $isNested)
+    {
+        $b = (array) $a["\0Exception\0previous"];
+
+        array_splice($b["\0Exception\0trace"], count($a["\0Exception\0trace"]));
+
+        $t = static::$traceArgs;
+        static::$traceArgs = false;
+        $b = static::castException($a["\0Exception\0previous"], $b, $stub, $isNested);
+        static::$traceArgs = $t;
+
+        if (empty($a["\0*\0message"])) {
+            $a["\0*\0message"] = "Unexpected exception thrown from a caster: ".get_class($a["\0Exception\0previous"]);
+        }
+
+        if (isset($b["\0*\0message"])) {
+            $a["\0~\0message"] = $b["\0*\0message"];
+        }
+        if (isset($b["\0*\0file"])) {
+            $a["\0~\0file"] = $b["\0*\0file"];
+        }
+        if (isset($b["\0*\0line"])) {
+            $a["\0~\0line"] = $b["\0*\0line"];
+        }
+        if (isset($b["\0Exception\0trace"])) {
+            $a["\0~\0trace"] = $b["\0Exception\0trace"];
+        }
+
+        unset($a["\0Exception\0trace"], $a["\0Exception\0previous"], $a["\0*\0code"], $a["\0*\0file"], $a["\0*\0line"]);
+
+        return $a;
+    }
+
+    public static function filterTrace(&$trace, $dumpArgs, $offset = 0)
+    {
+        if (0 > $offset || empty($trace[$offset])) {
+            return $trace = null;
+        }
+
+        $t = $trace[$offset];
+
+        if (empty($t['class']) && isset($t['function'])) {
+            if ('user_error' === $t['function'] || 'trigger_error' === $t['function']) {
+                ++$offset;
+            }
+        }
+
+        if ($offset) {
+            array_splice($trace, 0, $offset);
+        }
+
+        foreach ($trace as &$t) {
+            $t = array(
+                'call' => (isset($t['class']) ? $t['class'].$t['type'] : '').$t['function'].'()',
+                'file' => isset($t['line']) ? "{$t['file']}:{$t['line']}" : '',
+                'args' => &$t['args'],
+            );
+
+            if (!isset($t['args']) || !$dumpArgs) {
+                unset($t['args']);
+            }
+        }
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/PdoCaster.php b/src/Symfony/Component/VarDumper/Caster/PdoCaster.php
new file mode 100644
index 0000000000000..90dac4708d5f3
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/PdoCaster.php
@@ -0,0 +1,114 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts PDO related classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class PdoCaster
+{
+    private static $pdoAttributes = array(
+        'CASE' => array(
+            \PDO::CASE_LOWER => 'LOWER',
+            \PDO::CASE_NATURAL => 'NATURAL',
+            \PDO::CASE_UPPER => 'UPPER',
+        ),
+        'ERRMODE' => array(
+            \PDO::ERRMODE_SILENT => 'SILENT',
+            \PDO::ERRMODE_WARNING => 'WARNING',
+            \PDO::ERRMODE_EXCEPTION => 'EXCEPTION',
+        ),
+        'TIMEOUT',
+        'PREFETCH',
+        'AUTOCOMMIT',
+        'PERSISTENT',
+        'DRIVER_NAME',
+        'SERVER_INFO',
+        'ORACLE_NULLS' => array(
+            \PDO::NULL_NATURAL => 'NATURAL',
+            \PDO::NULL_EMPTY_STRING => 'EMPTY_STRING',
+            \PDO::NULL_TO_STRING => 'TO_STRING',
+        ),
+        'CLIENT_VERSION',
+        'SERVER_VERSION',
+        'STATEMENT_CLASS',
+        'EMULATE_PREPARES',
+        'CONNECTION_STATUS',
+        'STRINGIFY_FETCHES',
+        'DEFAULT_FETCH_MODE' => array(
+            \PDO::FETCH_ASSOC => 'ASSOC',
+            \PDO::FETCH_BOTH => 'BOTH',
+            \PDO::FETCH_LAZY => 'LAZY',
+            \PDO::FETCH_NUM => 'NUM',
+            \PDO::FETCH_OBJ => 'OBJ',
+        ),
+    );
+
+    public static function castPdo(\PDO $c, array $a, Stub $stub, $isNested)
+    {
+        $a = array();
+        $errmode = $c->getAttribute(\PDO::ATTR_ERRMODE);
+        $c->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+
+        foreach (self::$pdoAttributes as $attr => $values) {
+            if (!isset($attr[0])) {
+                $attr = $values;
+                $values = array();
+            }
+
+            try {
+                $a[$attr] = 'ERRMODE' === $attr ? $errmode : $c->getAttribute(constant("PDO::ATTR_{$attr}"));
+                if (isset($values[$a[$attr]])) {
+                    $a[$attr] = new CasterStub($values[$a[$attr]], 'const');
+                }
+            } catch (\Exception $m) {
+            }
+        }
+
+        $m = "\0~\0";
+        $a = (array) $c + array(
+            $m.'inTransaction' => method_exists($c, 'inTransaction'),
+            $m.'errorInfo' => $c->errorInfo(),
+            $m.'attributes' => $a,
+        );
+
+        if ($a[$m.'inTransaction']) {
+            $a[$m.'inTransaction'] = $c->inTransaction();
+        } else {
+            unset($a[$m.'inTransaction']);
+        }
+
+        if (!isset($a[$m.'errorInfo'][1], $a[$m.'errorInfo'][2])) {
+            unset($a[$m.'errorInfo']);
+        }
+
+        $c->setAttribute(\PDO::ATTR_ERRMODE, $errmode);
+
+        return $a;
+    }
+
+    public static function castPdoStatement(\PDOStatement $c, array $a, Stub $stub, $isNested)
+    {
+        $m = "\0~\0";
+        $a[$m.'errorInfo'] = $c->errorInfo();
+
+        if (!isset($a[$m.'errorInfo'][1], $a[$m.'errorInfo'][2])) {
+            unset($a[$m.'errorInfo']);
+        }
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php
new file mode 100644
index 0000000000000..94a25a89e484d
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Reflector related classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class ReflectionCaster
+{
+    public static function castReflector(\Reflector $c, array $a, Stub $stub, $isNested)
+    {
+        $a["\0~\0reflection"] = $c->__toString();
+
+        return $a;
+    }
+
+    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'], $a["\0+\0this"], $a["\0+\0parameter"]);
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php
new file mode 100644
index 0000000000000..903641f69c636
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts common resource types to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class ResourceCaster
+{
+    public static function castCurl($h, array $a, Stub $stub, $isNested)
+    {
+        return curl_getinfo($h);
+    }
+
+    public static function castDba($dba, array $a, Stub $stub, $isNested)
+    {
+        $list = dba_list();
+        $a['file'] = $list[(int) $dba];
+
+        return $a;
+    }
+
+    public static function castProcess($process, array $a, Stub $stub, $isNested)
+    {
+        return proc_get_status($process);
+    }
+
+    public static function castStream($stream, array $a, Stub $stub, $isNested)
+    {
+        return stream_get_meta_data($stream) + static::castStreamContext($stream, $a, $stub, $isNested);
+    }
+
+    public static function castStreamContext($stream, array $a, Stub $stub, $isNested)
+    {
+        return stream_context_get_params($stream);
+    }
+
+    public static function castGd($gd, array $a, Stub $stub, $isNested)
+    {
+        $a['size'] = imagesx($gd).'x'.imagesy($gd);
+        $a['trueColor'] = imageistruecolor($gd);
+
+        return $a;
+    }
+
+    public static function castMysqlLink($h, array $a, Stub $stub, $isNested)
+    {
+        $a['host'] = mysql_get_host_info($h);
+        $a['protocol'] = mysql_get_proto_info($h);
+        $a['server'] = mysql_get_server_info($h);
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/SplCaster.php b/src/Symfony/Component/VarDumper/Caster/SplCaster.php
new file mode 100644
index 0000000000000..9b5377195ecbb
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/SplCaster.php
@@ -0,0 +1,110 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts SPL related classes to array representation.
+ *
+ * @author Nicolas Grekas 
+ */
+class SplCaster
+{
+    public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, $isNested)
+    {
+        $class = $stub->class;
+        $flags = $c->getFlags();
+
+        $b = array(
+            "\0~\0flag::STD_PROP_LIST" => (bool) ($flags & \ArrayObject::STD_PROP_LIST),
+            "\0~\0flag::ARRAY_AS_PROPS" => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS),
+            "\0~\0iteratorClass" => $c->getIteratorClass(),
+            "\0~\0storage" => $c->getArrayCopy(),
+        );
+
+        if ($class === 'ArrayObject') {
+            $a = $b;
+        } else {
+            if (!($flags & \ArrayObject::STD_PROP_LIST)) {
+                $c->setFlags(\ArrayObject::STD_PROP_LIST);
+
+                if ($a = (array) $c) {
+                    $class = new \ReflectionClass($class);
+                    foreach ($a as $k => $p) {
+                        if (!isset($k[0]) || ("\0" !== $k[0] && !$class->hasProperty($k))) {
+                            unset($a[$k]);
+                            $a["\0+\0".$k] = $p;
+                        }
+                    }
+                }
+
+                $c->setFlags($flags);
+            }
+
+            $a += $b;
+        }
+
+        return $a;
+    }
+
+    public static function castHeap(\Iterator $c, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            "\0~\0heap" => iterator_to_array(clone $c),
+        );
+
+        return $a;
+    }
+
+    public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, Stub $stub, $isNested)
+    {
+        $mode = $c->getIteratorMode();
+        $c->setIteratorMode(\SplDoublyLinkedList::IT_MODE_KEEP | $mode & ~\SplDoublyLinkedList::IT_MODE_DELETE);
+
+        $a += array(
+            "\0~\0mode" => new CasterStub((($mode & \SplDoublyLinkedList::IT_MODE_LIFO) ? 'IT_MODE_LIFO' : 'IT_MODE_FIFO').' | '.(($mode & \SplDoublyLinkedList::IT_MODE_KEEP) ? 'IT_MODE_KEEP' : 'IT_MODE_DELETE'), 'const'),
+            "\0~\0dllist" => iterator_to_array($c),
+        );
+        $c->setIteratorMode($mode);
+
+        return $a;
+    }
+
+    public static function castFixedArray(\SplFixedArray $c, array $a, Stub $stub, $isNested)
+    {
+        $a += array(
+            "\0~\0storage" => $c->toArray(),
+        );
+
+        return $a;
+    }
+
+    public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $stub, $isNested)
+    {
+        $storage = array();
+        unset($a["\0+\0\0gcdata"]); // Don't hit https://bugs.php.net/65967
+
+        foreach ($c as $obj) {
+            $storage[spl_object_hash($obj)] = array(
+                'object' => $obj,
+                'info' => $c->getInfo(),
+             );
+        }
+
+        $a += array(
+            "\0~\0storage" => $storage,
+        );
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Caster/StubCaster.php b/src/Symfony/Component/VarDumper/Caster/StubCaster.php
new file mode 100644
index 0000000000000..294a13c562c2b
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Caster/StubCaster.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts a CasterStub.
+ *
+ * @author Nicolas Grekas 
+ */
+class StubCaster
+{
+    public static function castStub(CasterStub $c, array $a, Stub $stub, $isNested)
+    {
+        if ($isNested) {
+            $stub->type = $c->type;
+            $stub->class = $c->class;
+            $stub->value = $c->value;
+            $stub->cut = $c->cut;
+
+            return array();
+        }
+    }
+
+    public static function castNestedFat($obj, array $a, Stub $stub, $isNested)
+    {
+        if ($isNested) {
+            $stub->cut += count($a);
+
+            return array();
+        }
+
+        return $a;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
new file mode 100644
index 0000000000000..ef94af979e94a
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
@@ -0,0 +1,292 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+use Symfony\Component\VarDumper\Exception\ThrowingCasterException;
+
+/**
+ * AbstractCloner implements a generic caster mechanism for objects and resources.
+ *
+ * @author Nicolas Grekas 
+ */
+abstract class AbstractCloner implements ClonerInterface
+{
+    public static $defaultCasters = array(
+        'Symfony\Component\VarDumper\Caster\CasterStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub',
+
+        'Closure'        => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClosure',
+        'Reflector'      => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castReflector',
+
+        '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',
+
+        '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',
+
+        'ErrorException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castErrorException',
+        'Exception'      => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castException',
+        'Symfony\Component\DependencyInjection\ContainerInterface'
+                           => 'Symfony\Component\VarDumper\Caster\StubCaster::castNestedFat',
+        'Symfony\Component\VarDumper\Exception\ThrowingCasterException'
+                           => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castThrowingCasterException',
+
+        'PDO'            => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdo',
+        'PDOStatement'   => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdoStatement',
+
+        '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',
+
+        ':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;
+    protected $maxString = -1;
+
+    private $casters = array();
+    private $data = array(array(null));
+    private $prevErrorHandler;
+    private $classInfo = array();
+
+    /**
+     * @param callable[]|null $casters A map of casters.
+     *
+     * @see addCasters
+     */
+    public function __construct(array $casters = null)
+    {
+        if (null === $casters) {
+            $casters = static::$defaultCasters;
+        }
+        $this->addCasters($casters);
+    }
+
+    /**
+     * Adds casters for resources and objects.
+     *
+     * Maps resources or objects types to a callback.
+     * Types are in the key, with a callable caster for value.
+     * Resource types are to be prefixed with a `:`,
+     * see e.g. static::$defaultCasters.
+     *
+     * @param callable[] $casters A map of casters.
+     */
+    public function addCasters(array $casters)
+    {
+        foreach ($casters as $type => $callback) {
+            $this->casters[strtolower($type)][] = $callback;
+        }
+    }
+
+    /**
+     * Sets the maximum number of items to clone past the first level in nested structures.
+     *
+     * @param int $maxItems
+     */
+    public function setMaxItems($maxItems)
+    {
+        $this->maxItems = (int) $maxItems;
+    }
+
+    /**
+     * Sets the maximum cloned length for strings.
+     *
+     * @param int $maxString
+     */
+    public function setMaxString($maxString)
+    {
+        $this->maxString = (int) $maxString;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function cloneVar($var)
+    {
+        $this->prevErrorHandler = set_error_handler(array($this, 'handleError'));
+        try {
+            if (!function_exists('iconv')) {
+                $this->maxString = -1;
+            }
+            $data = $this->doClone($var);
+        } catch (\Exception $e) {
+        }
+        restore_error_handler();
+        $this->prevErrorHandler = null;
+
+        if (isset($e)) {
+            throw $e;
+        }
+
+        return new Data($data);
+    }
+
+    /**
+     * Effectively clones the PHP variable.
+     *
+     * @param mixed $var Any PHP variable.
+     *
+     * @return array The cloned variable represented in an array.
+     */
+    abstract protected function doClone($var);
+
+    /**
+     * Casts an object to an array representation.
+     *
+     * @param object $obj      The object itself.
+     * @param Stub   $stub     The Stub for the casted object.
+     * @param bool   $isNested True if the object is nested in the dumped structure.
+     *
+     * @return array The object casted as array.
+     */
+    protected function castObject($obj, Stub $stub, $isNested)
+    {
+        $class = $stub->class;
+
+        if (isset($this->classInfo[$class])) {
+            $classInfo = $this->classInfo[$class];
+            $stub->class = $classInfo[0];
+        } else {
+            $classInfo = array(
+                $class,
+                method_exists($class, '__debugInfo'),
+                new \ReflectionClass($class),
+                array_reverse(array($class => $class) + class_parents($class) + class_implements($class)),
+            );
+
+            $this->classInfo[$class] = $classInfo;
+        }
+
+        if ($classInfo[1]) {
+            $a = $this->callCaster(function ($obj) {return $obj->__debugInfo();}, $obj, array(), null, $isNested);
+        } else {
+            $a = (array) $obj;
+        }
+
+        foreach ($a as $k => $p) {
+            if (!isset($k[0]) || ("\0" !== $k[0] && !$classInfo[2]->hasProperty($k))) {
+                unset($a[$k]);
+                $a["\0+\0".$k] = $p;
+            }
+        }
+
+        foreach ($classInfo[3] as $p) {
+            if (!empty($this->casters[$p = strtolower($p)])) {
+                foreach ($this->casters[$p] as $p) {
+                    $a = $this->callCaster($p, $obj, $a, $stub, $isNested);
+                }
+            }
+        }
+
+        return $a;
+    }
+
+    /**
+     * Casts a resource to an array representation.
+     *
+     * @param resource $res      The resource.
+     * @param Stub     $stub     The Stub for the casted resource.
+     * @param bool     $isNested True if the object is nested in the dumped structure.
+     *
+     * @return array The resource casted as array.
+     */
+    protected function castResource($res, Stub $stub, $isNested)
+    {
+        $a = array();
+        $type = $stub->class;
+
+        if (!empty($this->casters[':'.$type])) {
+            foreach ($this->casters[':'.$type] as $c) {
+                $a = $this->callCaster($c, $res, $a, $stub, $isNested);
+            }
+        }
+
+        return $a;
+    }
+
+    /**
+     * Calls a custom caster.
+     *
+     * @param callable        $callback The caster.
+     * @param object|resource $obj      The object/resource being casted.
+     * @param array           $a        The result of the previous cast for chained casters.
+     * @param Stub            $stub     The Stub for the casted object/resource.
+     * @param bool            $isNested True if $obj is nested in the dumped structure.
+     *
+     * @return array The casted object/resource.
+     */
+    private function callCaster($callback, $obj, $a, $stub, $isNested)
+    {
+        try {
+            $cast = call_user_func($callback, $obj, $a, $stub, $isNested);
+
+            if (is_array($cast)) {
+                $a = $cast;
+            }
+        } catch (\Exception $e) {
+            $a["\0~\0⚠"] = new ThrowingCasterException($callback, $e);
+        }
+
+        return $a;
+    }
+
+    /**
+     * Special handling for errors: cloning must be fail-safe.
+     *
+     * @internal
+     */
+    public function handleError($type, $msg, $file, $line, $context)
+    {
+        if (E_RECOVERABLE_ERROR === $type || E_USER_ERROR === $type) {
+            // Cloner never dies
+            throw new \ErrorException($msg, 0, $type, $file, $line);
+        }
+
+        if ($this->prevErrorHandler) {
+            return call_user_func($this->prevErrorHandler, $type, $msg, $file, $line, $context);
+        }
+
+        return false;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/ClonerInterface.php b/src/Symfony/Component/VarDumper/Cloner/ClonerInterface.php
new file mode 100644
index 0000000000000..c1df5933dbf2e
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/ClonerInterface.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas 
+ */
+interface ClonerInterface
+{
+    /**
+     * Clones a PHP variable.
+     *
+     * @param mixed $var Any PHP variable.
+     *
+     * @return Data The cloned variable represented by a Data object.
+     */
+    public function cloneVar($var);
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/Cursor.php b/src/Symfony/Component/VarDumper/Cloner/Cursor.php
new file mode 100644
index 0000000000000..50266ea52ab3e
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/Cursor.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * Represents the current state of a dumper while dumping.
+ *
+ * @author Nicolas Grekas 
+ */
+class Cursor
+{
+    const HASH_INDEXED = Stub::ARRAY_INDEXED;
+    const HASH_ASSOC = Stub::ARRAY_ASSOC;
+    const HASH_OBJECT = Stub::TYPE_OBJECT;
+    const HASH_RESOURCE = Stub::TYPE_RESOURCE;
+
+    public $depth = 0;
+    public $refIndex = false;
+    public $softRefTo = false;
+    public $hardRefTo = false;
+    public $hashType;
+    public $hashKey;
+    public $hashIndex = 0;
+    public $hashLength = 0;
+    public $hashCut = 0;
+    public $stop = false;
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/Data.php b/src/Symfony/Component/VarDumper/Cloner/Data.php
new file mode 100644
index 0000000000000..3394cf796fd5c
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/Data.php
@@ -0,0 +1,220 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas 
+ */
+class Data
+{
+    private $data;
+    private $maxDepth = -1;
+    private $maxItemsPerDepth = -1;
+
+    /**
+     * @param array $data A array as returned by ClonerInterface::cloneVar().
+     */
+    public function __construct(array $data)
+    {
+        $this->data = $data;
+    }
+
+    /**
+     * @return array The raw data structure.
+     */
+    public function getRawData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Returns a depth limited clone of $this.
+     *
+     * @param int $maxDepth         The max dumped depth level.
+     * @param int $maxItemsPerDepth The max number of items dumped per depth level.
+     *
+     * @return self A depth limited clone of $this.
+     */
+    public function getLimitedClone($maxDepth, $maxItemsPerDepth)
+    {
+        $data = clone $this;
+        $data->maxDepth = (int) $maxDepth;
+        $data->maxItemsPerDepth = (int) $maxItemsPerDepth;
+
+        return $data;
+    }
+
+    /**
+     * Dumps data with a DumperInterface dumper.
+     */
+    public function dump(DumperInterface $dumper)
+    {
+        $refs = array(0);
+        $this->dumpItem($dumper, new Cursor, $refs, $this->data[0][0]);
+    }
+
+    /**
+     * Depth-first dumping of items.
+     *
+     * @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)
+    {
+        $cursor->refIndex = $cursor->softRefTo = $cursor->hardRefTo = false;
+
+        if (!$item instanceof Stub) {
+            $type = gettype($item);
+        } elseif (Stub::TYPE_REF === $item->type) {
+            if ($item->ref) {
+                if (isset($refs[$r = $item->ref])) {
+                    $cursor->hardRefTo = $refs[$r];
+                } else {
+                    $cursor->refIndex = $refs[$r] = ++$refs[0];
+                }
+            }
+            $type = $item->class ?: gettype($item->value);
+            $item = $item->value;
+        }
+        if ($item instanceof Stub) {
+            if ($item->ref) {
+                if (isset($refs[$r = $item->ref])) {
+                    if (Stub::TYPE_ARRAY === $item->type) {
+                        if (false === $cursor->hardRefTo) {
+                            $cursor->hardRefTo = $refs[$r];
+                        }
+                    } elseif (false === $cursor->softRefTo) {
+                        $cursor->softRefTo = $refs[$r];
+                    }
+                } elseif (false !== $cursor->refIndex) {
+                    $refs[$r] = $cursor->refIndex;
+                } else {
+                    $cursor->refIndex = $refs[$r] = ++$refs[0];
+                }
+            }
+            $cut = $item->cut;
+
+            if ($item->position && false === $cursor->softRefTo && false === $cursor->hardRefTo) {
+                $children = $this->data[$item->position];
+
+                if ($cursor->stop) {
+                    if ($cut >= 0) {
+                        $cut += count($children);
+                    }
+                    $children = array();
+                }
+            } else {
+                $children = array();
+            }
+            switch ($item->type) {
+                case Stub::TYPE_STRING:
+                    $dumper->dumpString($cursor, $item->value, Stub::STRING_BINARY === $item->class, $cut);
+                    break;
+
+                case Stub::TYPE_ARRAY:
+                    $dumper->enterArray($cursor, $item->value, Stub::ARRAY_INDEXED === $item->class, (bool) $children);
+                    $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, $item->class);
+                    $dumper->leaveArray($cursor, $item->value, Stub::ARRAY_INDEXED === $item->class, (bool) $children, $cut);
+                    break;
+
+                case Stub::TYPE_OBJECT:
+                    $dumper->enterObject($cursor, $item->class, (bool) $children);
+                    $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, Cursor::HASH_OBJECT);
+                    $dumper->leaveObject($cursor, $item->class, (bool) $children, $cut);
+                    break;
+
+                case Stub::TYPE_RESOURCE:
+                    $dumper->enterResource($cursor, $item->class, (bool) $children);
+                    $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, Cursor::HASH_RESOURCE);
+                    $dumper->leaveResource($cursor, $item->class, (bool) $children, $cut);
+                    break;
+
+                default:
+                    throw new \RuntimeException(sprintf('Unexpected Stub type: %s', $item->type));
+            }
+        } elseif ('array' === $type) {
+            $dumper->enterArray($cursor, 0, true, 0, 0);
+            $dumper->leaveArray($cursor, 0, true, 0, 0);
+        } else {
+            $dumper->dumpScalar($cursor, $type, $item);
+        }
+    }
+
+    /**
+     * Dumps children of hash structures.
+     *
+     * @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.
+     */
+    private function dumpChildren($dumper, $parentCursor, &$refs, $children, $hashCut, $hashType)
+    {
+        if ($children) {
+            if ($parentCursor->depth !== $this->maxDepth && $this->maxItemsPerDepth) {
+                $cursor = clone $parentCursor;
+                ++$cursor->depth;
+                $cursor->hashType = $hashType;
+                $cursor->hashIndex = 0;
+                $cursor->hashLength = count($children);
+                $cursor->hashCut = $hashCut;
+                foreach ($children as $cursor->hashKey => $child) {
+                    $this->dumpItem($dumper, $cursor, $refs, $child);
+                    if (++$cursor->hashIndex === $this->maxItemsPerDepth || $cursor->stop) {
+                        $parentCursor->stop = true;
+
+                        return $hashCut >= 0 ? $hashCut + $cursor->hashLength - $cursor->hashIndex : $hashCut;
+                    }
+                }
+            } elseif ($hashCut >= 0) {
+                $hashCut += count($children);
+            }
+        }
+
+        return $hashCut;
+    }
+
+    /**
+     * Portable variant of utf8_encode()
+     *
+     * @param string $s
+     *
+     * @return string
+     *
+     * @internal
+     */
+    public static function utf8Encode($s)
+    {
+        if (function_exists('iconv')) {
+            return iconv('CP1252', 'UTF-8', $s);
+        }
+
+        $s .= $s;
+        $len = strlen($s);
+
+        for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
+            switch (true) {
+                case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
+                case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
+                default: $s[$j] = "\xC3"; $s[++$j] = chr(ord($s[$i]) - 64); break;
+            }
+        }
+
+        return substr($s, 0, $j);
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/DumperInterface.php b/src/Symfony/Component/VarDumper/Cloner/DumperInterface.php
new file mode 100644
index 0000000000000..d910834ddf96b
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/DumperInterface.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * DumperInterface used by Data objects.
+ *
+ * @author Nicolas Grekas 
+ */
+interface DumperInterface
+{
+    /**
+     * Dumps a scalar value.
+     *
+     * @param Cursor $cursor The Cursor position in the dump.
+     * @param string $type   The PHP type of the value being dumped.
+     * @param scalar $value  The scalar value being dumped.
+     */
+    public function dumpScalar(Cursor $cursor, $type, $value);
+
+    /**
+     * Dumps a string.
+     *
+     * @param Cursor $cursor The Cursor position in the dump.
+     * @param string $str    The string being dumped.
+     * @param bool   $bin    Whether $str is UTF-8 or binary encoded.
+     * @param int    $cut    The number of characters $str has been cut by.
+     */
+    public function dumpString(Cursor $cursor, $str, $bin, $cut);
+
+    /**
+     * Dumps while entering an array.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param int    $count    The number of items in the original array.
+     * @param bool   $indexed  When the array is indexed or associative.
+     * @param bool   $hasChild When the dump of the array has child item.
+     */
+    public function enterArray(Cursor $cursor, $count, $indexed, $hasChild);
+
+    /**
+     * Dumps while leaving an array.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param int    $count    The number of items in the original array.
+     * @param bool   $indexed  Whether the array is indexed or associative.
+     * @param bool   $hasChild When the dump of the array has child item.
+     * @param int    $cut      The number of items the array has been cut by.
+     */
+    public function leaveArray(Cursor $cursor, $count, $indexed, $hasChild, $cut);
+
+    /**
+     * Dumps while entering an object.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $class    The class of the object.
+     * @param bool   $hasChild When the dump of the object has child item.
+     */
+    public function enterObject(Cursor $cursor, $class, $hasChild);
+
+    /**
+     * Dumps while leaving an object.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $class    The class of the object.
+     * @param bool   $hasChild When the dump of the object has child item.
+     * @param int    $cut      The number of items the object has been cut by.
+     */
+    public function leaveObject(Cursor $cursor, $class, $hasChild, $cut);
+
+    /**
+     * Dumps while entering a resource.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $res      The resource type.
+     * @param bool   $hasChild When the dump of the resource has child item.
+     */
+    public function enterResource(Cursor $cursor, $res, $hasChild);
+
+    /**
+     * Dumps while leaving a resource.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $res      The resource type.
+     * @param bool   $hasChild When the dump of the resource has child item.
+     * @param int    $cut      The number of items the resource has been cut by.
+     */
+    public function leaveResource(Cursor $cursor, $res, $hasChild, $cut);
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/ExtCloner.php b/src/Symfony/Component/VarDumper/Cloner/ExtCloner.php
new file mode 100644
index 0000000000000..9aefcc9f4aa3b
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/ExtCloner.php
@@ -0,0 +1,195 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas 
+ */
+class ExtCloner extends AbstractCloner
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function doClone($var)
+    {
+        $i = 0;                         // Current iteration position in $queue
+        $len = 1;                       // Length of $queue
+        $pos = 0;                       // Number of cloned items past the first level
+        $refs = 0;                      // Number of hard+soft references in $var
+        $queue = array(array($var));    // This breadth-first queue is the return value
+        $arrayRefs = array();           // Map of queue indexes to stub array objects
+        $hardRefs = array();            // Map of original zval hashes to stub objects
+        $softRefs = array();            // Map of original object hashes to their stub object couterpart
+        $maxItems = $this->maxItems;
+        $maxString = $this->maxString;
+        $a = null;                      // Array cast for nested structures
+        $stub = null;                   // Stub capturing the main properties of an original item value,
+                                        // or null if the original value is used directly
+
+        for ($i = 0; $i < $len; ++$i) {
+            $indexed = true;            // Whether the currently iterated array is numerically indexed or not
+            $j = -1;                    // Position in the currently iterated array
+            $step = $queue[$i];         // Copy of the currently iterated array used for hard references detection
+            foreach ($step as $k => $v) {
+                // $k is the original key
+                // $v is the original value or a stub object in case of hard references
+                if ($indexed && $k !== ++$j) {
+                    $indexed = false;
+                }
+                $zval = symfony_zval_info($k, $step);
+                if ($zval['zval_isref']) {
+                    $queue[$i][$k] =& $stub;    // Break hard references to make $queue completely
+                    unset($stub);               // independent from the original structure
+                    if (isset($hardRefs[$h = $zval['zval_hash']])) {
+                        $hardRefs[$h]->ref = ++$refs;
+                        $queue[$i][$k] = $hardRefs[$h];
+                        continue;
+                    }
+                }
+                // Create $stub when the original value $v can not be used directly
+                // If $v is a nested structure, put that structure in array $a
+                switch ($zval['type']) {
+                    case 'string':
+                        if (isset($v[0]) && !preg_match('//u', $v)) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_STRING;
+                            $stub->class = Stub::STRING_BINARY;
+                            if (0 <= $maxString && 0 < $cut = strlen($v) - $maxString) {
+                                $stub->cut = $cut;
+                                $v = substr_replace($v, '', -$cut);
+                            }
+                            $stub->value = Data::utf8Encode($v);
+                        } elseif (0 <= $maxString && isset($v[1+($maxString>>2)]) && 0 < $cut = iconv_strlen($v, 'UTF-8') - $maxString) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_STRING;
+                            $stub->class = Stub::STRING_UTF8;
+                            $stub->cut = $cut;
+                            $stub->value = iconv_substr($v, 0, $maxString, 'UTF-8');
+                        }
+                        break;
+
+                    case 'integer':
+                        break;
+
+                    case 'array':
+                        if ($v) {
+                            $stub = $arrayRefs[$len] = new Stub();
+                            $stub->type = Stub::TYPE_ARRAY;
+                            $stub->class = Stub::ARRAY_ASSOC;
+                            $stub->value = $zval['array_count'];
+                            $a = $v;
+                        }
+                        break;
+
+                    case 'object':
+                        if (empty($softRefs[$h = $zval['object_hash']])) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_OBJECT;
+                            $stub->class = $zval['object_class'];
+                            $stub->value = $h;
+                            $a = $this->castObject($v, $stub, 0 < $i);
+                            if (Stub::TYPE_OBJECT !== $stub->type) {
+                                break;
+                            }
+                            $h = $stub->value;
+                            $stub->value = '';
+                            if (0 <= $maxItems && $maxItems <= $pos) {
+                                $stub->cut = count($a);
+                                $a = array();
+                            }
+                        }
+                        if (empty($softRefs[$h])) {
+                            $softRefs[$h] = $stub;
+                        } else {
+                            $stub = $softRefs[$h];
+                            $stub->ref = ++$refs;
+                        }
+                        break;
+
+                    case 'resource':
+                        if (empty($softRefs[$h = (int) $v])) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_RESOURCE;
+                            $stub->class = $zval['resource_type'];
+                            $stub->value = $h;
+                            $a = $this->castResource($v, $stub, 0 < $i);
+                            if (Stub::TYPE_RESOURCE !== $stub->type) {
+                                break;
+                            }
+                            $h = $stub->value;
+                            $stub->value = '';
+                            if (0 <= $maxItems && $maxItems <= $pos) {
+                                $stub->cut = count($a);
+                                $a = array();
+                            }
+                        }
+                        if (empty($softRefs[$h])) {
+                            $softRefs[$h] = $stub;
+                        } else {
+                            $stub = $softRefs[$h];
+                            $stub->ref = ++$refs;
+                        }
+                        break;
+                }
+
+                if (isset($stub)) {
+                    if ($zval['zval_isref']) {
+                        if (Stub::TYPE_ARRAY === $stub->type) {
+                            $queue[$i][$k] = $hardRefs[$zval['zval_hash']] = $stub;
+                        } else {
+                            $queue[$i][$k] = $hardRefs[$zval['zval_hash']] = $v = new Stub();
+                            $v->value = $stub;
+                        }
+                    } else {
+                        $queue[$i][$k] = $stub;
+                    }
+
+                    if ($a) {
+                        if ($i && 0 <= $maxItems) {
+                            $k = count($a);
+                            if ($pos < $maxItems) {
+                                if ($maxItems < $pos += $k) {
+                                    $a = array_slice($a, 0, $maxItems - $pos);
+                                    if ($stub->cut >= 0) {
+                                        $stub->cut += $pos - $maxItems;
+                                    }
+                                }
+                            } else {
+                                if ($stub->cut >= 0) {
+                                    $stub->cut += $k;
+                                }
+                                $stub = $a = null;
+                                unset($arrayRefs[$len]);
+                                continue;
+                            }
+                        }
+                        $queue[$len] = $a;
+                        $stub->position = $len++;
+                    }
+                    $stub = $a = null;
+                } elseif ($zval['zval_isref']) {
+                    $queue[$i][$k] = $hardRefs[$zval['zval_hash']] = new Stub();
+                    $queue[$i][$k]->value = $v;
+                }
+            }
+
+            if (isset($arrayRefs[$i])) {
+                if ($indexed) {
+                    $arrayRefs[$i]->class = Stub::ARRAY_INDEXED;
+                }
+                unset($arrayRefs[$i]);
+            }
+        }
+
+        return $queue;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/PhpCloner.php b/src/Symfony/Component/VarDumper/Cloner/PhpCloner.php
new file mode 100644
index 0000000000000..46fa6b0f0cbc3
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/PhpCloner.php
@@ -0,0 +1,214 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas 
+ */
+class PhpCloner extends AbstractCloner
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function doClone($var)
+    {
+        $i = 0;                         // Current iteration position in $queue
+        $len = 1;                       // Length of $queue
+        $pos = 0;                       // Number of cloned items past the first level
+        $refs = 0;                      // Number of hard+soft references in $var
+        $queue = array(array($var));    // This breadth-first queue is the return value
+        $arrayRefs = array();           // Map of queue indexes to stub array objects
+        $hardRefs = array();            // By-ref map of stub objects' hashes to original hard `&` references
+        $softRefs = array();            // Map of original object hashes to their stub object couterpart
+        $values = array();              // Map of stub objects' hashes to original values
+        $maxItems = $this->maxItems;
+        $maxString = $this->maxString;
+        $cookie = (object) array();     // Unique object used to detect hard references
+        $isRef = false;
+        $a = null;                      // Array cast for nested structures
+        $stub = null;                   // Stub capturing the main properties of an original item value,
+                                        // or null if the original value is used directly
+
+        for ($i = 0; $i < $len; ++$i) {
+            $indexed = true;            // Whether the currently iterated array is numerically indexed or not
+            $j = -1;                    // Position in the currently iterated array
+            $step = $queue[$i];         // Copy of the currently iterated array used for hard references detection
+            foreach ($step as $k => $v) {
+                // $k is the original key
+                // $v is the original value or a stub object in case of hard references
+                if ($indexed && $k !== ++$j) {
+                    $indexed = false;
+                }
+                $step[$k] = $cookie;
+                if ($queue[$i][$k] === $cookie) {
+                    $queue[$i][$k] =& $stub;    // Break hard references to make $queue completely
+                    unset($stub);               // independent from the original structure
+                    if ($v instanceof Stub && isset($hardRefs[spl_object_hash($v)])) {
+                        $v->ref = ++$refs;
+                        $step[$k] = $queue[$i][$k] = $v;
+                        continue;
+                    }
+                    $isRef = true;
+                }
+                // Create $stub when the original value $v can not be used directly
+                // If $v is a nested structure, put that structure in array $a
+                switch (gettype($v)) {
+                    case 'string':
+                        if (isset($v[0]) && !preg_match('//u', $v)) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_STRING;
+                            $stub->class = Stub::STRING_BINARY;
+                            if (0 <= $maxString && 0 < $cut = strlen($v) - $maxString) {
+                                $stub->cut = $cut;
+                                $cut = substr_replace($v, '', -$cut);
+                            } else {
+                                $cut = $v;
+                            }
+                            $stub->value = Data::utf8Encode($cut);
+                        } elseif (0 <= $maxString && isset($v[1+($maxString>>2)]) && 0 < $cut = iconv_strlen($v, 'UTF-8') - $maxString) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_STRING;
+                            $stub->class = Stub::STRING_UTF8;
+                            $stub->cut = $cut;
+                            $stub->value = iconv_substr($v, 0, $maxString, 'UTF-8');
+                        }
+                        break;
+
+                    case 'integer':
+                        break;
+
+                    case 'array':
+                        if ($v) {
+                            $stub = $arrayRefs[$len] = new Stub();
+                            $stub->type = Stub::TYPE_ARRAY;
+                            $stub->class = Stub::ARRAY_ASSOC;
+                            $stub->value = count($v);
+                            $a = $v;
+                        }
+                        break;
+
+                    case 'object':
+                        if (empty($softRefs[$h = spl_object_hash($v)])) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_OBJECT;
+                            $stub->class = get_class($v);
+                            $stub->value = $h;
+                            $a = $this->castObject($v, $stub, 0 < $i);
+                            if (Stub::TYPE_OBJECT !== $stub->type) {
+                                break;
+                            }
+                            $h = $stub->value;
+                            $stub->value = '';
+                            if (0 <= $maxItems && $maxItems <= $pos) {
+                                $stub->cut = count($a);
+                                $a = array();
+                            }
+                        }
+                        if (empty($softRefs[$h])) {
+                            $softRefs[$h] = $stub;
+                        } else {
+                            $stub = $softRefs[$h];
+                            $stub->ref = ++$refs;
+                        }
+                        break;
+
+                    case 'resource':
+                    case 'unknown type':
+                        if (empty($softRefs[$h = (int) $v])) {
+                            $stub = new Stub();
+                            $stub->type = Stub::TYPE_RESOURCE;
+                            $stub->class = get_resource_type($v);
+                            $stub->value = $h;
+                            $a = $this->castResource($v, $stub, 0 < $i);
+                            if (Stub::TYPE_RESOURCE !== $stub->type) {
+                                break;
+                            }
+                            $h = $stub->value;
+                            $stub->value = '';
+                            if (0 <= $maxItems && $maxItems <= $pos) {
+                                $stub->cut = count($a);
+                                $a = array();
+                            }
+                        }
+                        if (empty($softRefs[$h])) {
+                            $softRefs[$h] = $stub;
+                        } else {
+                            $stub = $softRefs[$h];
+                            $stub->ref = ++$refs;
+                        }
+                        break;
+                }
+
+                if (isset($stub)) {
+                    if ($isRef) {
+                        if (Stub::TYPE_ARRAY === $stub->type) {
+                            $step[$k] = $stub;
+                        } else {
+                            $step[$k] = new Stub();
+                            $step[$k]->value = $stub;
+                        }
+                        $h = spl_object_hash($step[$k]);
+                        $queue[$i][$k] = $hardRefs[$h] =& $step[$k];
+                        $values[$h] = $v;
+                        $isRef = false;
+                    } else {
+                        $queue[$i][$k] = $stub;
+                    }
+
+                    if ($a) {
+                        if ($i && 0 <= $maxItems) {
+                            $k = count($a);
+                            if ($pos < $maxItems) {
+                                if ($maxItems < $pos += $k) {
+                                    $a = array_slice($a, 0, $maxItems - $pos);
+                                    if ($stub->cut >= 0) {
+                                        $stub->cut += $pos - $maxItems;
+                                    }
+                                }
+                            } else {
+                                if ($stub->cut >= 0) {
+                                    $stub->cut += $k;
+                                }
+                                $stub = $a = null;
+                                unset($arrayRefs[$len]);
+                                continue;
+                            }
+                        }
+                        $queue[$len] = $a;
+                        $stub->position = $len++;
+                    }
+                    $stub = $a = null;
+                } elseif ($isRef) {
+                    $step[$k] = $queue[$i][$k] = new Stub();
+                    $step[$k]->value = $v;
+                    $h = spl_object_hash($step[$k]);
+                    $hardRefs[$h] =& $step[$k];
+                    $values[$h] = $v;
+                    $isRef = false;
+                }
+            }
+
+            if (isset($arrayRefs[$i])) {
+                if ($indexed) {
+                    $arrayRefs[$i]->class = Stub::ARRAY_INDEXED;
+                }
+                unset($arrayRefs[$i]);
+            }
+        }
+
+        foreach ($values as $h => $v) {
+            $hardRefs[$h] = $v;
+        }
+
+        return $queue;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Cloner/Stub.php b/src/Symfony/Component/VarDumper/Cloner/Stub.php
new file mode 100644
index 0000000000000..9400a1e077d11
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Cloner/Stub.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * Represents the main properties of a PHP variable.
+ *
+ * @author Nicolas Grekas 
+ */
+class Stub
+{
+    const TYPE_REF = 'ref';
+    const TYPE_STRING = 'string';
+    const TYPE_ARRAY = 'array';
+    const TYPE_OBJECT = 'object';
+    const TYPE_RESOURCE = 'resource';
+
+    const STRING_BINARY = 'bin';
+    const STRING_UTF8 = 'utf8';
+
+    const ARRAY_ASSOC = 'assoc';
+    const ARRAY_INDEXED = 'indexed';
+
+    public $type = self::TYPE_REF;
+    public $class = '';
+    public $value;
+    public $cut = 0;
+    public $ref = 0;
+    public $position = 0;
+}
diff --git a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php
new file mode 100644
index 0000000000000..4b99c1f71ba8c
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php
@@ -0,0 +1,132 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+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 
+ */
+abstract class AbstractDumper implements DataDumperInterface, DumperInterface
+{
+    public static $defaultOutputStream = 'php://output';
+
+    protected $line = '';
+    protected $lineDumper;
+    protected $outputStream;
+    protected $decimalPoint; // This is locale dependent
+    protected $indentPad = '  ';
+
+    /**
+     * @param callable|resource|string|null $outputStream A line dumper callable, an opened stream or an output path, defaults to static::$defaultOutputStream.
+     */
+    public function __construct($outputStream = null)
+    {
+        $this->decimalPoint = (string) 0.5;
+        $this->decimalPoint = $this->decimalPoint[1];
+        if (is_callable($outputStream)) {
+            $this->setLineDumper($outputStream);
+        } else {
+            if (null === $outputStream) {
+                $outputStream =& static::$defaultOutputStream;
+            }
+            if (is_string($outputStream)) {
+                $outputStream = fopen($outputStream, 'wb');
+            }
+            $this->outputStream = $outputStream;
+            $this->setLineDumper(array($this, 'echoLine'));
+        }
+    }
+
+    /**
+     * Sets a line dumper callback.
+     *
+     * @param callable $callback A callback responsible for writing the dump, one line at a time.
+     *
+     * @return callable|null The previous line dumper.
+     */
+    public function setLineDumper($callback)
+    {
+        $prev = $this->lineDumper;
+        $this->lineDumper = $callback;
+
+        return $prev;
+    }
+
+    /**
+     * Sets the indentation pad string.
+     *
+     * @param string $pad A string the will be prepended to dumped lines, repeated by nesting level.
+     *
+     * @return string The indent pad.
+     */
+    public function setIndentPad($pad)
+    {
+        $prev = $this->indentPad;
+        $this->indentPad = $pad;
+
+        return $prev;
+    }
+
+    /**
+     * Dumps a Data object.
+     *
+     * @param Data          $data       A Data object.
+     * @param callable|null $lineDumper A callback for writing dump's lines.
+     */
+    public function dump(Data $data, $lineDumper = null)
+    {
+        $exception = null;
+        if ($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;
+        }
+    }
+
+    /**
+     * Dumps the current line.
+     *
+     * @param int $depth The recursive depth in the dumped structure for the line being dumped.
+     */
+    protected function dumpLine($depth)
+    {
+        call_user_func($this->lineDumper, $this->line, $depth);
+        $this->line = '';
+    }
+
+    /**
+     * Generic line dumper callback.
+     *
+     * @param string $line  The line to write.
+     * @param int    $depth The recursive depth in the dumped structure.
+     */
+    protected function echoLine($line, $depth)
+    {
+        if (-1 !== $depth) {
+            fwrite($this->outputStream, str_repeat($this->indentPad, $depth).$line."\n");
+        }
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php
new file mode 100644
index 0000000000000..87680de5984c2
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php
@@ -0,0 +1,430 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Cloner\Cursor;
+
+/**
+ * CliDumper dumps variables for command line output.
+ *
+ * @author Nicolas Grekas 
+ */
+class CliDumper extends AbstractDumper
+{
+    public static $defaultColors;
+    public static $defaultOutputStream = 'php://stdout';
+
+    protected $colors;
+    protected $maxStringWidth = 0;
+    protected $styles = array(
+        // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
+        'num'       => '1;38;5;33',
+        'const'     => '1;38;5;33',
+        'str'       => '1;38;5;37',
+        'cchr'      => '7',
+        'note'      => '38;5;178',
+        'ref'       => '38;5;245',
+        'public'    => '38;5;28',
+        'protected' => '38;5;166',
+        'private'   => '38;5;160',
+        'meta'      => '38;5;27',
+    );
+
+    protected static $controlChars = array(
+        "\x1B", // ESC must be the first
+        "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
+        "\x08", "\x09", "\x0A", "\x0B", "\x0C", "\x0D", "\x0E", "\x0F",
+        "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
+        "\x18", "\x19", "\x1A", "\x1C", "\x1D", "\x1E", "\x1F", "\x7F",
+    );
+
+    /**
+     * Enables/disables colored output.
+     *
+     * @param bool $colors
+     */
+    public function setColors($colors)
+    {
+        $this->colors = (bool) $colors;
+    }
+
+    /**
+     * Sets the maximum number of characters per line for dumped strings.
+     *
+     * @param int $maxStringWidth
+     */
+    public function setMaxStringWidth($maxStringWidth)
+    {
+        if (function_exists('iconv')) {
+            $this->maxStringWidth = (int) $maxStringWidth;
+        }
+    }
+
+    /**
+     * Configures styles.
+     *
+     * @param array $styles A map of style namaes to style definitions.
+     */
+    public function setStyles(array $styles)
+    {
+        $this->styles = $styles + $this->styles;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function dumpScalar(Cursor $cursor, $type, $val)
+    {
+        if ('string' === $type) {
+            return $this->dumpString($cursor, $val, false, 0);
+        }
+
+        $this->dumpKey($cursor);
+
+        $style = 'const';
+
+        switch ($type) {
+            case 'integer':
+                $style = 'num';
+                break;
+
+            case 'double':
+                $style = 'num';
+
+                switch (true) {
+                    case INF === $val:  $val = 'INF';  break;
+                    case -INF === $val: $val = '-INF'; break;
+                    case is_nan($val):  $val = 'NAN';  break;
+                    default:
+                        $val = (string) $val;
+                        if (false === strpos($val, $this->decimalPoint)) {
+                            $val .= $this->decimalPoint.'0';
+                        }
+                        break;
+                }
+                break;
+
+            case 'NULL':
+                $val = 'null';
+                break;
+
+            case 'boolean':
+                $val = $val ? 'true' : 'false';
+                break;
+        }
+
+        $this->line .= $this->style($style, $val);
+
+        $this->endLine($cursor);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function dumpString(Cursor $cursor, $str, $bin, $cut)
+    {
+        $this->dumpKey($cursor);
+
+        if ('' === $str) {
+            $this->line .= '""';
+            $this->endLine($cursor);
+        } else {
+            $str = explode("\n", $str);
+            $m = count($str) - 1;
+            $i = $lineCut = 0;
+
+            if ($bin) {
+                $this->line .= 'b';
+            }
+
+            if ($m) {
+                $this->line .= '"""';
+                $this->endLine($cursor);
+            } else {
+                $this->line .= '"';
+            }
+
+            foreach ($str as $str) {
+                if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = iconv_strlen($str, 'UTF-8')) {
+                    $str = iconv_substr($str, 0, $this->maxStringWidth, 'UTF-8');
+                    $lineCut = $len - $this->maxStringWidth;
+                }
+
+                if ($m) {
+                    $this->line .= $this->indentPad;
+                }
+                $this->line .= $this->style('str', $str);
+
+                if ($i++ == $m) {
+                    $this->line .= '"';
+                    if ($m) {
+                        $this->line .= '""';
+                    }
+                    if ($cut < 0) {
+                        $this->line .= '…';
+                        $lineCut = 0;
+                    } elseif ($cut) {
+                        $lineCut += $cut;
+                    }
+                }
+                if ($lineCut) {
+                    $this->line .= '…'.$lineCut;
+                    $lineCut = 0;
+                }
+
+                $this->endLine($cursor, !$m);
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function enterArray(Cursor $cursor, $count, $indexed, $hasChild)
+    {
+        $this->enterHash($cursor, $count ? $this->style('note', 'array:'.$count).' [' : '[', $hasChild);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function leaveArray(Cursor $cursor, $count, $indexed, $hasChild, $cut)
+    {
+        $this->leaveHash($cursor, ']', $hasChild, $cut);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function enterObject(Cursor $cursor, $class, $hasChild)
+    {
+        $this->enterHash($cursor, 'stdClass' !== $class ? $this->style('note', $class).' {' : '{', $hasChild);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function leaveObject(Cursor $cursor, $class, $hasChild, $cut)
+    {
+        $this->leaveHash($cursor, '}', $hasChild, $cut);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function enterResource(Cursor $cursor, $res, $hasChild)
+    {
+        $this->enterHash($cursor, 'resource:'.$this->style('note', $res).' {', $hasChild);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function leaveResource(Cursor $cursor, $res, $hasChild, $cut)
+    {
+        $this->leaveHash($cursor, '}', $hasChild, $cut);
+    }
+
+    /**
+     * Generic dumper used while entering any hash-style structure.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $prefix   The string that starts the next dumped line.
+     * @param bool   $hasChild When the dump of the hash has child item.
+     */
+    protected function enterHash(Cursor $cursor, $prefix, $hasChild)
+    {
+        $this->dumpKey($cursor);
+
+        $this->line .= $prefix;
+        if (false !== $cursor->softRefTo) {
+            $this->line .= $this->style('ref', '@'.$cursor->softRefTo);
+        } elseif (false !== $cursor->hardRefTo) {
+            $this->line .= $this->style('ref', '@'.$cursor->hardRefTo);
+        } elseif ($hasChild) {
+            $this->endLine($cursor);
+        }
+    }
+
+    /**
+     * Generic dumper used while leaving any hash-style structure.
+     *
+     * @param Cursor $cursor   The Cursor position in the dump.
+     * @param string $suffix   The string that ends the next dumped line.
+     * @param bool   $hasChild When the dump of the hash has child item.
+     * @param int    $cut      The number of items the hash has been cut by.
+     */
+    protected function leaveHash(Cursor $cursor, $suffix, $hasChild, $cut)
+    {
+        if ($cut && false === $cursor->softRefTo && false === $cursor->hardRefTo) {
+            $this->line .= '…';
+            if (0 < $cut) {
+                $this->line .= $cut;
+            }
+            if ($hasChild) {
+                $this->dumpLine($cursor->depth+1);
+            }
+        }
+        $this->line .= $suffix;
+        $this->endLine($cursor, !$hasChild);
+    }
+
+    /**
+     * Dumps a key in a hash structure.
+     *
+     * @param Cursor $cursor The Cursor position in the dump.
+     */
+    protected function dumpKey(Cursor $cursor)
+    {
+        if (null !== $key = $cursor->hashKey) {
+            if ($bin = isset($key[0]) && !preg_match('//u', $key)) {
+                $key = Data::utf8Encode($key);
+                $bin = 'b';
+            }
+            switch ($cursor->hashType) {
+                default:
+                case Cursor::HASH_INDEXED:
+                case Cursor::HASH_ASSOC:
+                    if (is_int($key)) {
+                        $this->line .= $this->style('meta', $key).' => ';
+                    } else {
+                        $this->line .= $bin.'"'.$this->style('meta', $key).'" => ';
+                    }
+                    break;
+
+                case Cursor::HASH_RESOURCE:
+                    $key = "\0~\0".$key;
+                    // No break;
+                case Cursor::HASH_OBJECT:
+                    if (!isset($key[0]) || "\0" !== $key[0]) {
+                        $this->line .= $bin.$this->style('public', $key).': ';
+                    } elseif (0 < strpos($key, "\0", 1)) {
+                        $key = explode("\0", substr($key, 1), 2);
+
+                        switch ($key[0]) {
+                            case '+': // User inserted keys
+                                $this->line .= $bin.'"'.$this->style('public', $key[1]).'": ';
+                                break 2;
+
+                            case '~': $style = 'meta';      break;
+                            case '*': $style = 'protected'; break;
+                            default:  $style = 'private';   break;
+                        }
+
+                        $this->line .= $bin.$this->style($style, $key[1]).': ';
+                    } else {
+                        // This case should not happen
+                        $this->line .= $bin.'"'.$this->style('private', $key).'": ';
+                    }
+                    break;
+            }
+
+            if (false !== $cursor->hardRefTo) {
+                $this->line .= $this->style('ref', '&'.$cursor->hardRefTo).' ';
+            }
+        }
+    }
+
+    /**
+     * Finishes a line and dumps it.
+     *
+     * @param Cursor $cursor  The current Cursor position.
+     * @param bool   $showRef Show/hide the current ref index.
+     */
+    protected function endLine(Cursor $cursor, $showRef = true)
+    {
+        if ($showRef && false !== $cursor->refIndex) {
+            $this->line .= ' '.$this->style('ref', '#'.$cursor->refIndex);
+        }
+        $this->dumpLine($cursor->depth);
+    }
+
+    /**
+     * Decorates a value with some style.
+     *
+     * @param string $style The type of style being applied.
+     * @param string $val   The value being styled.
+     *
+     * @return string The value with style decoration.
+     */
+    protected function style($style, $val)
+    {
+        if (null === $this->colors) {
+            $this->colors = $this->supportsColors($this->outputStream);
+        }
+
+        if (!$this->colors || '' === $val) {
+            return $val;
+        }
+
+        if ('str' === $style || 'meta' === $style || 'public' === $style) {
+            foreach (static::$controlChars as $c) {
+                if (false !== strpos($val, $c)) {
+                    $r = "\x7F" === $c ? '?' : chr(64 + ord($c));
+                    $r = "\033[{$this->styles[$style]};{$this->styles['cchr']}m{$r}\033[m";
+                    $r = "\033[m{$r}\033[{$this->styles[$style]}m";
+                    $val = str_replace($c, $r, $val);
+                }
+            }
+        }
+
+        return sprintf("\033[%sm%s\033[m", $this->styles[$style], $val);
+    }
+
+    /**
+     * @return bool Tells if the current output stream supports ANSI colors or not.
+     */
+    protected function supportsColors()
+    {
+        if ($this->outputStream !== static::$defaultOutputStream) {
+            return @(is_resource($this->outputStream) && function_exists('posix_isatty') && posix_isatty($this->outputStream));
+        }
+        if (null !== static::$defaultColors) {
+            return static::$defaultColors;
+        }
+        if (isset($_SERVER['argv'][1])) {
+            $colors = $_SERVER['argv'];
+            $i = count($colors);
+            while (--$i > 0) {
+                if (isset($colors[$i][5])) {
+                    switch ($colors[$i]) {
+                        case '--ansi':
+                        case '--color':
+                        case '--color=yes':
+                        case '--color=force':
+                        case '--color=always':
+                            return static::$defaultColors = true;
+
+                        case '--no-ansi':
+                        case '--color=no':
+                        case '--color=none':
+                        case '--color=never':
+                            return static::$defaultColors = false;
+                    }
+                }
+            }
+        }
+
+        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+            static::$defaultColors = @(false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'));
+        } elseif (function_exists('posix_isatty')) {
+            $h = stream_get_meta_data($this->outputStream) + array('wrapper_type' => null);
+            $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'wb') : $this->outputStream;
+            static::$defaultColors = @posix_isatty($h);
+        } else {
+            static::$defaultColors = false;
+        }
+
+        return static::$defaultColors;
+    }
+}
diff --git a/src/Symfony/Component/VarDumper/Dumper/DataDumperInterface.php b/src/Symfony/Component/VarDumper/Dumper/DataDumperInterface.php
new file mode 100644
index 0000000000000..ee6060cebf647
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Dumper/DataDumperInterface.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * DataDumperInterface for dumping Data objects.
+ *
+ * @author Nicolas Grekas 
+ */
+interface DataDumperInterface
+{
+    /**
+     * Dumps a Data object.
+     *
+     * @param Data $data A Data object.
+     */
+    public function dump(Data $data);
+}
diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
new file mode 100644
index 0000000000000..6e740c9dcd15b
--- /dev/null
+++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
@@ -0,0 +1,253 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Cursor;
+
+/**
+ * HtmlDumper dumps variables as HTML.
+ *
+ * @author Nicolas Grekas 
+ */
+class HtmlDumper extends CliDumper
+{
+    public static $defaultOutputStream = 'php://output';
+
+    protected $dumpHeader;
+    protected $dumpPrefix = '
';
+    protected $dumpSuffix = '
'; + protected $colors = true; + protected $headerIsDumped = false; + protected $lastDepth = -1; + protected $styles = array( + 'num' => 'font-weight:bold;color:#0087FF', + 'const' => 'font-weight:bold;color:#0087FF', + 'str' => 'font-weight:bold;color:#00D7FF', + 'cchr' => 'font-style: italic', + 'note' => 'color:#D7AF00', + 'ref' => 'color:#444444', + 'public' => 'color:#008700', + 'protected' => 'color:#D75F00', + 'private' => 'color:#D70000', + 'meta' => 'color:#005FFF', + ); + + /** + * {@inheritdoc} + */ + public function setLineDumper($callback) + { + $this->headerIsDumped = false; + + return parent::setLineDumper($callback); + } + + /** + * {@inheritdoc} + */ + public function setStyles(array $styles) + { + $this->headerIsDumped = false; + $this->styles = $styles + $this->styles; + } + + /** + * Sets an HTML header that will be dumped once in the output stream. + * + * @param string $header An HTML string. + */ + public function setDumpHeader($header) + { + $this->dumpHeader = $header; + } + + /** + * Sets an HTML prefix and suffix that will encapse every single dump. + * + * @param string $prefix The prepended HTML string. + * @param string $suffix The appended HTML string. + */ + public function setDumpBoundaries($prefix, $suffix) + { + $this->dumpPrefix = $prefix; + $this->dumpSuffix = $suffix; + } + + /** + * Dumps the HTML header. + */ + protected function getDumpHeader() + { + $this->headerIsDumped = true; + + if (null !== $this->dumpHeader) { + return $this->dumpHeader; + } + + $line = <<<'EOHTML' + +'.$this->dumpHeader; + } + + /** + * {@inheritdoc} + */ + protected function enterHash(Cursor $cursor, $prefix, $hasChild) + { + if ($hasChild) { + $prefix .= ''; + } + + return parent::enterHash($cursor, $prefix, $hasChild); + } + + /** + * {@inheritdoc} + */ + protected function leaveHash(Cursor $cursor, $suffix, $hasChild, $cut) + { + if ($hasChild) { + $suffix = ''.$suffix; + } + + return parent::leaveHash($cursor, $suffix, $hasChild, $cut); + } + + /** + * {@inheritdoc} + */ + protected function style($style, $val) + { + if ('' === $val) { + return ''; + } + + if ('ref' === $style) { + $ref = substr($val, 1); + if ('#' === $val[0]) { + return "$val"; + } else { + return "$val"; + } + } + + $val = htmlspecialchars($val, ENT_QUOTES, 'UTF-8'); + + if ('str' === $style || 'meta' === $style || 'public' === $style) { + foreach (static::$controlChars as $c) { + if (false !== strpos($val, $c)) { + $r = "\x7F" === $c ? '?' : chr(64 + ord($c)); + $val = str_replace($c, "$r", $val); + } + } + } elseif ('note' === $style) { + if (false !== $c = strrpos($val, '\\')) { + $val = sprintf('%s', $val, $style, substr($val, $c+1)); + } + } + + return "$val"; + } + + /** + * {@inheritdoc} + */ + protected function dumpLine($depth) + { + switch ($this->lastDepth - $depth) { + case +1: $this->line = ''.$this->line; break; + case -1: $this->line = "$this->line"; break; + } + + if (-1 === $this->lastDepth) { + $this->line = $this->dumpPrefix.$this->line; + } + if (!$this->headerIsDumped) { + $this->line = $this->getDumpHeader().$this->line; + } + + if (-1 === $depth) { + $this->line .= $this->dumpSuffix; + parent::dumpLine(0); + } + $this->lastDepth = $depth; + + parent::dumpLine($depth); + } +} diff --git a/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php b/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php new file mode 100644 index 0000000000000..1959ac8985e48 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php @@ -0,0 +1,36 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace Symfony\Component\VarDumper\Exception; + +/** + * @author Nicolas Grekas + */ +class ThrowingCasterException extends \Exception +{ + private $caster; + + /** + * @param callable $caster The failing caster + * @param \Exception $prev The exception thrown from the caster + */ + public function __construct($caster, \Exception $prev) + { + if (is_array($caster)) { + if (isset($caster[0]) && is_object($caster[0])) { + $caster[0] = get_class($caster[0]); + } + $caster = implode('::', $caster); + } + $this->caster = $caster; + parent::__construct(null, 0, $prev); + } +} diff --git a/src/Symfony/Component/VarDumper/LICENSE b/src/Symfony/Component/VarDumper/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/VarDumper/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/VarDumper/README.md b/src/Symfony/Component/VarDumper/README.md new file mode 100644 index 0000000000000..3eb3ef24052b8 --- /dev/null +++ b/src/Symfony/Component/VarDumper/README.md @@ -0,0 +1,14 @@ +Symfony mechanism for exploring and dumping PHP variables +========================================================= + +This component provides a mechanism that allows exploring then dumping +any PHP variable. + +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. + +The component exposes all the parts involved in the different steps of +cloning then dumping a PHP variable, while applying size limits and having +specialized output formats and methods. diff --git a/src/Symfony/Component/VarDumper/Resources/functions/dump.php b/src/Symfony/Component/VarDumper/Resources/functions/dump.php new file mode 100644 index 0000000000000..b6c243c8b6e2f --- /dev/null +++ b/src/Symfony/Component/VarDumper/Resources/functions/dump.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\VarDumper\VarDumper; + +if (!function_exists('dump')) { + /** + * @author Nicolas Grekas + */ + function dump($var) + { + foreach (func_get_args() as $var) { + VarDumper::dump($var); + } + } +} diff --git a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php new file mode 100644 index 0000000000000..6a4e9c3f882fe --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests; + +use Symfony\Component\VarDumper\Cloner\PhpCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +/** + * @author Nicolas Grekas + */ +class CliDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testGet() + { + require __DIR__.'/Fixtures/dumb-var.php'; + + $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(); + $dumper->dump($data); + $out = ob_get_clean(); + $closureLabel = PHP_VERSION_ID >= 50400 ? 'public method' : 'function'; + $out = preg_replace('/[ \t]+$/m', '', $out); + + $this->assertSame( + << 1 + 0 => null #1 + "const" => 1.1 + 1 => true + 2 => false + 3 => NAN + 4 => INF + 5 => -INF + 6 => 9223372036854775807 + "str" => "déjà" + 7 => b"é" + "[]" => [] + "res" => resource:stream { + wrapper_type: "plainfile" + stream_type: "STDIO" + mode: "r" + unread_bytes: 0 + seekable: true + timed_out: false + blocked: true + eof: false + options: [] + } + 8 => resource:Unknown {} + "obj" => Symfony\Component\VarDumper\Tests\Fixture\DumbFoo { #2 + foo: "foo" + "bar": "bar" + } + "closure" => Closure { + reflection: """ + Closure [ {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { + @@ {$var['file']} {$var['line']} - {$var['line']} + + - Parameters [2] { + Parameter #0 [ \$a ] + Parameter #1 [ PDO or NULL &\$b = NULL ] + } + } + """ + } + "line" => {$var['line']} + "nobj" => array:1 [ + 0 => {} #3 + ] + "recurs" => array:1 [ #4 + 0 => &4 array:1 [@4] + ] + 9 => &1 null + "sobj" => Symfony\Component\VarDumper\Tests\Fixture\DumbFoo {@2} + "snobj" => &3 {@3} + "snobj2" => {@3} + "file" => "{$var['file']}" + b"bin-key-é" => "" +] + +EOTXT + , + + $out + ); + } +} diff --git a/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php b/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php new file mode 100644 index 0000000000000..6ffc8dd214388 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Fixtures/dumb-var.php @@ -0,0 +1,43 @@ +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); diff --git a/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php new file mode 100644 index 0000000000000..25b027d62e269 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests; + +use Symfony\Component\VarDumper\Cloner\PhpCloner; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + +/** + * @author Nicolas Grekas + */ +class HtmlDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testGet() + { + require __DIR__.'/Fixtures/dumb-var.php'; + + $dumper = new HtmlDumper('php://output'); + $dumper->setColors(false); + $dumper->setDumpHeader(''); + $dumper->setDumpBoundaries('', ''); + $cloner = new PhpCloner(); + $cloner->addCasters(array( + ':stream' => function ($res, $a) { + unset($a['uri']); + + return $a; + } + )); + $data = $cloner->cloneVar($var); + + ob_start(); + $dumper->dump($data); + $out = ob_get_clean(); + $closureLabel = PHP_VERSION_ID >= 50400 ? 'public method' : 'function'; + $out = preg_replace('/[ \t]+$/m', '', $out); + $var['file'] = htmlspecialchars($var['file'], ENT_QUOTES, 'UTF-8'); + + $this->assertSame( + <<array:25 [ + "number" => 1 + 0 => null #1 + "const" => 1.1 + 1 => true + 2 => false + 3 => NAN + 4 => INF + 5 => -INF + 6 => 9223372036854775807 + "str" => "déjà" + 7 => b"é" + "[]" => [] + "res" => resource:stream { + wrapper_type: "plainfile" + stream_type: "STDIO" + mode: "r" + unread_bytes: 0 + seekable: true + timed_out: false + blocked: true + eof: false + options: [] + } + 8 => resource:Unknown {} + "obj" => DumbFoo { #2 + foo: "foo" + "bar": "bar" + } + "closure" => Closure { + reflection: """ + Closure [ <user> {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { + @@ {$var['file']} {$var['line']} - {$var['line']} + + - Parameters [2] { + Parameter #0 [ <required> \$a ] + Parameter #1 [ <optional> PDO or NULL &\$b = NULL ] + } + } + """ + } + "line" => {$var['line']} + "nobj" => array:1 [ + 0 => {} #3 + ] + "recurs" => array:1 [ #4 + 0 => &4 array:1 [@4] + ] + 9 => &1 null + "sobj" => DumbFoo {@2} + "snobj" => &3 {@3} + "snobj2" => {@3} + "file" => "{$var['file']}" + b"bin-key-é" => "" +] + + +EOTXT + , + + $out + ); + } +} diff --git a/src/Symfony/Component/VarDumper/VarDumper.php b/src/Symfony/Component/VarDumper/VarDumper.php new file mode 100644 index 0000000000000..b92f5bafd2cf6 --- /dev/null +++ b/src/Symfony/Component/VarDumper/VarDumper.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper; + +use Symfony\Component\VarDumper\Cloner\ExtCloner; +use Symfony\Component\VarDumper\Cloner\PhpCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + +/** + * @author Nicolas Grekas + */ +class VarDumper +{ + private static $handler; + + public static function dump($var) + { + if (null === self::$handler) { + $cloner = extension_loaded('symfony_debug') ? new ExtCloner() : new PhpCloner(); + $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); + self::$handler = function ($var) use ($cloner, $dumper) { + $dumper->dump($cloner->cloneVar($var)); + }; + } + + return call_user_func(self::$handler, $var); + } + + public static function setHandler($callable) + { + if (null !== $callable && !is_callable($callable, true)) { + throw new \InvalidArgumentException('Invalid PHP callback.'); + } + + $prevHandler = self::$handler; + self::$handler = $callable; + + return $prevHandler; + } +} diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json new file mode 100644 index 0000000000000..4a22fc2098aa1 --- /dev/null +++ b/src/Symfony/Component/VarDumper/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/var-dumper", + "type": "library", + "description": "Symfony mechanism for exploring and dumping PHP variables", + "keywords": ["dump", "debug"], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + } + ], + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-symfony_debug": "" + }, + "autoload": { + "files": [ "Resources/functions/dump.php" ], + "psr-0": { "Symfony\\Component\\VarDumper\\": "" } + }, + "target-dir": "Symfony/Component/VarDumper", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + } +} diff --git a/src/Symfony/Component/VarDumper/phpunit.xml.dist b/src/Symfony/Component/VarDumper/phpunit.xml.dist new file mode 100644 index 0000000000000..a75c8fc69c008 --- /dev/null +++ b/src/Symfony/Component/VarDumper/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./Resources + ./vendor + + + +