From 0f51a54fc902536ae7304ae8691d27d5e0b91120 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 9 Jun 2010 17:15:52 +0200 Subject: [PATCH] added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? minimum : base %}) --- CHANGELOG | 1 + doc/02-Twig-for-Template-Designers.markdown | 21 +++++++ doc/06-Recipes.markdown | 31 +-------- lib/Twig/Node/Block.php | 2 +- lib/Twig/Node/BlockReference.php | 2 +- lib/Twig/Node/Expression/Filter.php | 2 +- lib/Twig/Node/Module.php | 74 +++++++++++++++++------ lib/Twig/Node/Parent.php | 2 +- lib/Twig/Node/SandboxedModule.php | 8 +- lib/Twig/Parser.php | 18 ++---- lib/Twig/Template.php | 30 +++++++++ lib/Twig/TokenParser/Extends.php | 3 +- test/Twig/Tests/Node/BlockReferenceTest.php | 2 +- test/Twig/Tests/Node/BlockTest.php | 2 +- test/Twig/Tests/Node/Expression/FilterTest.php | 2 +- test/Twig/Tests/Node/ModuleTest.php | 60 +++++++++++++++---- test/Twig/Tests/Node/ParentTest.php | 2 +- test/Twig/Tests/Node/SandboxedModuleTest.php | 21 ++++--- test/fixtures/tags/inheritance/conditional.test | 14 ++++ test/fixtures/tags/inheritance/dynamic.test | 14 ++++ 20 files changed, 221 insertions(+), 90 deletions(-) create mode 100644 test/fixtures/tags/inheritance/conditional.test create mode 100644 test/fixtures/tags/inheritance/dynamic.test diff --git a/CHANGELOG b/CHANGELOG index f357805..76594a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ Backward incompatibilities: * removed the sandboxed attribute of the include tag (use the new sandbox tag instead) * refactored the Node system (if you have custom nodes, you will have to update them to use the new API) + * added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? "minimum" : "base" %}) * added a grammar sub-framework to ease the creation of custom tags * fixed the for tag for large arrays (some loop variables are now only available for arrays and objects that implement the Countable interface) * removed the Twig_Resource::resolveMissingFilter() method diff --git a/doc/02-Twig-for-Template-Designers.markdown b/doc/02-Twig-for-Template-Designers.markdown index 79fe666..2d841ee 100644 --- a/doc/02-Twig-for-Template-Designers.markdown +++ b/doc/02-Twig-for-Template-Designers.markdown @@ -313,6 +313,27 @@ following constructs do the same: [twig] {% block title page_title|title %} +### Dynamic Inheritance (as of Twig 0.9.7) + +Twig supports dynamic inheritance by using a variable as the base template: + + [twig] + {% extends some_var %} + +If the variable evaluates to a `Twig_Template` object, Twig will use it as the +parent template. + +### Conditional Inheritance (as of Twig 0.9.7) + +As a matter of fact, the template name can be any valid expression. So, it's +also possible to make the inheritance mechanism conditional: + + [twig] + {% extends standalone ? "minimum.html" : "base.html" %} + +In this example, the template will extend the "minimum.html" layout template +if the `standalone` variable evaluates to `true`, and "base.html" otherwise. + Import Context Behavior ----------------------- diff --git a/doc/06-Recipes.markdown b/doc/06-Recipes.markdown index 1425cc5..e618a1f 100644 --- a/doc/06-Recipes.markdown +++ b/doc/06-Recipes.markdown @@ -5,40 +5,17 @@ Making a Layout conditional --------------------------- Working with Ajax means that the same content is sometimes displayed as is, -and sometimes decorated with a layout. But as Twig templates are compiled as -PHP classes, wrapping an `extends` tag with an `if` tag does not work: +and sometimes decorated with a layout. As Twig layout template names can be +any valid expression, you can pass a variable that evaluates to `true` when +the request is made via Ajax and choose the layout accordingly: [twig] - {# this does not work #} - - {% if request.ajax %} - {% extends "base.html" %} - {% endif %} + {% extends request.ajax ? "base_ajax.html" : "base.html" %} {% block content %} This is the content to be displayed. {% endblock %} -One way to solve this problem is to have two different templates: - - [twig] - {# index.html #} - {% extends "layout.html" %} - - {% block content %} - {% include "index_for_ajax.html" %} - {% endblock %} - - - {# index_for_ajax.html #} - This is the content to be displayed. - -Now, the decision to display one of the template is the responsibility of the -controller: - - [php] - $twig->render($request->isAjax() ? 'index_for_ajax.html' : 'index.html'); - Making an Include dynamic ------------------------- diff --git a/lib/Twig/Node/Block.php b/lib/Twig/Node/Block.php index 1655f86..ac22ced 100644 --- a/lib/Twig/Node/Block.php +++ b/lib/Twig/Node/Block.php @@ -28,7 +28,7 @@ class Twig_Node_Block extends Twig_Node { $compiler ->addDebugInfo($this) - ->write(sprintf("public function block_%s(\$context)\n", $this['name']), "{\n") + ->write(sprintf("public function block_%s(\$context, \$parents)\n", $this['name']), "{\n") ->indent() ; diff --git a/lib/Twig/Node/BlockReference.php b/lib/Twig/Node/BlockReference.php index 6213404..38d1e14 100644 --- a/lib/Twig/Node/BlockReference.php +++ b/lib/Twig/Node/BlockReference.php @@ -28,7 +28,7 @@ class Twig_Node_BlockReference extends Twig_Node { $compiler ->addDebugInfo($this) - ->write(sprintf('$this->block_%s($context);'."\n", $this['name'])) + ->write(sprintf("\$this->getBlock('%s', \$context);\n", $this['name'])) ; } } diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index 3f5eec9..9d7c856 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -27,7 +27,7 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression if (!isset($filterMap[$name])) { throw new Twig_SyntaxError(sprintf('The filter "%s" does not exist', $name), $this->getLine()); } else { - $compiler->raw($filterMap[$name]->compile().($filterMap[$name]->needsEnvironment() ? '($this->getEnvironment(), ' : '(')); + $compiler->raw($filterMap[$name]->compile().($filterMap[$name]->needsEnvironment() ? '($this->env, ' : '(')); } $postponed[] = $attrs; } diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php index 8070417..ed98ac6 100644 --- a/lib/Twig/Node/Module.php +++ b/lib/Twig/Node/Module.php @@ -19,9 +19,9 @@ */ class Twig_Node_Module extends Twig_Node { - public function __construct(Twig_NodeInterface $body, $extends, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, $filename) + public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, $filename) { - parent::__construct(array('body' => $body, 'blocks' => $blocks, 'macros' => $macros), array('filename' => $filename, 'extends' => $extends), 1); + parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros), array('filename' => $filename), 1); } public function compile($compiler) @@ -34,6 +34,10 @@ class Twig_Node_Module extends Twig_Node { $this->compileClassHeader($compiler); + if (count($this->blocks)) { + $this->compileConstructor($compiler); + } + $this->compileDisplayHeader($compiler); $this->compileDisplayBody($compiler); @@ -49,7 +53,7 @@ class Twig_Node_Module extends Twig_Node protected function compileDisplayBody($compiler) { - if (null !== $this['extends']) { + if (null !== $this->parent) { // remove all but import nodes foreach ($this->body as $node) { if ($node instanceof Twig_Node_Import) { @@ -57,9 +61,29 @@ class Twig_Node_Module extends Twig_Node } } + if ($this->parent instanceof Twig_Node_Expression_Constant) { + $compiler + ->write("\$this->parent = \$this->env->loadTemplate(") + ->subcompile($this->parent) + ->raw(");\n") + ; + } else { + $compiler + ->write("\$this->parent = ") + ->subcompile($this->parent) + ->raw(";\n") + ->write("if (!\$this->parent") + ->raw(" instanceof Twig_Template) {\n") + ->indent() + ->write("\$this->parent = \$this->env->loadTemplate(\$this->parent);\n") + ->outdent() + ->write("}\n") + ; + } + $compiler - ->raw("\n") - ->write("parent::display(\$context);\n") + ->write("\$this->parent->pushBlocks(\$this->blocks);\n") + ->write("\$this->parent->display(\$context);\n") ; } else { $compiler->subcompile($this->body); @@ -68,29 +92,43 @@ class Twig_Node_Module extends Twig_Node protected function compileClassHeader($compiler) { - $compiler->write("write('$this->loadTemplate(') - ->repr($this['extends']) - ->raw(");\n\n") - ; - } - $compiler + ->write("write("/* ".str_replace('*/', '* /', $this['filename'])." */\n") ->write('class '.$compiler->getEnvironment()->getTemplateClass($this['filename'])) + ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass())) + ->write("{\n") + ->indent() ; - $parent = null === $this['extends'] ? $compiler->getEnvironment()->getBaseTemplateClass() : $compiler->getEnvironment()->getTemplateClass($this['extends']); + if (null !== $this->parent) { + $compiler->write("protected \$parent;\n\n"); + } + } + protected function compileConstructor($compiler) + { $compiler - ->raw(" extends $parent\n") - ->write("{\n") + ->write("public function __construct(Twig_Environment \$env)\n", "{\n") + ->indent() + ->write("parent::__construct(\$env);\n\n") + ->write("\$this->blocks = array(\n") ->indent() ; + + foreach ($this->blocks as $name => $node) { + $compiler + ->write(sprintf("'%s' => array(array(\$this, 'block_%s')),\n", $name, $name)) + ; + } + + $compiler + ->outdent() + ->write(");\n") + ->outdent() + ->write("}\n\n"); + ; } protected function compileDisplayHeader($compiler) diff --git a/lib/Twig/Node/Parent.php b/lib/Twig/Node/Parent.php index fd2f490..93c5fa3 100644 --- a/lib/Twig/Node/Parent.php +++ b/lib/Twig/Node/Parent.php @@ -28,7 +28,7 @@ class Twig_Node_Parent extends Twig_Node { $compiler ->addDebugInfo($this) - ->write('parent::block_'.$this['name'].'($context);'."\n") + ->write("\$this->getParent(\$context, \$parents);\n") ; } } diff --git a/lib/Twig/Node/SandboxedModule.php b/lib/Twig/Node/SandboxedModule.php index 1ff09f6..c2f3cb7 100644 --- a/lib/Twig/Node/SandboxedModule.php +++ b/lib/Twig/Node/SandboxedModule.php @@ -24,7 +24,7 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module public function __construct(Twig_Node_Module $node, array $usedFilters, array $usedTags) { - parent::__construct($node->body, $node['extends'], $node->blocks, $node->macros, $node['filename'], $node->getLine(), $node->getNodeTag()); + parent::__construct($node->body, $node->parent, $node->blocks, $node->macros, $node['filename'], $node->getLine(), $node->getNodeTag()); $this->usedFilters = $usedFilters; $this->usedTags = $usedTags; @@ -32,7 +32,7 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module protected function compileDisplayBody($compiler) { - if (null === $this['extends']) { + if (null === $this->parent) { $compiler->write("\$this->checkSecurity();\n"); } @@ -54,10 +54,10 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module ->write(");\n") ; - if (null !== $this['extends']) { + if (null !== $this->parent) { $compiler ->raw("\n") - ->write("parent::checkSecurity();\n") + ->write("\$this->parent->checkSecurity();\n") ; } diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index ccc714e..56e2f3e 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -12,7 +12,7 @@ class Twig_Parser implements Twig_ParserInterface { protected $stream; - protected $extends; + protected $parent; protected $handlers; protected $visitors; protected $expressionParser; @@ -60,7 +60,7 @@ class Twig_Parser implements Twig_ParserInterface } $this->stream = $stream; - $this->extends = null; + $this->parent = null; $this->blocks = array(); $this->macros = array(); $this->blockStack = array(); @@ -75,7 +75,7 @@ class Twig_Parser implements Twig_ParserInterface throw $e; } - if (!is_null($this->extends)) { + if (null !== $this->parent) { // check that the body only contains block references and empty text nodes foreach ($body as $node) { @@ -87,13 +87,9 @@ class Twig_Parser implements Twig_ParserInterface throw new Twig_SyntaxError('A template that extends another one cannot have a body', $node->getLine(), $this->stream->getFilename()); } } - - foreach ($this->blocks as $block) { - $block['parent'] = $this->extends; - } } - $node = new Twig_Node_Module($body, $this->extends, new Twig_Node($this->blocks), new Twig_Node($this->macros), $this->stream->getFilename()); + $node = new Twig_Node_Module($body, $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), $this->stream->getFilename()); $traverser = new Twig_NodeTraverser($this->env, $this->visitors); @@ -212,12 +208,12 @@ class Twig_Parser implements Twig_ParserInterface public function getParent() { - return $this->extends; + return $this->parent; } - public function setParent($extends) + public function setParent($parent) { - $this->extends = $extends; + $this->parent = $parent; } public function getStream() diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index b455151..812a1b3 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -11,6 +11,36 @@ */ abstract class Twig_Template extends Twig_Resource implements Twig_TemplateInterface { + protected $blocks; + + public function __construct(Twig_Environment $env) + { + parent::__construct($env); + + $this->blocks = array(); + } + + protected function getBlock($name, array $context) + { + return call_user_func($this->blocks[$name][0], $context, array_slice($this->blocks[$name], 1)); + } + + protected function getParent($context, $parents) + { + return call_user_func($parents[0], $context, array_slice($parents, 0)); + } + + public function pushBlocks($blocks) + { + foreach ($blocks as $name => $call) { + if (!isset($this->blocks[$name])) { + $this->blocks[$name] = array(); + } + + $this->blocks[$name] = array_merge($call, $this->blocks[$name]); + } + } + /** * Renders the template with the given context and returns it as string. * diff --git a/lib/Twig/TokenParser/Extends.php b/lib/Twig/TokenParser/Extends.php index 037d57a..69722fe 100644 --- a/lib/Twig/TokenParser/Extends.php +++ b/lib/Twig/TokenParser/Extends.php @@ -23,7 +23,8 @@ class Twig_TokenParser_Extends extends Twig_TokenParser if (null !== $this->parser->getParent()) { throw new Twig_SyntaxError('Multiple extends tags are forbidden', $token->getLine()); } - $this->parser->setParent($this->parser->getStream()->expect(Twig_Token::STRING_TYPE)->getValue()); + $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); return null; diff --git a/test/Twig/Tests/Node/BlockReferenceTest.php b/test/Twig/Tests/Node/BlockReferenceTest.php index cc5e74b..77e3986 100644 --- a/test/Twig/Tests/Node/BlockReferenceTest.php +++ b/test/Twig/Tests/Node/BlockReferenceTest.php @@ -35,7 +35,7 @@ class Twig_Tests_Node_BlockReferenceTest extends Twig_Tests_Node_TestCase public function getTests() { return array( - array(new Twig_Node_BlockReference('foo', 0), '$this->block_foo($context);'), + array(new Twig_Node_BlockReference('foo', 0), '$this->getBlock(\'foo\', $context);'), ); } } diff --git a/test/Twig/Tests/Node/BlockTest.php b/test/Twig/Tests/Node/BlockTest.php index a6470ef..6c4d86c 100644 --- a/test/Twig/Tests/Node/BlockTest.php +++ b/test/Twig/Tests/Node/BlockTest.php @@ -41,7 +41,7 @@ class Twig_Tests_Node_BlockTest extends Twig_Tests_Node_TestCase return array( array($node, <<getEnvironment(), twig_upper_filter($this->getEnvironment(), "foo"), "bar", "foobar")'); + $tests[] = array($node, 'twig_lower_filter($this->env, twig_upper_filter($this->env, "foo"), "bar", "foobar")'); return $tests; } diff --git a/test/Twig/Tests/Node/ModuleTest.php b/test/Twig/Tests/Node/ModuleTest.php index cea65ac..837a298 100644 --- a/test/Twig/Tests/Node/ModuleTest.php +++ b/test/Twig/Tests/Node/ModuleTest.php @@ -19,17 +19,17 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase public function testConstructor() { $body = new Twig_Node_Text('foo', 0); - $extends = 'layout.twig'; + $parent = new Twig_Node_Expression_Constant('layout.twig', 0); $blocks = new Twig_Node(); $macros = new Twig_Node(); $filename = 'foo.twig'; - $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename); + $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $filename); $this->assertEquals($body, $node->body); $this->assertEquals($blocks, $node->blocks); $this->assertEquals($macros, $node->macros); + $this->assertEquals($parent, $node->parent); $this->assertEquals($filename, $node['filename']); - $this->assertEquals($extends, $node['extends']); } /** @@ -89,25 +89,63 @@ EOF $import = new Twig_Node_Import(new Twig_Node_Expression_Constant('foo.twig', 0), new Twig_Node_Expression_AssignName('macro', 0), 0); $body = new Twig_Node(array($import, new Twig_Node_Text('foo', 0))); - $extends = 'layout.twig'; - $blocks = new Twig_Node(); - $macros = new Twig_Node(); - $filename = 'foo.twig'; + $extends = new Twig_Node_Expression_Constant('layout.twig', 0); $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename); $tests[] = array($node, <<loadTemplate("layout.twig"); - /* foo.twig */ -class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8fb9d03f55738ff78518e1bc2741faf +class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template { + protected \$parent; + public function display(array \$context) { \$context['macro'] = \$this->env->loadTemplate("foo.twig", true); + \$this->parent = \$this->env->loadTemplate("layout.twig"); + \$this->parent->pushBlocks(\$this->blocks); + \$this->parent->display(\$context); + } - parent::display(\$context); + public function getName() + { + return "foo.twig"; + } + +} + +class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34_Macro extends Twig_Macro +{ +} +EOF + , $twig); + + $body = new Twig_Node_Text('foo', 0); + $extends = new Twig_Node_Expression_Conditional( + new Twig_Node_Expression_Constant(true, 0), + new Twig_Node_Expression_Constant('foo', 0), + new Twig_Node_Expression_Constant('foo', 0), + 0 + ); + + $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename); + $tests[] = array($node, <<parent = (true) ? ("foo") : ("foo"); + if (!\$this->parent instanceof Twig_Template) { + \$this->parent = \$this->env->loadTemplate(\$this->parent); + } + \$this->parent->pushBlocks(\$this->blocks); + \$this->parent->display(\$context); } public function getName() diff --git a/test/Twig/Tests/Node/ParentTest.php b/test/Twig/Tests/Node/ParentTest.php index 51f5419..8ea761c 100644 --- a/test/Twig/Tests/Node/ParentTest.php +++ b/test/Twig/Tests/Node/ParentTest.php @@ -35,7 +35,7 @@ class Twig_Tests_Node_ParentTest extends Twig_Tests_Node_TestCase public function getTests() { $tests = array(); - $tests[] = array(new Twig_Node_Parent('foo', 0), 'parent::block_foo($context);'); + $tests[] = array(new Twig_Node_Parent('foo', 0), '$this->getParent($context, $parents);'); return $tests; } diff --git a/test/Twig/Tests/Node/SandboxedModuleTest.php b/test/Twig/Tests/Node/SandboxedModuleTest.php index 3ef84c1..ac476db 100644 --- a/test/Twig/Tests/Node/SandboxedModuleTest.php +++ b/test/Twig/Tests/Node/SandboxedModuleTest.php @@ -19,18 +19,18 @@ class Twig_Tests_Node_SandboxedModuleTest extends Twig_Tests_Node_TestCase public function testConstructor() { $body = new Twig_Node_Text('foo', 0); - $extends = 'layout.twig'; + $parent = new Twig_Node_Expression_Constant('layout.twig', 0); $blocks = new Twig_Node(); $macros = new Twig_Node(); $filename = 'foo.twig'; - $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename); + $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $filename); $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper')); $this->assertEquals($body, $node->body); $this->assertEquals($blocks, $node->blocks); $this->assertEquals($macros, $node->macros); + $this->assertEquals($parent, $node->parent); $this->assertEquals($filename, $node['filename']); - $this->assertEquals($extends, $node['extends']); } /** @@ -92,7 +92,7 @@ EOF , $twig); $body = new Twig_Node_Text('foo', 0); - $extends = 'layout.twig'; + $extends = new Twig_Node_Expression_Constant('layout.twig', 0); $blocks = new Twig_Node(); $macros = new Twig_Node(); $filename = 'foo.twig'; @@ -103,15 +103,16 @@ EOF $tests[] = array($node, <<loadTemplate("layout.twig"); - /* foo.twig */ -class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8fb9d03f55738ff78518e1bc2741faf +class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template { + protected \$parent; + public function display(array \$context) { - - parent::display(\$context); + \$this->parent = \$this->env->loadTemplate("layout.twig"); + \$this->parent->pushBlocks(\$this->blocks); + \$this->parent->display(\$context); } protected function checkSecurity() { @@ -120,7 +121,7 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8f array('for') ); - parent::checkSecurity(); + \$this->parent->checkSecurity(); } public function getName() diff --git a/test/fixtures/tags/inheritance/conditional.test b/test/fixtures/tags/inheritance/conditional.test new file mode 100644 index 0000000..3be8c47 --- /dev/null +++ b/test/fixtures/tags/inheritance/conditional.test @@ -0,0 +1,14 @@ +--TEST-- +"extends" tag +--TEMPLATE-- +{% extends standalone ? foo : 'bar.twig' %} + +{% block content %}{% parent %}FOO{% endblock %} +--TEMPLATE(foo.twig)-- +{% block content %}FOO{% endblock %} +--TEMPLATE(bar.twig)-- +{% block content %}BAR{% endblock %} +--DATA-- +return array('foo' => 'foo.twig', 'standalone' => true) +--EXPECT-- +FOOFOO diff --git a/test/fixtures/tags/inheritance/dynamic.test b/test/fixtures/tags/inheritance/dynamic.test new file mode 100644 index 0000000..ee06ddc --- /dev/null +++ b/test/fixtures/tags/inheritance/dynamic.test @@ -0,0 +1,14 @@ +--TEST-- +"extends" tag +--TEMPLATE-- +{% extends foo %} + +{% block content %} +FOO +{% endblock %} +--TEMPLATE(foo.twig)-- +{% block content %}{% endblock %} +--DATA-- +return array('foo' => 'foo.twig') +--EXPECT-- +FOO -- 1.7.2.5