* 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
[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
-----------------------
---------------------------
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
-------------------------
{
$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()
;
{
$compiler
->addDebugInfo($this)
- ->write(sprintf('$this->block_%s($context);'."\n", $this['name']))
+ ->write(sprintf("\$this->getBlock('%s', \$context);\n", $this['name']))
;
}
}
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;
}
*/
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)
{
$this->compileClassHeader($compiler);
+ if (count($this->blocks)) {
+ $this->compileConstructor($compiler);
+ }
+
$this->compileDisplayHeader($compiler);
$this->compileDisplayBody($compiler);
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) {
}
}
+ 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);
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)
{
$compiler
->addDebugInfo($this)
- ->write('parent::block_'.$this['name'].'($context);'."\n")
+ ->write("\$this->getParent(\$context, \$parents);\n")
;
}
}
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;
protected function compileDisplayBody($compiler)
{
- if (null === $this['extends']) {
+ if (null === $this->parent) {
$compiler->write("\$this->checkSecurity();\n");
}
->write(");\n")
;
- if (null !== $this['extends']) {
+ if (null !== $this->parent) {
$compiler
->raw("\n")
- ->write("parent::checkSecurity();\n")
+ ->write("\$this->parent->checkSecurity();\n")
;
}
class Twig_Parser implements Twig_ParserInterface
{
protected $stream;
- protected $extends;
+ protected $parent;
protected $handlers;
protected $visitors;
protected $expressionParser;
}
$this->stream = $stream;
- $this->extends = null;
+ $this->parent = null;
$this->blocks = array();
$this->macros = array();
$this->blockStack = array();
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)
{
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);
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()
*/
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.
*
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;
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);'),
);
}
}
return array(
array($node, <<<EOF
-public function block_foo(\$context)
+public function block_foo(\$context, \$parents)
{
echo "foo";
}
), 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;
}
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']);
}
/**
$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()
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;
}
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']);
}
/**
, $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';
$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() {
array('for')
);
- parent::checkSecurity();
+ \$this->parent->checkSecurity();
}
public function getName()
--- /dev/null
+--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
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends foo %}
+
+{% block content %}
+FOO
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}{% endblock %}
+--DATA--
+return array('foo' => 'foo.twig')
+--EXPECT--
+FOO