Added support for named arguments for macros

This commit is contained in:
Martin Hasoň
2013-05-11 12:50:59 +02:00
parent 09fedb80f8
commit 63615a6cc3
11 changed files with 211 additions and 31 deletions
+1
View File
@@ -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)
+17 -3
View 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;
* 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
{% 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:
+17 -1
View 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
+3 -5
View 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);
}
+28 -4
View 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(')')
;
}
+9
View 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")
;
+40 -5
View 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);
}
/**
-11
View File
@@ -226,17 +226,6 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
$parser->parse($env->tokenize('{{ foo.bar(name="Foo") }}', 'index'));
}
/**
* @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
@@ -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
@@ -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));
}
}
+41 -2
View 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);
}
}