Added support for named arguments for macros
authorMartin Hasoň <martin.hason@gmail.com>
Sat, 11 May 2013 10:50:59 +0000 (12:50 +0200)
committerMartin Hasoň <martin.hason@gmail.com>
Mon, 29 Jul 2013 21:13:25 +0000 (23:13 +0200)
CHANGELOG
doc/tags/macro.rst
doc/templates.rst
lib/Twig/ExpressionParser.php
lib/Twig/Node/Expression/MacroCall.php
lib/Twig/Node/Module.php
lib/Twig/Template.php
test/Twig/Tests/ExpressionParserTest.php
test/Twig/Tests/Fixtures/macros/named_arguments.test [new file with mode: 0644]
test/Twig/Tests/Node/Expression/MacroCallTest.php [new file with mode: 0644]
test/Twig/Tests/TemplateTest.php

index b2d3978..0736a2b 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 * 1.13.2 (2013-XX-XX)
 
+ * added support for named arguments for macros
  * fixed fatal error that should be an exception when macro does not exist in template
 
 * 1.13.1 (2013-06-06)
index 11c115a..4cb70fe 100644 (file)
@@ -1,6 +1,9 @@
 ``macro``
 =========
 
+.. 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 put often used HTML idioms into reusable elements to not repeat
 yourself.
@@ -15,10 +18,20 @@ Here is a small example of a macro that renders a form element:
 
 Macros differs from native PHP functions in a few ways:
 
-* Default argument values are defined by using the ``default`` filter in the
-  macro body;
+* Arguments of a macro are always optional;
+
+* Default argument values may be defined by using the ``default`` filter in the
+  macro body.
+
+.. tip::
+
+    A macro may define default values for scalar arguments as follows:
+
+    .. code-block:: jinja
 
-* Arguments of a macro are always optional.
+        {% macro input(name, value = "", type = "text", size = 20) %}
+            <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}" />
+        {% endmacro %}
 
 But as with PHP functions, macros don't have access to the current template
 variables.
@@ -46,6 +59,7 @@ The macro can then be called at will:
 
     <p>{{ forms.input('username') }}</p>
     <p>{{ forms.input('password', null, 'password') }}</p>
+    <p>{{ forms.input(name='username', size=40) }}</p>
 
 If macros are defined and used in the same template, you can use the
 special ``_self`` variable to import them:
index 542b8ae..c144702 100644 (file)
@@ -191,13 +191,18 @@ progression of integers:
 Go to the :doc:`functions<functions/index>` page to learn more about the
 built-in functions.
 
+.. _named_arguments:
+
 Named Arguments
 ---------------
 
 .. versionadded:: 1.12
     Support for named arguments was added in Twig 1.12.
 
-Arguments for filters and functions can also be passed as *named arguments*:
+.. versionadded:: 1.13.2
+    Support for named arguments for macros was added in Twig 1.13.2.
+
+Arguments for filters, functions and macros can also be passed as *named arguments*:
 
 .. code-block:: jinja
 
@@ -498,6 +503,9 @@ Macros
 .. versionadded:: 1.12
     Support for default argument values was added in Twig 1.12.
 
+.. versionadded:: 1.13.2
+    Support for macro call with named arguments was added in Twig 1.13.2.
+
 Macros are comparable with functions in regular programming languages. They
 are useful to reuse often used HTML fragments to not repeat yourself.
 
@@ -542,6 +550,14 @@ macro call:
         <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}" />
     {% endmacro %}
 
+Arguments for macro can also be passed as :ref:`named arguments<named_arguments>`:
+
+.. code-block:: jinja
+
+    {% import "forms.html" as forms %}
+
+    <p>{{ forms.input(name='username', size=40) }}</p>
+
 .. _twig-expressions:
 
 Expressions
index 18ac2a3..8565f1b 100644 (file)
@@ -318,13 +318,11 @@ class Twig_ExpressionParser
 
                 return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line);
             default:
+                $args = $this->parseArguments(true);
                 if (null !== $alias = $this->parser->getImportedSymbol('macro', $name)) {
-                    $arguments = $this->createArrayFromArguments($this->parseArguments());
-
-                    return new Twig_Node_Expression_MacroCall($alias['node'], $alias['name'], $arguments, $line);
+                    return new Twig_Node_Expression_MacroCall($alias['node'], $alias['name'], $this->createArrayFromArguments($args), $line);
                 }
 
-                $args = $this->parseArguments(true);
                 $class = $this->getFunctionNodeClass($name, $line);
 
                 return new $class($name, $args, $line);
@@ -357,7 +355,7 @@ class Twig_ExpressionParser
                     throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename());
                 }
 
-                $arguments = $this->createArrayFromArguments($this->parseArguments());
+                $arguments = $this->createArrayFromArguments($this->parseArguments(true));
 
                 return new Twig_Node_Expression_MacroCall($node, $arg->getAttribute('value'), $arguments, $lineno);
             }
