From 3adb1e45f614fef040659d58c78cc0454874d074 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 20 Mar 2024 13:43:14 -0500 Subject: [PATCH] New debugbar twig extensions (#632) --- demo/bridge/twig/hello.html | 5 + demo/bridge/twig/index.php | 11 +++ docs/bridge_collectors.md | 57 ++++++++--- .../Bridge/Twig/DebugTwigExtension.php | 94 ++++++++++++++++++ .../Bridge/Twig/DumpTwigExtension.php | 99 +++++++++++++++++++ .../Bridge/Twig/MeasureTwigExtension.php | 77 +++++++++++++++ src/DebugBar/Bridge/Twig/MeasureTwigNode.php | 50 ++++++++++ .../Bridge/Twig/MeasureTwigTokenParser.php | 78 +++++++++++++++ 8 files changed, 460 insertions(+), 11 deletions(-) create mode 100644 src/DebugBar/Bridge/Twig/DebugTwigExtension.php create mode 100644 src/DebugBar/Bridge/Twig/DumpTwigExtension.php create mode 100644 src/DebugBar/Bridge/Twig/MeasureTwigExtension.php create mode 100644 src/DebugBar/Bridge/Twig/MeasureTwigNode.php create mode 100644 src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php diff --git a/demo/bridge/twig/hello.html b/demo/bridge/twig/hello.html index 296b3f6..229e715 100644 --- a/demo/bridge/twig/hello.html +++ b/demo/bridge/twig/hello.html @@ -1,2 +1,7 @@ Hello {{ name }} {% include "foobar.html" %} + +{% measure 'Measure Debugs' %} +{{ debug('Hello ' ~ name) }} +{{ dump({'name' : name}) }} +{% endmeasure %} diff --git a/demo/bridge/twig/index.php b/demo/bridge/twig/index.php index e6837bc..98b3843 100644 --- a/demo/bridge/twig/index.php +++ b/demo/bridge/twig/index.php @@ -8,8 +8,19 @@ $debugbarRenderer->setBaseUrl('../../../src/DebugBar/Resources'); $loader = new Twig\Loader\FilesystemLoader('.'); $twig = new Twig\Environment($loader); $profile = new Twig\Profiler\Profile(); + +// enable template measure on timeline $twig->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); +// enable {% measure 'foo' %} {% endmeasure %} tags for time measure on templates +$twig->addExtension(new DebugBar\Bridge\Twig\MeasureTwigExtension($debugbar['time'])); + +$twig->enableDebug(); +// enable {{ dump('foo') }} function on templates +$twig->addExtension(new DebugBar\Bridge\Twig\DumpTwigExtension()); +// enable {{ debug('foo') }} function on templates +$twig->addExtension(new DebugBar\Bridge\Twig\DebugTwigExtension($debugbar['messages'])); + $debugbar->addCollector(new DebugBar\Bridge\NamespacedTwigProfileCollector($profile, $twig)); render_demo_page(function() use ($twig) { diff --git a/docs/bridge_collectors.md b/docs/bridge_collectors.md index e923b07..9ea1d61 100644 --- a/docs/bridge_collectors.md +++ b/docs/bridge_collectors.md @@ -118,17 +118,6 @@ $env->addExtension(new Twig_Extension_Profiler($profile)); $debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); ``` -You can optionally use `DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler` in place of -`Twig_Extension_Profiler` so render operation can be measured. - -```php -$loader = new Twig_Loader_Filesystem('.'); -$env = new Twig_Environment($loader); -$profile = new Twig_Profiler_Profile(); -$env->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); -$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); -``` - ### Version 2 and 3 This collector uses the class `Twig\Extension\ProfilerExtension` to collect info about rendered @@ -149,3 +138,49 @@ $env->addExtension(new ProfilerExtension($profile)); $debugbar->addCollector(new NamespacedTwigProfileCollector($profile)); ``` +### Optional debugbar twig extensions + +You can optionally use `DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler` in place of +`Twig\Profiler\Profile` so render operation can be measured. + +```php +use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use Twig\Profiler\Profile; + +$loader = new FilesystemLoader('.'); +$env = new Environment($loader); +$profile = new Profile(); + +$env->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); +$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); +``` + +Other optional extensions add functions and tags for debugbar integration into templates. + +```php +use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use Twig\Profiler\Profile; + +$loader = new FilesystemLoader('.'); +$env = new Environment($loader); +$profile = new Profile(); + +// enable {% measure 'foo' %} {% endmeasure %} tags for time measure on templates +// this extension adds timeline items to TimeDataCollector +$twig->addExtension(new DebugBar\Bridge\Twig\MeasureTwigExtension($debugbar['time'])); + +$twig->enableDebug(); // if Twig\Environment debug is disabled, dump/debug are ignored + +// enable {{ dump('foo') }} function on templates +// this extension allows dumping data using debugbar DataFormatter +$twig->addExtension(new DebugBar\Bridge\Twig\DumpTwigExtension()); + +// enable {{ debug('foo') }} function on templates +// this extension allows debugging in MessageCollector +$twig->addExtension(new DebugBar\Bridge\Twig\DebugTwigExtension($debugbar['messages'])); + +$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); +``` + diff --git a/src/DebugBar/Bridge/Twig/DebugTwigExtension.php b/src/DebugBar/Bridge/Twig/DebugTwigExtension.php new file mode 100644 index 0000000..a7f6bce --- /dev/null +++ b/src/DebugBar/Bridge/Twig/DebugTwigExtension.php @@ -0,0 +1,94 @@ +messagesCollector = $messagesCollector; + $this->functionName = $functionName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * {@inheritDoc} + */ + public function getFunctions() + { + return [ + new TwigFunction( + $this->functionName, + [$this, 'debug'], + ['needs_context' => true, 'needs_environment' => true] + ), + ]; + } + + /** + * Based on Twig_Extension_Debug / twig_var_dump + * + * @param Environment $env + * @param $context + */ + public function debug(Environment $env, $context) + { + if (!$env->isDebug() || !$this->messagesCollector) { + return; + } + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = "Object (" . get_class($value) . ")"; + } + } else { + $data[$key] = $value; + } + } + $this->messagesCollector->addMessage($data, 'debug'); + } else { + for ($i = 2; $i < $count; $i++) { + $this->messagesCollector->addMessage(func_get_arg($i), 'debug'); + } + } + + return; + } +} diff --git a/src/DebugBar/Bridge/Twig/DumpTwigExtension.php b/src/DebugBar/Bridge/Twig/DumpTwigExtension.php new file mode 100644 index 0000000..83714cb --- /dev/null +++ b/src/DebugBar/Bridge/Twig/DumpTwigExtension.php @@ -0,0 +1,99 @@ +functionName = $functionName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * {@inheritDoc} + */ + public function getFunctions() + { + return [ + new TwigFunction( + $this->functionName, + [$this, 'dump'], + ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true] + ), + ]; + } + + /** + * Based on Twig_Extension_Debug / twig_var_dump + * + * @param Environment $env + * @param $context + * + * @return string + */ + public function dump(Environment $env, $context) + { + if (!$env->isDebug()) { + return; + } + + $output = ''; + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = "Object (" . get_class($value) . ")"; + } + } else { + $data[$key] = $value; + } + } + $output .= $this->formatVar($data); + } else { + for ($i = 2; $i < $count; $i++) { + $output .= $this->formatVar(func_get_arg($i)); + } + } + + if ($this->isHtmlVarDumperUsed()) { + return $output; + } + + return '
' . $output . '
'; + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php b/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php new file mode 100644 index 0000000..b89ab6f --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php @@ -0,0 +1,77 @@ +timeCollector = $timeCollector; + $this->tagName = $tagName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * @return \Twig\TokenParser\TokenParserInterface[] + */ + public function getTokenParsers() + { + return [ + /* + * {% measure foo %} + * Some stuff which will be recorded on the timeline + * {% endmeasure %} + */ + new MeasureTwigTokenParser(!is_null($this->timeCollector), $this->tagName, $this->getName()), + ]; + } + + public function startMeasure(...$arg) + { + if (!$this->timeCollector) { + return; + } + + $this->timeCollector->startMeasure(...$arg); + } + + public function stopMeasure(...$arg) + { + if (!$this->timeCollector) { + return; + } + + $this->timeCollector->stopMeasure(...$arg); + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigNode.php b/src/DebugBar/Bridge/Twig/MeasureTwigNode.php new file mode 100644 index 0000000..565dd6e --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigNode.php @@ -0,0 +1,50 @@ + $body, 'name' => $name, 'var' => $var], [], $lineno, $tag); + $this->extName = $extName ?: MeasureTwigExtension::class; + } + + public function compile(Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('var')) + ->raw(' = ') + ->subcompile($this->getNode('name')) + ->write(";\n") + ->write("\$this->env->getExtension('".$this->extName."')->startMeasure(") + ->subcompile($this->getNode('var')) + ->raw(");\n") + ->subcompile($this->getNode('body')) + ->write("\$this->env->getExtension('".$this->extName."')->stopMeasure(") + ->subcompile($this->getNode('var')) + ->raw(");\n"); + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php b/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php new file mode 100644 index 0000000..6926a08 --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php @@ -0,0 +1,78 @@ +enabled = $enabled; + $this->tagName = $tagName; + $this->extName = $extName; + } + + public function parse(Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + // {% measure 'bar' %} + $name = $this->parser->getExpressionParser()->parseExpression(); + + $stream->expect(Token::BLOCK_END_TYPE); + + // {% endmeasure %} + $body = $this->parser->subparse([$this, 'decideMeasureEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + + if ($this->enabled) { + return new MeasureTwigNode( + $name, + $body, + new AssignNameExpression($this->parser->getVarName(), $token->getLine()), + $lineno, + $this->getTag(), + $this->extName + ); + } + + return $body; + } + + public function getTag() + { + return $this->tagName; + } + + public function decideMeasureEnd(Token $token) + { + return $token->test('end'.$this->getTag()); + } +}