added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? minimum : base %})

This commit is contained in:
Fabien Potencier
2010-06-09 17:15:52 +02:00
parent 9519a084f1
commit 0f51a54fc9
20 changed files with 224 additions and 93 deletions
+1
View File
@@ -5,6 +5,7 @@ Backward incompatibilities:
* removed the sandboxed attribute of the include tag (use the new sandbox tag instead) * 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) * 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 * 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) * 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 * removed the Twig_Resource::resolveMissingFilter() method
@@ -313,6 +313,27 @@ following constructs do the same:
[twig] [twig]
{% block title page_title|title %} {% 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 Import Context Behavior
----------------------- -----------------------
+4 -27
View File
@@ -5,40 +5,17 @@ Making a Layout conditional
--------------------------- ---------------------------
Working with Ajax means that the same content is sometimes displayed as is, 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 and sometimes decorated with a layout. As Twig layout template names can be
PHP classes, wrapping an `extends` tag with an `if` tag does not work: 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] [twig]
{# this does not work #} {% extends request.ajax ? "base_ajax.html" : "base.html" %}
{% if request.ajax %}
{% extends "base.html" %}
{% endif %}
{% block content %} {% block content %}
This is the content to be displayed. This is the content to be displayed.
{% endblock %} {% 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 Making an Include dynamic
------------------------- -------------------------
+1 -1
View File
@@ -28,7 +28,7 @@ class Twig_Node_Block extends Twig_Node
{ {
$compiler $compiler
->addDebugInfo($this) ->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() ->indent()
; ;
+1 -1
View File
@@ -28,7 +28,7 @@ class Twig_Node_BlockReference extends Twig_Node
{ {
$compiler $compiler
->addDebugInfo($this) ->addDebugInfo($this)
->write(sprintf('$this->block_%s($context);'."\n", $this['name'])) ->write(sprintf("\$this->getBlock('%s', \$context);\n", $this['name']))
; ;
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression
if (!isset($filterMap[$name])) { if (!isset($filterMap[$name])) {
throw new Twig_SyntaxError(sprintf('The filter "%s" does not exist', $name), $this->getLine()); throw new Twig_SyntaxError(sprintf('The filter "%s" does not exist', $name), $this->getLine());
} else { } else {
$compiler->raw($filterMap[$name]->compile().($filterMap[$name]->needsEnvironment() ? '($this->getEnvironment(), ' : '(')); $compiler->raw($filterMap[$name]->compile().($filterMap[$name]->needsEnvironment() ? '($this->env, ' : '('));
} }
$postponed[] = $attrs; $postponed[] = $attrs;
} }
+59 -21
View File
@@ -19,9 +19,9 @@
*/ */
class Twig_Node_Module extends Twig_Node 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) public function compile($compiler)
@@ -34,6 +34,10 @@ class Twig_Node_Module extends Twig_Node
{ {
$this->compileClassHeader($compiler); $this->compileClassHeader($compiler);
if (count($this->blocks)) {
$this->compileConstructor($compiler);
}
$this->compileDisplayHeader($compiler); $this->compileDisplayHeader($compiler);
$this->compileDisplayBody($compiler); $this->compileDisplayBody($compiler);
@@ -49,7 +53,7 @@ class Twig_Node_Module extends Twig_Node
protected function compileDisplayBody($compiler) protected function compileDisplayBody($compiler)
{ {
if (null !== $this['extends']) { if (null !== $this->parent) {
// remove all but import nodes // remove all but import nodes
foreach ($this->body as $node) { foreach ($this->body as $node) {
if ($node instanceof Twig_Node_Import) { 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 $compiler
->raw("\n") ->write("\$this->parent->pushBlocks(\$this->blocks);\n")
->write("parent::display(\$context);\n") ->write("\$this->parent->display(\$context);\n")
; ;
} else { } else {
$compiler->subcompile($this->body); $compiler->subcompile($this->body);
@@ -68,28 +92,42 @@ class Twig_Node_Module extends Twig_Node
protected function compileClassHeader($compiler) protected function compileClassHeader($compiler)
{ {
$compiler->write("<?php\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()
;
if (null !== $this['extends']) { if (null !== $this->parent) {
$compiler->write("protected \$parent;\n\n");
}
}
protected function compileConstructor($compiler)
{
$compiler
->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 $compiler
->write('$this->loadTemplate(') ->write(sprintf("'%s' => array(array(\$this, 'block_%s')),\n", $name, $name))
->repr($this['extends'])
->raw(");\n\n")
; ;
} }
$compiler $compiler
// if the filename contains */, add a blank to avoid a PHP parse error ->outdent()
->write("/* ".str_replace('*/', '* /', $this['filename'])." */\n") ->write(");\n")
->write('class '.$compiler->getEnvironment()->getTemplateClass($this['filename'])) ->outdent()
; ->write("}\n\n");
$parent = null === $this['extends'] ? $compiler->getEnvironment()->getBaseTemplateClass() : $compiler->getEnvironment()->getTemplateClass($this['extends']);
$compiler
->raw(" extends $parent\n")
->write("{\n")
->indent()
; ;
} }
+1 -1
View File
@@ -28,7 +28,7 @@ class Twig_Node_Parent extends Twig_Node
{ {
$compiler $compiler
->addDebugInfo($this) ->addDebugInfo($this)
->write('parent::block_'.$this['name'].'($context);'."\n") ->write("\$this->getParent(\$context, \$parents);\n")
; ;
} }
} }
+4 -4
View File
@@ -24,7 +24,7 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module
public function __construct(Twig_Node_Module $node, array $usedFilters, array $usedTags) 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->usedFilters = $usedFilters;
$this->usedTags = $usedTags; $this->usedTags = $usedTags;
@@ -32,7 +32,7 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module
protected function compileDisplayBody($compiler) protected function compileDisplayBody($compiler)
{ {
if (null === $this['extends']) { if (null === $this->parent) {
$compiler->write("\$this->checkSecurity();\n"); $compiler->write("\$this->checkSecurity();\n");
} }
@@ -54,10 +54,10 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module
->write(");\n") ->write(");\n")
; ;
if (null !== $this['extends']) { if (null !== $this->parent) {
$compiler $compiler
->raw("\n") ->raw("\n")
->write("parent::checkSecurity();\n") ->write("\$this->parent->checkSecurity();\n")
; ;
} }
+7 -11
View File
@@ -12,7 +12,7 @@
class Twig_Parser implements Twig_ParserInterface class Twig_Parser implements Twig_ParserInterface
{ {
protected $stream; protected $stream;
protected $extends; protected $parent;
protected $handlers; protected $handlers;
protected $visitors; protected $visitors;
protected $expressionParser; protected $expressionParser;
@@ -60,7 +60,7 @@ class Twig_Parser implements Twig_ParserInterface
} }
$this->stream = $stream; $this->stream = $stream;
$this->extends = null; $this->parent = null;
$this->blocks = array(); $this->blocks = array();
$this->macros = array(); $this->macros = array();
$this->blockStack = array(); $this->blockStack = array();
@@ -75,7 +75,7 @@ class Twig_Parser implements Twig_ParserInterface
throw $e; throw $e;
} }
if (!is_null($this->extends)) { if (null !== $this->parent) {
// check that the body only contains block references and empty text nodes // check that the body only contains block references and empty text nodes
foreach ($body as $node) 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()); 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); $traverser = new Twig_NodeTraverser($this->env, $this->visitors);
@@ -212,12 +208,12 @@ class Twig_Parser implements Twig_ParserInterface
public function getParent() 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() public function getStream()
+30
View File
@@ -11,6 +11,36 @@
*/ */
abstract class Twig_Template extends Twig_Resource implements Twig_TemplateInterface 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. * Renders the template with the given context and returns it as string.
* *
+2 -1
View File
@@ -23,7 +23,8 @@ class Twig_TokenParser_Extends extends Twig_TokenParser
if (null !== $this->parser->getParent()) { if (null !== $this->parser->getParent()) {
throw new Twig_SyntaxError('Multiple extends tags are forbidden', $token->getLine()); 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); $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
return null; return null;
+1 -1
View File
@@ -35,7 +35,7 @@ class Twig_Tests_Node_BlockReferenceTest extends Twig_Tests_Node_TestCase
public function getTests() public function getTests()
{ {
return array( return array(
array(new Twig_Node_BlockReference('foo', 0), '$this->block_foo($context);'), array(new Twig_Node_BlockReference('foo', 0), '$this->getBlock(\'foo\', $context);'),
); );
} }
} }
+1 -1
View File
@@ -41,7 +41,7 @@ class Twig_Tests_Node_BlockTest extends Twig_Tests_Node_TestCase
return array( return array(
array($node, <<<EOF array($node, <<<EOF
public function block_foo(\$context) public function block_foo(\$context, \$parents)
{ {
echo "foo"; echo "foo";
} }
@@ -163,7 +163,7 @@ class Twig_Tests_Node_Expression_FilterTest extends Twig_Tests_Node_TestCase
), array(), 0); ), array(), 0);
$node = new Twig_Node_Expression_Filter($expr, $filters, 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; return $tests;
} }
+49 -11
View File
@@ -19,17 +19,17 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase
public function testConstructor() public function testConstructor()
{ {
$body = new Twig_Node_Text('foo', 0); $body = new Twig_Node_Text('foo', 0);
$extends = 'layout.twig'; $parent = new Twig_Node_Expression_Constant('layout.twig', 0);
$blocks = new Twig_Node(); $blocks = new Twig_Node();
$macros = new Twig_Node(); $macros = new Twig_Node();
$filename = 'foo.twig'; $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($body, $node->body);
$this->assertEquals($blocks, $node->blocks); $this->assertEquals($blocks, $node->blocks);
$this->assertEquals($macros, $node->macros); $this->assertEquals($macros, $node->macros);
$this->assertEquals($parent, $node->parent);
$this->assertEquals($filename, $node['filename']); $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); $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))); $body = new Twig_Node(array($import, 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';
$node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename); $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $filename);
$tests[] = array($node, <<<EOF $tests[] = array($node, <<<EOF
<?php <?php
\$this->loadTemplate("layout.twig");
/* foo.twig */ /* foo.twig */
class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8fb9d03f55738ff78518e1bc2741faf class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
{ {
protected \$parent;
public function display(array \$context) public function display(array \$context)
{ {
\$context['macro'] = \$this->env->loadTemplate("foo.twig", true); \$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() public function getName()
+1 -1
View File
@@ -35,7 +35,7 @@ class Twig_Tests_Node_ParentTest extends Twig_Tests_Node_TestCase
public function getTests() public function getTests()
{ {
$tests = array(); $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; return $tests;
} }
+11 -10
View File
@@ -19,18 +19,18 @@ class Twig_Tests_Node_SandboxedModuleTest extends Twig_Tests_Node_TestCase
public function testConstructor() public function testConstructor()
{ {
$body = new Twig_Node_Text('foo', 0); $body = new Twig_Node_Text('foo', 0);
$extends = 'layout.twig'; $parent = new Twig_Node_Expression_Constant('layout.twig', 0);
$blocks = new Twig_Node(); $blocks = new Twig_Node();
$macros = new Twig_Node(); $macros = new Twig_Node();
$filename = 'foo.twig'; $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')); $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'));
$this->assertEquals($body, $node->body); $this->assertEquals($body, $node->body);
$this->assertEquals($blocks, $node->blocks); $this->assertEquals($blocks, $node->blocks);
$this->assertEquals($macros, $node->macros); $this->assertEquals($macros, $node->macros);
$this->assertEquals($parent, $node->parent);
$this->assertEquals($filename, $node['filename']); $this->assertEquals($filename, $node['filename']);
$this->assertEquals($extends, $node['extends']);
} }
/** /**
@@ -92,7 +92,7 @@ EOF
, $twig); , $twig);
$body = new Twig_Node_Text('foo', 0); $body = new Twig_Node_Text('foo', 0);
$extends = 'layout.twig'; $extends = new Twig_Node_Expression_Constant('layout.twig', 0);
$blocks = new Twig_Node(); $blocks = new Twig_Node();
$macros = new Twig_Node(); $macros = new Twig_Node();
$filename = 'foo.twig'; $filename = 'foo.twig';
@@ -103,15 +103,16 @@ EOF
$tests[] = array($node, <<<EOF $tests[] = array($node, <<<EOF
<?php <?php
\$this->loadTemplate("layout.twig");
/* foo.twig */ /* foo.twig */
class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8fb9d03f55738ff78518e1bc2741faf class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
{ {
protected \$parent;
public function display(array \$context) public function display(array \$context)
{ {
\$this->parent = \$this->env->loadTemplate("layout.twig");
parent::display(\$context); \$this->parent->pushBlocks(\$this->blocks);
\$this->parent->display(\$context);
} }
protected function checkSecurity() { protected function checkSecurity() {
@@ -120,7 +121,7 @@ class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends __TwigTemplate_d8f
array('for') array('for')
); );
parent::checkSecurity(); \$this->parent->checkSecurity();
} }
public function getName() public function getName()
+14
View File
@@ -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
+14
View File
@@ -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