added the ability to set default values for macro arguments (closes #447)

This commit is contained in:
Fabien Potencier
2012-11-15 10:41:23 +01:00
parent a59dcde3c2
commit 4647913e50
8 changed files with 158 additions and 17 deletions
+1
View File
@@ -1,5 +1,6 @@
* 1.12.0 (2012-XX-XX)
* added the ability to set default values for macro arguments
* added support for named arguments for filters, tests, and functions
* moved filters/functions/tests syntax errors to the parser
* added support for extended ternary operator syntaxes
+12
View File
@@ -493,6 +493,9 @@ For bigger sections it makes sense to mark a block :doc:`raw<tags/raw>`.
Macros
------
.. versionadded:: 1.12
Support for default argument values was added in Twig 1.12.
Macros are comparable with functions in regular programming languages. They
are useful to reuse often used HTML fragments to not repeat yourself.
@@ -528,6 +531,15 @@ current namespace via the :doc:`from<tags/from>` tag and optionally alias them:
<dd>{{ input_field('password', '', 'password') }}</dd>
</dl>
A default value can also be defined for macro arguments when not provided in a
macro call:
.. code-block:: jinja
{% macro input(name, value = "", type = "text", size = 20) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}" />
{% endmacro %}
Expressions
-----------
+44 -6
View File
@@ -452,22 +452,44 @@ class Twig_ExpressionParser
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
}
$value = $this->parseExpression();
if ($definition) {
$token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name');
$value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else {
$value = $this->parseExpression();
}
$name = null;
if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) {
$token = $stream->next();
if (!$value instanceof Twig_Node_Expression_Name) {
throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine());
throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename());
}
$name = $value->getAttribute('name');
$value = $definition ? $this->parsePrimaryExpression() : $this->parseExpression();
if ($definition) {
$value = $this->parsePrimaryExpression();
if (!$this->checkConstantExpression($value)) {
throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename());
}
} else {
$value = $this->parseExpression();
}
}
if (null === $name) {
$args[] = $value;
} else {
if ($definition) {
if (null === $name) {
$name = $value->getAttribute('name');
$value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine());
}
$args[$name] = $value;
} else {
if (null === $name) {
$args[] = $value;
} else {
$args[$name] = $value;
}
}
}
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
@@ -539,4 +561,20 @@ class Twig_ExpressionParser
return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
}
// checks that the node only contains "constant" elements
protected function checkConstantExpression(Twig_NodeInterface $node)
{
if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) {
return false;
}
foreach ($node as $n) {
if (!$this->checkConstantExpression($n)) {
return false;
}
}
return true;
}
}
+21 -8
View File
@@ -29,14 +29,27 @@ class Twig_Node_Macro extends Twig_Node
*/
public function compile(Twig_Compiler $compiler)
{
$arguments = array();
foreach ($this->getNode('arguments') as $argument) {
$arguments[] = '$_'.$argument->getAttribute('name').' = null';
$compiler
->addDebugInfo($this)
->write(sprintf("public function get%s(", $this->getAttribute('name')))
;
$count = count($this->getNode('arguments'));
$pos = 0;
foreach ($this->getNode('arguments') as $name => $default) {
$compiler
->raw('$_'.$name.' = ')
->subcompile($default)
;
if (++$pos < $count) {
$compiler->raw(', ');
}
}
$compiler
->addDebugInfo($this)
->write(sprintf("public function get%s(%s)\n", $this->getAttribute('name'), implode(', ', $arguments)), "{\n")
->raw(")\n")
->write("{\n")
->indent()
;
@@ -48,11 +61,11 @@ class Twig_Node_Macro extends Twig_Node
->indent()
;
foreach ($this->getNode('arguments') as $argument) {
foreach ($this->getNode('arguments') as $name => $default) {
$compiler
->write('')
->string($argument->getAttribute('name'))
->raw(' => $_'.$argument->getAttribute('name'))
->string($name)
->raw(' => $_'.$name)
->raw(",\n")
;
}
+1 -1
View File
@@ -33,7 +33,7 @@ class Twig_TokenParser_Macro extends Twig_TokenParser
$stream = $this->parser->getStream();
$name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
$arguments = $this->parser->getExpressionParser()->parseArguments();
$arguments = $this->parser->getExpressionParser()->parseArguments(true, true);
$stream->expect(Twig_Token::BLOCK_END_TYPE);
$this->parser->pushLocalScope();
+57
View File
@@ -237,6 +237,63 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
$parser->parse($env->tokenize('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'));
}
/**
* @expectedException Twig_Error_Syntax
* @expectedExceptionMessage An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1
*/
public function testMacroDefinitionDoesNotSupportNonNameVariableName()
{
$env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
$parser = new Twig_Parser($env);
$parser->parse($env->tokenize('{% macro foo("a") %}{% endmacro %}', 'index'));
}
/**
* @expectedException Twig_Error_Syntax
* @expectedExceptionMessage A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1
* @dataProvider getMacroDefinitionDoesNotSupportNonConstantDefaultValues
*/
public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template)
{
$env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
$parser = new Twig_Parser($env);
$parser->parse($env->tokenize($template, 'index'));
}
public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues()
{
return array(
array('{% macro foo(name = "a #{foo} a") %}{% endmacro %}'),
array('{% macro foo(name = [["b", "a #{foo} a"]]) %}{% endmacro %}'),
);
}
/**
* @dataProvider getMacroDefinitionSupportsConstantDefaultValues
*/
public function testMacroDefinitionSupportsConstantDefaultValues($template)
{
$env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
$parser = new Twig_Parser($env);
$parser->parse($env->tokenize($template, 'index'));
}
public function getMacroDefinitionSupportsConstantDefaultValues()
{
return array(
array('{% macro foo(name = "aa") %}{% endmacro %}'),
array('{% macro foo(name = 12) %}{% endmacro %}'),
array('{% macro foo(name = true) %}{% endmacro %}'),
array('{% macro foo(name = ["a"]) %}{% endmacro %}'),
array('{% macro foo(name = [["a"]]) %}{% endmacro %}'),
array('{% macro foo(name = {a: "a"}) %}{% endmacro %}'),
array('{% macro foo(name = {a: {b: "a"}}) %}{% endmacro %}'),
);
}
/**
* @expectedException Twig_Error_Syntax
* @expectedExceptionMessage The function "cycl" does not exist. Did you mean "cycle" in "index" at line 1
@@ -0,0 +1,16 @@
--TEST--
macro
--TEMPLATE--
{% from _self import test %}
{% macro test(a, b = 'bar') -%}
{{ a }}{{ b }}
{%- endmacro %}
{{ test('foo') }}
{{ test('bar', 'foo') }}
--DATA--
return array();
--EXPECT--
foobar
barfoo
+6 -2
View File
@@ -37,16 +37,20 @@ class Twig_Tests_Node_MacroTest extends Twig_Test_NodeTestCase
public function getTests()
{
$body = new Twig_Node_Text('foo', 1);
$arguments = new Twig_Node(array(new Twig_Node_Expression_Name('foo', 1)), array(), 1);
$arguments = new Twig_Node(array(
'foo' => new Twig_Node_Expression_Constant(null, 1),
'bar' => new Twig_Node_Expression_Constant('Foo', 1),
), array(), 1);
$node = new Twig_Node_Macro('foo', $body, $arguments, 1);
return array(
array($node, <<<EOF
// line 1
public function getfoo(\$_foo = null)
public function getfoo(\$_foo = null, \$_bar = "Foo")
{
\$context = \$this->env->mergeGlobals(array(
"foo" => \$_foo,
"bar" => \$_bar,
));
\$blocks = array();