added support for traits (experimental)
authorFabien Potencier <fabien.potencier@gmail.com>
Wed, 13 Apr 2011 08:23:46 +0000 (10:23 +0200)
committerFabien Potencier <fabien.potencier@gmail.com>
Thu, 28 Apr 2011 14:22:09 +0000 (16:22 +0200)
Traits are "special" templates that define blocks that you can "include" in
other templates.

Let's take an example:

    {# 'foo' template defines the 'foo' block #}
    {% block 'foo' %}
        FOO
    {% endblock %}

    {# 'main' template defines the 'bar' block and include the 'foo' block from the 'foo' template #}
    {% extends 'base' %}

    {% use 'foo' %}

    {% block 'bar' %}
        BAR
    {% endblock %}

In the previous example, the 'main' template use the 'foo' one. It means that
the 'foo' block defined in the 'foo' template is available as if it were
defined in the 'main' template.

You can use as many 'use' statements as you want in a template:

    {% extends 'base' %}

    {% use 'foo' %}
    {% use 'bar' %}
    {% use 'foobar' %}

If two templates define the same block, the latest one wins. The template can
also overrides any block.

The 'use' tag also supports "dynamic" names:

    {% set foo = 'foo' %}

    {% use foo %}

The 'use' tag only imports a template if it does not extend another template,
if it does not define macros, and if the body is empty. But it can 'use' other
templates:

    {# 'foo' template #}
    {% block 'foo' %}
        FOO
    {% endblock %}

    {# 'bar' template #}
    {% use 'foo' %}

    {% block 'bar' %}
        BAR
    {% endblock %}

    {# 'main' template #}
    {% extends 'base' %}

    {% use 'bar' %}

In this example, the 'main' template has access to both 'foo' and 'bar'.

Traits are mainly useful when you consider blocks as reusable "functions";
like we do in Symfony2 forms or if you use Twig for code generation.

lib/Twig/Extension/Core.php
lib/Twig/Node/Module.php
lib/Twig/Node/SandboxedModule.php
lib/Twig/Parser.php
lib/Twig/Template.php
lib/Twig/TokenParser/Use.php [new file with mode: 0644]
test/Twig/Tests/Node/ModuleTest.php
test/Twig/Tests/Node/SandboxedModuleTest.php

index ad7494f..07ceea0 100644 (file)
@@ -23,6 +23,7 @@ class Twig_Extension_Core extends Twig_Extension
             new Twig_TokenParser_Extends(),
             new Twig_TokenParser_Include(),
             new Twig_TokenParser_Block(),
+            new Twig_TokenParser_Use(),
             new Twig_TokenParser_Filter(),
             new Twig_TokenParser_Macro(),
             new Twig_TokenParser_Import(),
index e8d8913..6f80e10 100644 (file)
@@ -18,9 +18,9 @@
  */
 class Twig_Node_Module extends Twig_Node
 {
-    public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, $filename)
+    public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $filename)
     {
-        parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros), array('filename' => $filename), 1);
+        parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits), array('filename' => $filename), 1);
     }
 
     /**
@@ -37,7 +37,7 @@ class Twig_Node_Module extends Twig_Node
     {
         $this->compileClassHeader($compiler);
 
-        if (count($this->getNode('blocks'))) {
+        if (count($this->getNode('blocks')) || count($this->getNode('traits'))) {
             $this->compileConstructor($compiler);
         }
 
@@ -55,6 +55,8 @@ class Twig_Node_Module extends Twig_Node
 
         $this->compileGetTemplateName($compiler);
 
+        $this->compileIsTraitable($compiler);
+
         $this->compileClassFooter($compiler);
     }
 
@@ -143,7 +145,47 @@ class Twig_Node_Module extends Twig_Node
             ->write("public function __construct(Twig_Environment \$env)\n", "{\n")
             ->indent()
             ->write("parent::__construct(\$env);\n\n")
-            ->write("\$this->blocks = array(\n")
+        ;
+
+        if (count($this->getNode('traits'))) {
+            // traits
+            foreach ($this->getNode('traits') as $i => $node) {
+                $compiler
+                    ->write(sprintf('$_trait_%s = $this->env->loadTemplate(', $i))
+                    ->subcompile($node)
+                    ->raw(");\n")
+                    ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i))
+                    ->indent()
+                    ->write("throw new Twig_Error_Runtime('Template \"'.")
+                    ->subcompile($node)
+                    ->raw(".'\" cannot be used as a trait.');\n")
+                    ->outdent()
+                    ->write("}\n\n");
+                ;
+            }
+
+            $compiler
+                ->write("\$this->blocks = array_replace(\n")
+                ->indent()
+            ;
+
+            for ($i = count($this->getNode('traits')) - 1; $i >= 0; --$i) {
+                $compiler
+                    ->write(sprintf("\$_trait_%s->getBlocks(),\n", $i))
+                ;
+            }
+
+            $compiler
+                ->write("array(\n")
+            ;
+        } else {
+            $compiler
+                ->write("\$this->blocks = array(\n")
+            ;
+        }
+
+        // blocks
+        $compiler
             ->indent()
         ;
 
@@ -153,6 +195,13 @@ class Twig_Node_Module extends Twig_Node
             ;
         }
 
+        if (count($this->getNode('traits'))) {
+            $compiler
+                ->outdent()
+                ->write(")\n")
+            ;
+        }
+
         $compiler
             ->outdent()
             ->write(");\n")
@@ -199,6 +248,44 @@ class Twig_Node_Module extends Twig_Node
             ->repr($this->getAttribute('filename'))
             ->raw(";\n")
             ->outdent()
+            ->write("}\n\n")
+        ;
+    }
+
+    protected function compileIsTraitable(Twig_Compiler $compiler)
+    {
+        // A template can be used as a trait if:
+        //   * it has no parent
+        //   * it has no macros
+        //   * it has no body
+        //
+        // Put another way, a template can be used as a trait if it
+        // only contains blocks and use statements.
+        $traitable = null === $this->getNode('parent') && 0 === count($this->getNode('macros'));
+        if ($traitable) {
+            if (!count($nodes = $this->getNode('body'))) {
+                $nodes = new Twig_Node(array($this->getNode('body')));
+            }
+
+            foreach ($nodes as $node) {
+                if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) {
+                    continue;
+                }
+
+                if ($node instanceof Twig_Node_BlockReference) {
+                    continue;
+                }
+
+                $traitable = false;
+                break;
+            }
+        }
+
+        $compiler
+            ->write("public function isTraitable()\n", "{\n")
+            ->indent()
+            ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false'))
+            ->outdent()
             ->write("}\n")
         ;
     }
index 35c7aff..a797cf4 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, array $usedFunctions)
     {
-        parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag());
+        parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag());
 
         $this->usedFilters = $usedFilters;
         $this->usedTags = $usedTags;
index e7ea3fc..3f97184 100644 (file)
@@ -30,6 +30,7 @@ class Twig_Parser implements Twig_ParserInterface
     protected $reservedMacroNames;
     protected $importedFunctions;
     protected $tmpVarCount;
+    protected $traits;
 
     /**
      * Constructor.
@@ -72,6 +73,7 @@ class Twig_Parser implements Twig_ParserInterface
         $this->parent = null;
         $this->blocks = array();
         $this->macros = array();
+        $this->traits = array();
         $this->blockStack = array();
         $this->importedFunctions = array(array());
 
@@ -89,7 +91,7 @@ class Twig_Parser implements Twig_ParserInterface
             throw $e;
         }
 
-        $node = new Twig_Node_Module($body, $this->parent, 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), new Twig_Node($this->traits), $this->stream->getFilename());
 
         $traverser = new Twig_NodeTraverser($this->env, $this->visitors);
 
@@ -221,6 +223,11 @@ class Twig_Parser implements Twig_ParserInterface
         $this->macros[$name] = $node;
     }
 
+    public function addTrait($name)
+    {
+        $this->traits[] = $name;
+    }
+
     public function addImportedFunction($alias, $name, Twig_Node_Expression $node)
     {
         $this->importedFunctions[0][$alias] = array('name' => $name, 'node' => $node);
index 4e1c6f3..4484293 100644 (file)
@@ -157,6 +157,16 @@ abstract class Twig_Template implements Twig_TemplateInterface
     }
 
     /**
+     * Returns all blocks.
+     *
+     * @return array An array of blocks
+     */
+    public function getBlocks()
+    {
+        return $this->blocks;
+    }
+
+    /**
      * Displays the template with the given context.
      *
      * @param array $context An array of parameters to pass to the template
diff --git a/lib/Twig/TokenParser/Use.php b/lib/Twig/TokenParser/Use.php
new file mode 100644 (file)
index 0000000..a368611
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2011 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Use extends Twig_TokenParser
+{
+    /**
+     * Parses a token and returns a node.
+     *
+     * @param Twig_Token $token A Twig_Token instance
+     *
+     * @return Twig_NodeInterface A Twig_NodeInterface instance
+     */
+    public function parse(Twig_Token $token)
+    {
+        $this->parser->addTrait($this->parser->getExpressionParser()->parseExpression());
+
+        $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+        return null;
+    }
+
+    /**
+     * Gets the tag name associated with this token parser.
+     *
+     * @param string The tag name
+     */
+    public function getTag()
+    {
+        return 'use';
+    }
+}
index dc9079c..f928800 100644 (file)
@@ -22,8 +22,9 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase
         $parent = new Twig_Node_Expression_Constant('layout.twig', 0);
         $blocks = new Twig_Node();
         $macros = new Twig_Node();
