From: Martin HasoĊˆ Date: Sat, 11 May 2013 10:50:59 +0000 (+0200) Subject: Added support for named arguments for macros X-Git-Url: http://git.silmor.de/gitweb/?a=commitdiff_plain;h=63615a6cc38de8d031439c4fe2f6c8fbeedae8d7;p=web%2Fkonrad%2Ftwig.git Added support for named arguments for macros --- diff --git a/CHANGELOG b/CHANGELOG index b2d3978..0736a2b 100644 --- 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) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index 11c115a..4cb70fe 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -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) %} + + {% 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:

{{ forms.input('username') }}

{{ forms.input('password', null, 'password') }}

+

{{ forms.input(name='username', size=40) }}

If macros are defined and used in the same template, you can use the special ``_self`` variable to import them: diff --git a/doc/templates.rst b/doc/templates.rst index 542b8ae..c144702 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -191,13 +191,18 @@ progression of integers: Go to the :doc:`functions` 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: {% endmacro %} +Arguments for macro can also be passed as :ref:`named arguments`: + +.. code-block:: jinja + + {% import "forms.html" as forms %} + +

{{ forms.input(name='username', size=40) }}

+ .. _twig-expressions: Expressions diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index 18ac2a3..8565f1b 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -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); } diff --git a/lib/Twig/Node/Expression/MacroCall.php b/lib/Twig/Node/Expression/MacroCall.php index 0618bcc..3e6b8c1 100644 --- a/lib/Twig/Node/Expression/MacroCall.php +++ b/lib/Twig/Node/Expression/MacroCall.php @@ -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(')') ; } diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php index 959e2ff..551458a 100644 --- a/lib/Twig/Node/Module.php +++ b/lib/Twig/Node/Module.php @@ -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") ; diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 46b8714..afa5b96 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -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); } /** diff --git a/test/Twig/Tests/ExpressionParserTest.php b/test/Twig/Tests/ExpressionParserTest.php index 8ec6537..7848077 100644 --- a/test/Twig/Tests/ExpressionParserTest.php +++ b/test/Twig/Tests/ExpressionParserTest.php @@ -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 index 0000000..a9d04d5 --- /dev/null +++ b/test/Twig/Tests/Fixtures/macros/named_arguments.test @@ -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 index 0000000..2687e6e --- /dev/null +++ b/test/Twig/Tests/Node/Expression/MacroCallTest.php @@ -0,0 +1,24 @@ +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)); + } +} diff --git a/test/Twig/Tests/TemplateTest.php b/test/Twig/Tests/TemplateTest.php index a022260..1db26d6 100644 --- a/test/Twig/Tests/TemplateTest.php +++ b/test/Twig/Tests/TemplateTest.php @@ -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); } }