index 0618bcc..3e6b8c1 100644 (file)
@@ -23,13 +23,37 @@ class Twig_Node_Expression_MacroCall extends Twig_Node_Expression
 
     public function compile(Twig_Compiler $compiler)
     {
+        $namedNames = array();
+        $namedCount = 0;
+        $positionalCount = 0;
+        foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) {
+            $name = $pair['key']->getAttribute('value');
+            if (!is_int($name)) {
+                $namedCount++;
+                $namedNames[$name] = 1;
+            } elseif ($namedCount > 0) {
+                throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for macro "%s".', $this->getAttribute('name')), $this->lineno);
+            } else {
+                $positionalCount++;
+            }
+        }
+
         $compiler
             ->raw('$this->callMacro(')
             ->subcompile($this->getNode('template'))
-            ->raw(', ')
-            ->repr($this->getAttribute('name'))
-            ->raw(', ')
-            ->subcompile($this->getNode('arguments'))
+            ->raw(', ')->repr($this->getAttribute('name'))
+            ->raw(', ')->subcompile($this->getNode('arguments'))
+        ;
+
+        if ($namedCount > 0) {
+            $compiler
+                ->raw(', ')->repr($namedNames)
+                ->raw(', ')->repr($namedCount)
+                ->raw(', ')->repr($positionalCount)
+            ;
+        }
+
+        $compiler
             ->raw(')')
         ;
     }
index 959e2ff..551458a 100644 (file)
@@ -249,6 +249,15 @@ class Twig_Node_Module extends Twig_Node
                 ->addIndentation()->repr($name)->raw(" => array(\n")
                 ->indent()
                 ->write("'method' => ")->repr($node->getAttribute('method'))->raw(",\n")
+                ->write("'default_argument_values' => array(\n")
+                ->indent()
+            ;
+            foreach ($node->getNode('arguments') as $argument => $value) {
+                $compiler->addIndentation()->repr($argument)->raw (' => ')->subcompile($value)->raw(",\n");
+            }
+            $compiler
+                ->outdent()
+                ->write(")\n")
                 ->outdent()
                 ->write("),\n")
             ;
index 46b8714..afa5b96 100644 (file)
@@ -450,15 +450,20 @@ abstract class Twig_Template implements Twig_TemplateInterface
     /**
      * Calls macro in a template.
      *
-     * @param Twig_Template $template  The template
-     * @param string        $macro     The name of macro
-     * @param array         $arguments The arguments of macro
+     * @param Twig_Template $template        The template
+     * @param string        $macro           The name of macro
+     * @param array         $arguments       The arguments of macro
+     * @param array         $namedNames      An array of names of arguments as keys
+     * @param integer       $namedCount      The count of named arguments
+     * @param integer       $positionalCount The count of positional arguments
      *
      * @return string The content of a macro
      *
      * @throws Twig_Error_Runtime if the macro is not defined
+     * @throws Twig_Error_Runtime if the argument is defined twice
+     * @throws Twig_Error_Runtime if the argument is unknown
      */
