* 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)
``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.
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.
<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:
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
.. 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.
<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
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);
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);
}
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(')')
;
}
->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")
;
/**
* 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])) {
$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);
}
/**
}
/**
- * @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
*/
--- /dev/null
+--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
--- /dev/null
+<?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));
+ }
+}
$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
}
}
- 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);
}
}