added the ability to set default values for macro arguments (closes #447)
authorFabien Potencier <fabien.potencier@gmail.com>
Thu, 15 Nov 2012 09:41:23 +0000 (10:41 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Thu, 15 Nov 2012 11:59:42 +0000 (12:59 +0100)
CHANGELOG
doc/templates.rst
lib/Twig/ExpressionParser.php
lib/Twig/Node/Macro.php
lib/Twig/TokenParser/Macro.php
test/Twig/Tests/ExpressionParserTest.php
test/Twig/Tests/Fixtures/macros/default_values.test [new file with mode: 0644]
test/Twig/Tests/Node/MacroTest.php

index 6faeb42..fffef7e 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
index 602588a..1bcb8ee 100644 (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
 -----------
 
index b572197..6a555e1 100644 (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;
+    }
 }
index 8bb5d9d..347e4b2 100644 (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")
                 ;
             }
index de10059..c2a0336 100644 (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();
index e3fb732..8ec6537 100644 (file)
@@ -239,6 +239,63 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
 
     /**
      * @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
      */
     public function testUnknownFunction()
diff --git a/test/Twig/Tests/Fixtures/macros/default_values.test b/test/Twig/Tests/Fixtures/macros/default_values.test
new file mode 100644 (file)
index 0000000..4ccff7b
--- /dev/null
@@ -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
index 39e8131..4d2f641 100644 (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();