added support for dynamic and conditional inheritance ({% extends some_var %} and...
authorFabien Potencier <fabien.potencier@gmail.com>
Wed, 9 Jun 2010 15:15:52 +0000 (17:15 +0200)
committerFabien Potencier <fabien.potencier@gmail.com>
Wed, 9 Jun 2010 21:05:45 +0000 (23:05 +0200)
20 files changed:
CHANGELOG
doc/02-Twig-for-Template-Designers.markdown
doc/06-Recipes.markdown
lib/Twig/Node/Block.php
lib/Twig/Node/BlockReference.php
lib/Twig/Node/Expression/Filter.php
lib/Twig/Node/Module.php
lib/Twig/Node/Parent.php
lib/Twig/Node/SandboxedModule.php
lib/Twig/Parser.php
lib/Twig/Template.php
lib/Twig/TokenParser/Extends.php
test/Twig/Tests/Node/BlockReferenceTest.php
test/Twig/Tests/Node/BlockTest.php
test/Twig/Tests/Node/Expression/FilterTest.php
test/Twig/Tests/Node/ModuleTest.php
test/Twig/Tests/Node/ParentTest.php
test/Twig/Tests/Node/SandboxedModuleTest.php
test/fixtures/tags/inheritance/conditional.test [new file with mode: 0644]
test/fixtures/tags/inheritance/dynamic.test [new file with mode: 0644]

index f357805..76594a0 100644 (file)
--- 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
index 79fe666..2d841ee 100644 (file)
@@ -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
 -----------------------
 
index 1425cc5..e618a1f 100644 (file)
@@ -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
 -------------------------
 
index 1655f86..ac22ced 100644 (file)
@@ -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()
         ;
 
index 6213404..38d1e14 100644 (file)
@@ -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']))
         ;
     }
 }
index 3f5eec9..9d7c856 100644 (file)
@@ -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;
         }
index 8070417..ed98ac6 100644 (file)
@@ -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("<?php\n\n");
-
-        if (null !== $this['extends']) {
-            $compiler
-                ->write('$this->loadTemplate(')
-                ->repr($this['extends'])
-                ->raw(");\n\n")
-            ;
-        }
-
         $compiler
+            ->write("<?php\n\n")
             // if the filename contains */, add a blank to avoid a PHP parse error
             ->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)
index fd2f490..93c5fa3 100644 (file)
@@ -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")
         ;
     }
 }
index 1ff09f6..c2f3cb7 100644 (file)
@@ -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")
             ;
         }
 
index ccc714e..56e2f3e 100644 (file)
@@ -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()
index b455151..812a1b3 100644 (file)
  */
 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.
      *
index 037d57a..69722fe 100644 (file)
@@ -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;
index cc5e74b..77e3986 100644 (file)
@@ -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);'),
         );
     }
 }
index a6470ef..6c4d86c 100644 (file)
@@ -41,7 +41,7 @@ class Twig_Tests_Node_BlockTest extends Twig_Tests_Node_TestCase
 
         return array(
             array($node, <<<EOF
-public function block_foo(\$context)
+public function block_foo(\$context, \$parents)
 {
     echo "foo";
 }
index 77f4d5f..24b21bb 100644 (file)
@@ -163,7 +163,7 @@ class Twig_Tests_Node_Expression_FilterTest extends Twig_Tests_Node_TestCase
         ), array(), 0);
         $node = new Twig_Node_Expression_Filter($expr, $filters, 0);
 
-        $tests[] = array($node, 'twig_lower_filter($this->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;
     }
index cea65ac..837a298 100644 (file)
@@ -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, <<<EOF
 <?php
 
-\$this->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, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+    protected \$parent;
+
+    public function display(array \$context)
+    {
+        \$this->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()
index 51f5419..8ea761c 100644 (file)
@@ -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;
     }
index 3ef84c1..ac476db 100644 (file)
@@ -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, <<<EOF
 <?php
 
-\$this->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 (file)
index 0000000..3be8c47
--- /dev/null
@@ -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 (file)
index 0000000..ee06ddc
--- /dev/null
@@ -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