From: Fabien Potencier Date: Wed, 13 Apr 2011 08:23:46 +0000 (+0200) Subject: added support for traits (experimental) X-Git-Url: http://git.silmor.de/gitweb/?a=commitdiff_plain;h=221eb48ea822fbab35303ef7522653475f90b49d;p=konrad%2Ftwig.git added support for traits (experimental) 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. --- diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index ad7494f..07ceea0 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -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(), diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php index e8d8913..6f80e10 100644 --- a/lib/Twig/Node/Module.php +++ b/lib/Twig/Node/Module.php @@ -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") ; } diff --git a/lib/Twig/Node/SandboxedModule.php b/lib/Twig/Node/SandboxedModule.php index 35c7aff..a797cf4 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, 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; diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index e7ea3fc..3f97184 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -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); diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 4e1c6f3..4484293 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -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 index 0000000..a368611 --- /dev/null +++ b/lib/Twig/TokenParser/Use.php @@ -0,0 +1,38 @@ +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'; + } +} diff --git a/test/Twig/Tests/Node/ModuleTest.php b/test/Twig/Tests/Node/ModuleTest.php index dc9079c..f928800 100644 --- a/test/Twig/Tests/Node/ModuleTest.php +++ b/test/Twig/Tests/Node/ModuleTest.php @@ -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, <<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, <<