diff --git a/composer.json b/composer.json index 07e4ee4254..0a14b8ff74 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ ], "files": [ "src/Symfony/Component/Intl/Resources/stubs/functions.php", - "src/Symfony/Bundle/DebugBundle/Resources/functions/debug.php" + "src/Symfony/Bundle/DebugBundle/Resources/functions/dump.php" ] }, "minimum-stability": "dev", diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php new file mode 100644 index 0000000000..a0e895dd87 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -0,0 +1,32 @@ + + * + * 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; + +/** + * Provides integration of the dump() function with Twig. + * + * @author Nicolas Grekas + */ +class DumpExtension extends \Twig_Extension +{ + public function getTokenParsers() + { + return array(new DumpTokenParser()); + } + + public function getName() + { + return 'dump'; + } +} diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php new file mode 100644 index 0000000000..ca64660243 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.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\Node; + +/** + * @author Julien Galenski + */ +class DumpNode extends \Twig_Node +{ + public function __construct(\Twig_NodeInterface $values = null, $lineno, $tag = null) + { + parent::__construct(array('values' => $values), array(), $lineno, $tag); + } + + /** + * {@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("\$vars = array();\n") + ->write("foreach (\$context as \$key => \$value) {\n") + ->indent() + ->write("if (!\$value instanceof Twig_Template) {\n") + ->indent() + ->write("\$vars[\$key] = \$value;\n") + ->outdent() + ->write("}\n") + ->outdent() + ->write("}\n") + ->addDebugInfo($this) + ->write('\Symfony\Component\Debug\Debug::dump($vars);'."\n") + ; + } elseif (1 === $values->count()) { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\Debug\Debug::dump(') + ->subcompile($values->getNode(0)) + ->raw(");\n") + ; + } else { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\Debug\Debug::dump(array(') + ->indent() + ; + foreach ($values as $node) { + $compiler->addIndentation(); + if ($node->hasAttribute('name')) { + $compiler + ->string($node->getAttribute('name')) + ->raw(' => ') + ; + } + $compiler + ->subcompile($node) + ->raw(",\n") + ; + } + $compiler + ->outdent() + ->raw("));\n") + ; + } + + $compiler + ->outdent() + ->write("}\n") + ; + } +} diff --git a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php new file mode 100644 index 0000000000..1505e5bcca --- /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($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 59db37b44f..d9a864b919 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/Component/Debug/Debug.php b/src/Symfony/Component/Debug/Debug.php index fd308e8003..b6b2d15125 100644 --- a/src/Symfony/Component/Debug/Debug.php +++ b/src/Symfony/Component/Debug/Debug.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Debug; +use Symfony\Component\VarDumper\Cloner\ExtCloner; +use Symfony\Component\VarDumper\Cloner\PhpCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + /** * Registers all the debug tools. * @@ -19,6 +24,7 @@ namespace Symfony\Component\Debug; class Debug { private static $enabled = false; + private static $dumpHandler; /** * Enables the debug tools. @@ -59,4 +65,42 @@ class Debug DebugClassLoader::enable(); } + + public static function dump($var) + { + if (null === self::$dumpHandler) { + $cloner = extension_loaded('symfony_debug') ? new ExtCloner() : new PhpCloner(); + $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); + self::$dumpHandler = function ($var) use ($cloner, $dumper) { + $dumper->dump($cloner->cloneVar($var)); + }; + } + + $h = self::$dumpHandler; + + if (is_array($h)) { + return $h[0]->{$h[1]}($var); + } + + return $h($var); + } + + public static function setDumpHandler($callable) + { + if (!is_callable($callable)) { + throw new \InvalidArgumentException('Invalid PHP callback.'); + } + + $prevHandler = self::$dumpHandler; + + if (is_array($callable)) { + if (!is_object($callable[0])) { + self::$dumpHandler = $callable[0].'::'.$callable[1]; + } + } else { + self::$dumpHandler = $callable; + } + + return $prevHandler; + } } diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index b919aa4fd6..54e7a9cab3 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -20,10 +20,12 @@ "psr/log": "~1.0" }, "require-dev": { + "symfony/var-dumper": "~2.6", "symfony/http-kernel": "~2.1", "symfony/http-foundation": "~2.1" }, "suggest": { + "symfony/var-dumper": "For using Debug::dump()", "symfony/http-foundation": "", "symfony/http-kernel": "" }, diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php new file mode 100644 index 0000000000..298573c586 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php @@ -0,0 +1,252 @@ + + * + * 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 $isCollected = true; + private $clonesRoot; + private $clonesCount = 0; + + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + $this->clonesRoot = $this; + } + + public function __clone() + { + $this->data = array(); + $this->clonesRoot->clonesCount++; + } + + public function dump(Data $data) + { + if ($this->stopwatch) { + $this->stopwatch->start('dump'); + } + if ($this->clonesRoot->isCollected) { + $this->clonesRoot->isCollected = false; + register_shutdown_function(array($this->clonesRoot, 'flushDumps')); + } + + $trace = PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS : true; + if (PHP_VERSION_ID >= 50400) { + $trace = debug_backtrace($trace, 6); + } else { + $trace = debug_backtrace($trace); + } + + $file = $trace[0]['file']; + $line = $trace[0]['line']; + $name = false; + $fileExcerpt = false; + + for ($i = 1; $i < 6; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dump' === $trace[$i]['function'] + && 'Symfony\Bundle\DebugBundle\DebugBundle' === $trace[$i]['class'] + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 6) { + if (isset($trace[$i]['function']) && empty($trace[$i]['class'])) { + $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->clonesRoot->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt'); + + if ($this->stopwatch) { + $this->stopwatch->stop('dump'); + } + } + + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + public function serialize() + { + $ser = serialize($this->clonesRoot->data); + $this->clonesRoot->data = array(); + $this->clonesRoot->isCollected = true; + + return $ser; + } + + public function unserialize($data) + { + parent::unserialize($data); + + $this->clonesRoot = $this; + } + + public function getDumpsCount() + { + return count($this->clonesRoot->data); + } + + public function getDumpsExcerpts() + { + $dumps = array(); + + foreach ($this->data as $dump) { + $data = $dump['data']->getRawData(); + unset($dump['data']); + + $data = $data[0][0]; + + if (isset($data->val)) { + $data = $data->val; + } + + if (isset($data->bin)) { + $data = 'b"'.$data->bin.'"'; + } elseif (isset($data->str)) { + $data = '"'.$data->str.'"'; + } elseif (isset($data->count)) { + $data = 'array('.$data->count.')'; + } elseif (isset($data->class)) { + $data = $data->class.'{...}'; + } elseif (isset($data->res)) { + $data = 'resource:'.$data->res.'{...}'; + } elseif (is_array($data)) { + $data = 'array()'; + } elseif (null === $data) { + $data = 'null'; + } elseif (false === $data) { + $data = 'false'; + } elseif (INF === $data) { + $data = 'INF'; + } elseif (-INF === $data) { + $data = '-INF'; + } elseif (NAN === $data) { + $data = 'NAN'; + } elseif (true === $data) { + $data = 'true'; + } + + $dump['dataExcerpt'] = $data; + $dumps[] = $dump; + } + + return $dumps; + } + + public function getDumps($getData = false) + { + if ($getData) { + $dumper = new JsonDumper(); + } + $dumps = array(); + + foreach ($this->clonesRoot->data as $dump) { + $json = ''; + if ($getData) { + $dumper->dump($dump['data'], function ($line) use (&$json) {$json .= $line;}); + } + $dump['data'] = $json; + $dumps[] = $dump; + } + + return $dumps; + } + + public function getName() + { + return 'dump'; + } + + public function flushDumps() + { + if (0 === $this->clonesRoot->clonesCount-- && !$this->clonesRoot->isCollected && $this->clonesRoot->data) { + $this->clonesRoot->clonesCount = 0; + $this->clonesRoot->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 (stripos($h[$i], 'html')) { + echo ''; + $dumper = new HtmlDumper(); + } else { + $dumper = new CliDumper(); + $dumper->setColors(false); + } + + foreach ($this->clonesRoot->data as $i => $dump) { + $this->clonesRoot->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
in {$dump['name']} on line {$dump['line']}:"; + } else { + echo "\nin {$dump['name']} on line {$dump['line']}:\n\n"; + } + $dumper->dump($dump['data']); + } + + $this->clonesRoot->data = array(); + } + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DumpListener.php b/src/Symfony/Component/HttpKernel/EventListener/DumpListener.php new file mode 100644 index 0000000000..89dee034b5 --- /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\Debug\Debug; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * 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; + + Debug::setDumpHandler(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));}; + Debug::setDumpHandler($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/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php index 3182dc4cb1..e333eb8539 100644 --- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php @@ -21,7 +21,7 @@ class HtmlDumper extends CliDumper public static $defaultOutputStream = 'php://output'; protected $dumpHeader; - protected $dumpPrefix = '
';
+    protected $dumpPrefix = '
';
     protected $dumpSuffix = '
'; protected $colors = true; protected $headerIsDumped = false; @@ -88,7 +88,7 @@ class HtmlDumper extends CliDumper $this->headerIsDumped = true; $line = $this->line; - $p = 'sf-var-debug'; + $p = 'sf-dump'; $this->line = ' -
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: "dir"
-    mode: "r"
-    unread_bytes: 0
-    seekable: true
-    timed_out: false
-    blocked: true
-    eof: false
-    options: []
+
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: "dir"
+    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"
+  8 => resource:Unknown {}
+  "obj" => Symfony\Component\VarDumper\Tests\Fixture\DumbFoo { #2
+    foo: "foo"
+    "bar": "bar"
   }
-  "closure" => Closure {
-    reflection: """
-      Closure [ <user> {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] {
-        @@ {$var['file']} {$var['line']} - {$var['line']}
+  "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 ]
-        }
-      }
+        - Parameters [2] {
+          Parameter #0 [ <required> \$a ]
+          Parameter #1 [ <optional> PDO or NULL &\$b = NULL ]
+        }
+      }
       """
   }
-  "line" => {$var['line']}
-  "nobj" => array:1 [
-    0 => {} #3
+  "line" => {$var['line']}
+  "nobj" => array:1 [
+    0 => {} #3
   ]
-  "recurs" => array:1 [ #4
-    0 => array:1 [&4]
+  "recurs" => array:1 [ #4
+    0 => array:1 [&4]
   ]
-  9 => null &1
-  "sobj" => Symfony\Component\VarDumper\Tests\Fixture\DumbFoo {@2}
-  "snobj" => {&3}
-  "snobj2" => {@3}
-  "file" => "{$var['file']}"
-  b"bin-key-é" => ""
+  9 => null &1
+  "sobj" => Symfony\Component\VarDumper\Tests\Fixture\DumbFoo {@2}
+  "snobj" => {&3}
+  "snobj2" => {@3}
+  "file" => "{$var['file']}"
+  b"bin-key-é" => ""
 ]