+        $traits = new Twig_Node();
         $filename = 'foo.twig';
-        $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, $filename);
 
         $this->assertEquals($body, $node->getNode('body'));
         $this->assertEquals($blocks, $node->getNode('blocks'));
@@ -58,9 +59,10 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase
         $extends = null;
         $blocks = new Twig_Node();
         $macros = new Twig_Node();
+        $traits = new Twig_Node();
         $filename = 'foo.twig';
 
-        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename);
         $tests[] = array($node, <<<EOF
 <?php
 
@@ -78,6 +80,11 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
     {
         return "foo.twig";
     }
+
+    public function isTraitable()
+    {
+        return false;
+    }
 }
 EOF
         , $twig);
@@ -87,7 +94,7 @@ EOF
         $body = new Twig_Node(array($import, new Twig_Node_Text('foo', 0)));
         $extends = new Twig_Node_Expression_Constant('layout.twig', 0);
 
-        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename);
         $tests[] = array($node, <<<EOF
 <?php
 
@@ -117,6 +124,11 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
     {
         return "foo.twig";
     }
+
+    public function isTraitable()
+    {
+        return false;
+    }
 }
 EOF
         , $twig);
@@ -129,7 +141,7 @@ EOF
                         0
                     );
 
-        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename);
         $tests[] = array($node, <<<EOF
 <?php
 
@@ -161,6 +173,11 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
     {
         return "foo.twig";
     }
+
+    public function isTraitable()
+    {
+        return false;
+    }
 }
 EOF
         , $twig);