-    protected function callMacro(Twig_Template $template, $macro, array $arguments)
+    protected function callMacro(Twig_Template $template, $macro, array $arguments, array $namedNames = array(), $namedCount = 0, $positionalCount = -1)
     {
         if (!isset($template->macros[$macro]['reflection'])) {
             if (!isset($template->macros[$macro])) {
@@ -468,7 +473,37 @@ abstract class Twig_Template implements Twig_TemplateInterface
             $template->macros[$macro]['reflection'] = new ReflectionMethod($template, $template->macros[$macro]['method']);
         }
 
-        return $template->macros[$macro]['reflection']->invokeArgs($template, $arguments);
+        if ($namedCount < 1) {
+            return $template->macros[$macro]['reflection']->invokeArgs($template, $arguments);
+        }
+
+        $i = 0;
+        $args = array();
+        foreach ($template->macros[$macro]['default_argument_values'] as $name => $value) {
+            if (isset($namedNames[$name])) {
+                if ($i < $positionalCount) {
+                    throw new Twig_Error_Runtime(sprintf('Argument "%s" is defined twice for macro "%s" defined in the template "%s".', $name, $macro, $template->getTemplateName()));
+                }
+
+                $args[] = $arguments[$name];
+                if (--$namedCount < 1) {
+                    break;
+                }
+            } elseif ($i < $positionalCount) {
+                $args[] = $arguments[$i];
+            } else {
+                $args[] = $value;
+            }
+
+            $i++;
+        }
+
+        if ($namedCount > 0) {
+            $parameters = array_keys(array_diff_key($namedNames, $template->macros[$macro]['default_argument_values']));
+            throw new Twig_Error_Runtime(sprintf('Unknown argument%s "%s" for macro "%s" defined in the template "%s".', count($parameters) > 1 ? 's' : '' , implode('", "', $parameters), $macro, $template->getTemplateName()));
+        }
+
+        return $template->macros[$macro]['reflection']->invokeArgs($template, $args);
     }
 
     /**
index 8ec6537..7848077 100644 (file)
@@ -227,17 +227,6 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException Twig_Error_Syntax
-     */
-    public function testMacroCallDoesNotSupportNamedArguments()
-    {
-        $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
-        $parser = new Twig_Parser($env);
-
-        $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
      */
diff --git a/test/Twig/Tests/Fixtures/macros/named_arguments.test b/test/Twig/Tests/Fixtures/macros/named_arguments.test
new file mode 100644 (file)
index 0000000..a9d04d5
--- /dev/null
@@ -0,0 +1,31 @@
+--TEST--
+macro with named arguments
+--TEMPLATE--
+{% import _self as test %}
+{% from _self import test %}
+
+{% macro test(a, b = 'bar') -%}
+{{ a }}{{ b }}
+{%- endmacro %}
+
+{{ test(b = 'bar', a = 'foo') }}
+{{ test(b = '2', a = 'foo') }}
+{{ test('bar', b = 'foo') }}
+{{ test.test(b = 1) }}
+{{ test.test(b = 'foo') }}
+{{ test.test(2, b = 'foo') }}
+{{ test.test(3, b = 'foobar') }}
+{{ test.test(a = 1) }}
+{{ test.test(a = 2) }}
+--DATA--
+return array();
+--EXPECT--
+foobar
+foo2
+barfoo
+1
+foo
+2foo
+3foobar
+1bar
+2bar
diff --git a/test/Twig/Tests/Node/Expression/MacroCallTest.php b/test/Twig/Tests/Node/Expression/MacroCallTest.php
new file mode 100644 (file)
index 0000000..2687e6e
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_MacroCallTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException        Twig_Error_Syntax
+     * @expectedExceptionMessage Positional arguments cannot be used after named arguments for macro "foo".
+     */
+    public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments()
+    {
+        $arguments = new Twig_Node_Expression_Array(array(new Twig_Node_Expression_Constant('named', -1), $this->getMock('Twig_Node'), new Twig_Node_Expression_Constant(0, -1), $this->getMock('Twig_Node')), -1);
+        $node = new Twig_Node_Expression_MacroCall($this->getMock('Twig_Node_Expression'), 'foo', $arguments, -1);
+        $node->compile($this->getMock('Twig_Compiler', null, array(), '', false));
+    }
+}
index a022260..1db26d6 100644 (file)
@@ -371,6 +371,45 @@ class Twig_Tests_TemplateTest extends PHPUnit_Framework_TestCase
         $template = new Twig_TemplateTest($this->getMock('Twig_Environment'));
         $template->callMacro(new Twig_Tests_TemplateWithMacros('my/template'), 'foo', array());
     }
+
+    /**
+     * @expectedException Twig_Error_Runtime
+     * @expectedExceptionMessage Argument "format" is defined twice for macro "date" defined in the template "my/template".
+     */
+    public function testCallMacroWhenArgumentIsDefinedTwice()
+    {
+        $template = new Twig_TemplateTest($this->getMock('Twig_Environment'));
+        $template->callMacro(new Twig_Tests_TemplateWithMacros('my/template', array('date' => array(
+            'method' => 'getDate',
+            'default_argument_values' => array('format' => null, 'template' => null)
+        ))), 'date', array('d', 'format' => 'H'), array('format' => 1), 1, 1);
+    }
+
+    /**
+     * @expectedException        Twig_Error_Runtime
+     * @expectedExceptionMessage Unknown argument "unknown" for macro "date" defined in the template "my/template".
+     */
+    public function testCallMacroWithWrongNamedArgumentName()
+    {
+        $template = new Twig_TemplateTest($this->getMock('Twig_Environment'));
+        $template->callMacro(new Twig_Tests_TemplateWithMacros('my/template', array('date' => array(
+            'method' => 'getDate',
+            'default_argument_values' => array('foo' => 1, 'bar' => 2)
+        ))), 'date', array('foo' => 2), array('foo' => 1, 'unknown' => 1), 2, 0);
+    }
+
+    /**
+     * @expectedException        Twig_Error_Runtime
+     * @expectedExceptionMessage Unknown arguments "unknown1", "unknown2" for macro "date" defined in the template "my/template".
+     */
+    public function testCallMacroWithWrongNamedArgumentNames()
+    {
+        $template = new Twig_TemplateTest($this->getMock('Twig_Environment'));
+        $template->callMacro(new Twig_Tests_TemplateWithMacros('my/template', array('date' => array(
+            'method' => 'getDate',
+            'default_argument_values' => array()
+        ))), 'date', array(), array('unknown1' => 1, 'unknown2' => 2), 2, 0);
+    }
 }
 
 class Twig_TemplateTest extends Twig_Template
@@ -430,9 +469,9 @@ class Twig_TemplateTest extends Twig_Template
         }
     }
 
-    public function callMacro(Twig_Template $template, $macro, array $arguments)
+    public function callMacro(Twig_Template $template, $macro, array $arguments, array $namedNames = array(), $namedCount = 0, $positionalCount = -1)
     {
-        return parent::callMacro($template, $macro, $arguments);
+        return parent::callMacro($template, $macro, $arguments, $namedNames, $namedCount, $positionalCount);
     }
 }