diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index ad22216e40..346d52a5ad 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.4.0 +----- + + * added stopwatch tag to time templates with the WebProfilerBundle + 2.3.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php new file mode 100644 index 0000000000..66f9b3a0e2 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.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\Bridge\Twig\Extension; + +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Bridge\Twig\TokenParser\StopwatchTokenParser; + +/** + * Twig extension for the stopwatch helper. + * + * @author Wouter J + */ +class StopwatchExtension extends \Twig_Extension +{ + private $stopwatch; + + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + } + + public function getTokenParsers() + { + return array( + /* + * {% stopwatch foo %} + * Some stuff which will be recorded on the timeline + * {% endstopwatch %} + */ + new StopwatchTokenParser($this->stopwatch !== null), + ); + } + + public function getName() + { + return 'stopwatch'; + } + + public function startEvent($name) + { + if ($this->stopwatch instanceof Stopwatch) { + $this->stopwatch->start($name, 'template'); + } + } + + public function stopEvent($name) + { + if ($this->stopwatch instanceof Stopwatch) { + $this->stopwatch->stop($name); + } + } +} diff --git a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php new file mode 100644 index 0000000000..f217bc0bc0 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/StopwatchNode.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\Bridge\Twig\Node; + +/** + * Represents a stopwatch node. + * + * @author Wouter J + */ +class StopwatchNode extends \Twig_Node +{ + public function __construct($name, $body, $lineno = 0, $tag = null) + { + parent::__construct(array('body' => $body), array('name' => $name), $lineno, $tag); + } + + public function compile(\Twig_Compiler $compiler) + { + $name = $this->getAttribute('name'); + + $compiler + ->write('$this->env->getExtension(\'stopwatch\')->startEvent(\''.$name.'\');') + ->subcompile($this->getNode('body')) + ->write('$this->env->getExtension(\'stopwatch\')->stopEvent(\''.$name.'\');') + ->raw("\n"); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php new file mode 100644 index 0000000000..b8cadf2af3 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php @@ -0,0 +1,91 @@ + + * + * 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\StopwatchExtension; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Bridge\Twig\Tests\TestCase; + +class StopwatchExtensionTest extends TestCase +{ + protected function setUp() + { + parent::setUp(); + + if (!class_exists('Symfony\Component\Stopwatch\Stopwatch')) { + $this->markTestSkipped('The "Stopwatch" component is not available'); + } + } + + /** + * @expectedException \Twig_Error_Syntax + */ + public function testFailIfNameAlreadyExists() + { + $this->testTiming('{% stopwatch foo %}{% endstopwatch %}{% stopwatch foo %}{% endstopwatch %}', array()); + } + + /** + * @expectedException \Twig_Error_Syntax + */ + public function testFailIfStoppingWrongEvent() + { + $this->testTiming('{% stopwatch foo %}{% endstopwatch bar %}', array()); + } + + /** + * @dataProvider getTimingTemplates + */ + public function testTiming($template, $events) + { + $twig = new \Twig_Environment(new \Twig_Loader_String(), array('debug' => true, 'cache' => false, 'autoescape' => true, 'optimizations' => 0)); + $twig->addExtension(new StopwatchExtension($this->getStopwatch($events))); + + try { + $nodes = $twig->render($template); + } catch (\Twig_Error_Runtime $e) { + throw $e->getPrevious(); + } + } + + public function getTimingTemplates() + { + return array( + array('{% stopwatch foo %}something{% endstopwatch %}', 'foo'), + array('{% stopwatch foo %}symfony2 is fun{% endstopwatch %}{% stopwatch bar %}something{% endstopwatch %}', array('foo', 'bar')), + + array('{% stopwatch foo %}something{% endstopwatch foo %}', 'foo'), + + array("{% stopwatch 'foo.bar' %}something{% endstopwatch %}", 'foo.bar'), + ); + } + + protected function getStopwatch($events = array()) + { + $events = is_array($events) ? $events : array($events); + $stopwatch = $this->getMock('Symfony\Component\Stopwatch\Stopwatch'); + + $i = -1; + foreach ($events as $eventName) { + $stopwatch->expects($this->at(++$i)) + ->method('start') + ->with($this->equalTo($eventName), 'template') + ; + $stopwatch->expects($this->at(++$i)) + ->method('stop') + ->with($this->equalTo($eventName)) + ; + } + + return $stopwatch; + } +} diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php new file mode 100644 index 0000000000..bdba8f50cb --- /dev/null +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php @@ -0,0 +1,77 @@ + + * + * 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\StopwatchNode; + +/** + * Token Parser for the stopwatch tag. + * + * @author Wouter J + */ +class StopwatchTokenParser extends \Twig_TokenParser +{ + protected $stopwatchIsAvailable; + protected $_events = array(); + + public function __construct($stopwatchIsAvailable) + { + $this->stopwatchIsAvailable = $stopwatchIsAvailable; + } + + public function parse(\Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + // {% stopwatch bar %} + if ($stream->test(\Twig_Token::NAME_TYPE)) { + $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); + } else { + $name = $stream->expect(\Twig_Token::STRING_TYPE)->getValue(); + } + + if (in_array($name, $this->_events)) { + throw new \Twig_Error_Syntax(sprintf("The stopwatch event '%s' has already been defined", $name)); + } + $this->_events[] = $name; + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + // {% endstopwatch %} or {% endstopwatch bar %} + $body = $this->parser->subparse(array($this, 'decideStopwatchEnd'), true); + if ($stream->test(\Twig_Token::NAME_TYPE) || $stream->test(\Twig_Token::STRING_TYPE)) { + $value = $stream->next()->getValue(); + + if ($name != $value) { + throw new \Twig_Error_Syntax(sprintf("Expected endstopwatch for event '%s' (but %s given)", $name, $value)); + } + } + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + if ($this->stopwatchIsAvailable) { + return new StopwatchNode($name, $body, $lineno, $this->getTag()); + } + + return $body; + } + + public function decideStopwatchEnd(\Twig_Token $token) + { + return $token->test('endstopwatch'); + } + + public function getTag() + { + return 'stopwatch'; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 971f4f17eb..48c0055d9c 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -18,6 +18,7 @@ Symfony\Bridge\Twig\Extension\YamlExtension Symfony\Bridge\Twig\Extension\FormExtension Symfony\Bridge\Twig\Extension\HttpKernelExtension + Symfony\Bridge\Twig\Extension\StopwatchExtension Symfony\Bridge\Twig\Form\TwigRendererEngine Symfony\Bridge\Twig\Form\TwigRenderer Symfony\Bridge\Twig\Translation\TwigExtractor @@ -86,6 +87,11 @@ + + + + +