index ada4bdf..097574e 100644 (file)
@@ -22,8 +22,9 @@ class Twig_Tests_Node_SandboxedModuleTest extends Twig_Tests_Node_TestCase
         $parent = new Twig_Node_Expression_Constant('layout.twig', 0);
         $blocks = new Twig_Node();
         $macros = new Twig_Node();
+        $traits = new Twig_Node();
         $filename = 'foo.twig';
-        $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, $filename);
         $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
 
         $this->assertEquals($body, $node->getNode('body'));
@@ -54,9 +55,10 @@ class Twig_Tests_Node_SandboxedModuleTest extends Twig_Tests_Node_TestCase
         $extends = null;
         $blocks = new Twig_Node();
         $macros = new Twig_Node();
+        $traits = new Twig_Node();
         $filename = 'foo.twig';
 
-        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename);
         $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
 
         $tests[] = array($node, <<<EOF
@@ -85,6 +87,11 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
     {
         return "foo.twig";
     }
+
+    public function isTraitable()
+    {
+        return false;
+    }
 }
 EOF
         , $twig);
@@ -93,9 +100,10 @@ EOF
         $extends = new Twig_Node_Expression_Constant('layout.twig', 0);
         $blocks = new Twig_Node();
         $macros = new Twig_Node();
+        $traits = new Twig_Node();
         $filename = 'foo.twig';
 
-        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
+        $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename);
         $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
 
         $tests[] = array($node, <<<EOF
@@ -136,6 +144,11 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
     {
         return "foo.twig";
     }
+
+    public function isTraitable()
+    {
+        return false;
+    }
 }
 EOF
         , $twig);