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