From a59dcde3c2ae9293b449078b9a371f0450e045a9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 15 Nov 2012 07:55:50 +0100 Subject: [PATCH] added support for named arguments for filters, tests, and functions --- CHANGELOG | 1 + doc/filters/convert_encoding.rst | 6 ++ doc/filters/date.rst | 6 ++ doc/filters/date_modify.rst | 5 + doc/filters/default.rst | 5 + doc/filters/escape.rst | 6 ++ doc/filters/join.rst | 5 + doc/filters/json_encode.rst | 5 + doc/filters/number_format.rst | 7 ++ doc/filters/replace.rst | 5 + doc/filters/slice.rst | 7 ++ doc/filters/split.rst | 6 ++ doc/filters/trim.rst | 5 + doc/functions/cycle.rst | 5 + doc/functions/date.rst | 6 ++ doc/functions/dump.rst | 5 + doc/functions/random.rst | 5 + doc/functions/range.rst | 7 ++ doc/functions/template_from_string.rst | 5 + doc/templates.rst | 50 +++++++++++ lib/Twig/ExpressionParser.php | 39 +++++++-- lib/Twig/Extension/Core.php | 12 ++-- lib/Twig/Filter.php | 8 ++- lib/Twig/Filter/Function.php | 2 + lib/Twig/Filter/Method.php | 2 + lib/Twig/FilterCallableInterface.php | 21 +++++ lib/Twig/Function.php | 8 ++- lib/Twig/Function/Function.php | 2 + lib/Twig/Function/Method.php | 2 + lib/Twig/FunctionCallableInterface.php | 21 +++++ lib/Twig/Node/Expression/Call.php | 87 +++++++++++++++++++- lib/Twig/Node/Expression/Filter.php | 8 ++- lib/Twig/Node/Expression/Function.php | 8 ++- lib/Twig/Node/Expression/Test.php | 9 ++- lib/Twig/Test.php | 34 ++++++++ lib/Twig/Test/Function.php | 8 ++- lib/Twig/Test/Method.php | 8 ++- lib/Twig/Test/Node.php | 6 +- lib/Twig/TestCallableInterface.php | 21 +++++ test/Twig/Tests/ExpressionParserTest.php | 22 +++++ .../Tests/Fixtures/filters/date_namedargs.test | 15 ++++ test/Twig/Tests/Fixtures/filters/reverse.test | 6 ++ .../Tests/Fixtures/functions/date_namedargs.test | 11 +++ test/Twig/Tests/Fixtures/functions/range.test | 8 ++ test/Twig/Tests/Node/Expression/FilterTest.php | 56 +++++++++++++ test/Twig/Tests/Node/Expression/FunctionTest.php | 7 ++ 46 files changed, 557 insertions(+), 26 deletions(-) create mode 100644 lib/Twig/FilterCallableInterface.php create mode 100644 lib/Twig/FunctionCallableInterface.php create mode 100644 lib/Twig/Test.php create mode 100644 lib/Twig/TestCallableInterface.php create mode 100644 test/Twig/Tests/Fixtures/filters/date_namedargs.test create mode 100644 test/Twig/Tests/Fixtures/functions/date_namedargs.test create mode 100644 test/Twig/Tests/Fixtures/functions/range.test diff --git a/CHANGELOG b/CHANGELOG index ce2854d..6faeb42 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 1.12.0 (2012-XX-XX) + * 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 diff --git a/doc/filters/convert_encoding.rst b/doc/filters/convert_encoding.rst index 2febab2..1b0eb60 100644 --- a/doc/filters/convert_encoding.rst +++ b/doc/filters/convert_encoding.rst @@ -18,5 +18,11 @@ is the input charset: them must be installed. In case both are installed, `mbstring`_ is used by default (Twig before 1.8.1 uses `iconv`_ by default). +Arguments +--------- + + * ``from``: The input charset + * ``to``: The output charset + .. _`iconv`: http://php.net/iconv .. _`mbstring`: http://php.net/mbstring diff --git a/doc/filters/date.rst b/doc/filters/date.rst index 39ad732..8e2f31f 100644 --- a/doc/filters/date.rst +++ b/doc/filters/date.rst @@ -77,6 +77,12 @@ The default timezone can also be set globally by calling ``setTimezone()``: $twig = new Twig_Environment($loader); $twig->getExtension('core')->setTimezone('Europe/Paris'); +Arguments +--------- + + * ``format``: The date format + * ``timezone``: The date timezone + .. _`strtotime`: http://www.php.net/strtotime .. _`DateTime`: http://www.php.net/DateTime .. _`DateInterval`: http://www.php.net/DateInterval diff --git a/doc/filters/date_modify.rst b/doc/filters/date_modify.rst index ae12c52..6a5c73d 100644 --- a/doc/filters/date_modify.rst +++ b/doc/filters/date_modify.rst @@ -14,5 +14,10 @@ The ``date_modify`` filter accepts strings (it must be in a format supported by the `strtotime`_ function) or `DateTime`_ instances. You can easily combine it with the :doc:`date` filter for formatting. +Arguments +--------- + + * ``modifier``: The modifier + .. _`strtotime`: http://www.php.net/strtotime .. _`DateTime`: http://www.php.net/DateTime diff --git a/doc/filters/default.rst b/doc/filters/default.rst index 4055ead..46ed963 100644 --- a/doc/filters/default.rst +++ b/doc/filters/default.rst @@ -26,3 +26,8 @@ undefined: Read the documentation for the :doc:`defined<../tests/defined>` and :doc:`empty<../tests/empty>` tests to learn more about their semantics. + +Arguments +--------- + + * ``default``: The default value diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index ddb2bbb..5c3bec4 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -84,4 +84,10 @@ The ``escape`` filter supports the following escaping strategies: {{ var|escape(strategy)|raw }} {# won't be double-escaped #} {% endautoescape %} +Arguments +--------- + + * ``strategy``: The escaping strategy + * ``charset``: The string charset + .. _`htmlspecialchars`: http://php.net/htmlspecialchars diff --git a/doc/filters/join.rst b/doc/filters/join.rst index eec2045..f495242 100644 --- a/doc/filters/join.rst +++ b/doc/filters/join.rst @@ -16,3 +16,8 @@ define it with the optional first parameter: {{ [1, 2, 3]|join('|') }} {# returns 1|2|3 #} + +Arguments +--------- + + * ``glue``: The separator diff --git a/doc/filters/json_encode.rst b/doc/filters/json_encode.rst index c7d19b3..374f519 100644 --- a/doc/filters/json_encode.rst +++ b/doc/filters/json_encode.rst @@ -11,4 +11,9 @@ The ``json_encode`` filter returns the JSON representation of a string: Internally, Twig uses the PHP `json_encode`_ function. +Arguments +--------- + + * ``options``: The options + .. _`json_encode`: http://php.net/json_encode diff --git a/doc/filters/number_format.rst b/doc/filters/number_format.rst index b591b1e..fedacd9 100644 --- a/doc/filters/number_format.rst +++ b/doc/filters/number_format.rst @@ -35,4 +35,11 @@ These defaults can be easily changed through the core extension: The defaults set for ``number_format`` can be over-ridden upon each call using the additional parameters. +Arguments +--------- + + * ``decimal``: The number of decimal points to display + * ``decimal_point``: The character(s) to use for the decimal point + * ``decimal_sep``: The character(s) to use for the thousands separator + .. _`number_format`: http://php.net/number_format diff --git a/doc/filters/replace.rst b/doc/filters/replace.rst index cc603fa..e961f23 100644 --- a/doc/filters/replace.rst +++ b/doc/filters/replace.rst @@ -11,4 +11,9 @@ The ``replace`` filter formats a given string by replacing the placeholders {# returns I like foo and bar if the foo parameter equals to the foo string. #} +Arguments +--------- + + * ``replace_pairs``: The placeholder values + .. seealso:: :doc:`format` diff --git a/doc/filters/slice.rst b/doc/filters/slice.rst index 80a4293..0205771 100644 --- a/doc/filters/slice.rst +++ b/doc/filters/slice.rst @@ -52,6 +52,13 @@ up until the end of the variable. It also works with objects implementing the `Traversable`_ interface. +Arguments +--------- + + * ``start``: The start of the slice + * ``length``: The size of the slice + * ``preserve_keys``: Whether to preserve key or not (when the input is an array) + .. _`Traversable`: http://php.net/manual/en/class.traversable.php .. _`array_slice`: http://php.net/array_slice .. _`substr`: http://php.net/substr diff --git a/doc/filters/split.rst b/doc/filters/split.rst index 9b9e4e6..9108a5c 100644 --- a/doc/filters/split.rst +++ b/doc/filters/split.rst @@ -43,5 +43,11 @@ chunks. Length is set by the ``limit`` argument (one character by default). Internally, Twig uses the PHP `explode`_ or `str_split`_ (if delimiter is empty) functions for string splitting. +Arguments +--------- + + * ``delimiter``: The delimiter + * ``limit``: The limit argument + .. _`explode`: http://php.net/explode .. _`str_split`: http://php.net/str_split diff --git a/doc/filters/trim.rst b/doc/filters/trim.rst index f1215f6..f38afd5 100644 --- a/doc/filters/trim.rst +++ b/doc/filters/trim.rst @@ -21,4 +21,9 @@ and end of a string: Internally, Twig uses the PHP `trim`_ function. +Arguments +--------- + + * ``character_mask``: The characters to strip + .. _`trim`: http://php.net/trim diff --git a/doc/functions/cycle.rst b/doc/functions/cycle.rst index fe11d68..0015cae 100644 --- a/doc/functions/cycle.rst +++ b/doc/functions/cycle.rst @@ -18,3 +18,8 @@ The array can contain any number of values: {% for i in 0..10 %} {{ cycle(fruits, i) }} {% endfor %} + +Arguments +--------- + + * ``position``: The cycle position diff --git a/doc/functions/date.rst b/doc/functions/date.rst index c1a011c..fd67fc5 100644 --- a/doc/functions/date.rst +++ b/doc/functions/date.rst @@ -43,4 +43,10 @@ If no argument is passed, the function returns the current date: $twig = new Twig_Environment($loader); $twig->getExtension('core')->setTimezone('Europe/Paris'); +Arguments +--------- + + * ``date``: The date + * ``timezone``: The timezone + .. _`date`: http://www.php.net/date diff --git a/doc/functions/dump.rst b/doc/functions/dump.rst index 8ff0c05..1500b0f 100644 --- a/doc/functions/dump.rst +++ b/doc/functions/dump.rst @@ -60,5 +60,10 @@ dumped: Internally, Twig uses the PHP `var_dump`_ function. +Arguments +--------- + + * ``context``: The context to dump + .. _`XDebug`: http://xdebug.org/docs/display .. _`var_dump`: http://php.net/var_dump diff --git a/doc/functions/random.rst b/doc/functions/random.rst index 104493d..a5a916b 100644 --- a/doc/functions/random.rst +++ b/doc/functions/random.rst @@ -21,4 +21,9 @@ parameter type: {{ random() }} {# example output: 15386094 (works as native PHP `mt_rand`_ function) #} {{ random(5) }} {# example output: 3 #} +Arguments +--------- + + * ``values``: The values + .. _`mt_rand`: http://php.net/mt_rand diff --git a/doc/functions/range.rst b/doc/functions/range.rst index c9bdd96..b1fa547 100644 --- a/doc/functions/range.rst +++ b/doc/functions/range.rst @@ -35,4 +35,11 @@ function (with a step of 1): The ``range`` function works as the native PHP `range`_ function. +Arguments +--------- + + * ``low``: The first value of the sequence. + * ``high``: The highest possible value of the sequence. + * ``step``: The increment between elements of the sequence. + .. _`range`: http://php.net/range diff --git a/doc/functions/template_from_string.rst b/doc/functions/template_from_string.rst index 0b3b0b4..6df650e 100644 --- a/doc/functions/template_from_string.rst +++ b/doc/functions/template_from_string.rst @@ -25,3 +25,8 @@ The ``template_from_string`` function loads a template from a string: Even if you will probably always use the ``template_from_string`` function with the ``include`` tag, you can use it with any tag or function that takes a template as an argument (like the ``embed`` or ``extends`` tags). + +Arguments +--------- + + * ``template``: The template diff --git a/doc/templates.rst b/doc/templates.rst index 64761db..602588a 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -190,6 +190,56 @@ progression of integers: Go to the :doc:`functions` page to learn more about the built-in functions. +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*: + +.. code-block:: jinja + + {% for i in range(low=1, high=10, step=2) %} + {{ i }}, + {% endfor %} + +Using named arguments makes your templates more explicit about the meaning of +the values you pass as arguments: + +.. code-block:: jinja + + {{ data|convert_encoding('UTF-8', 'iso-2022-jp') }} + + {# versus #} + + {{ data|convert_encoding(from='iso-2022-jp', to='UTF-8') }} + +Named arguments also allow you to skip some arguments for which you don't want +to change the default value:: + +.. code-block:: jinja + + {# the first argument is the date format, which defaults to the global date format if null is passed #} + {{ "now"|date(null, "Europe/Paris") }} + + {# or skip the format value by using a named argument for the timezone #} + {{ "now"|date(timezone="Europe/Paris") }} + +You can also use both positional and named arguments in one call, which is not +recommended as it can be confusing: + +.. code-block:: jinja + + {# both work #} + {{ "now"|date('d/m/Y H:i', timezone="Europe/Paris") }} + {{ "now"|date(timezone="Europe/Paris", 'd/m/Y H:i') }} + +.. tip:: + + Each function and filter documentation page has a section where the names + of all arguments are listed when supported. + Control Structure ----------------- diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index aa48303..b572197 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -297,9 +297,9 @@ class Twig_ExpressionParser public function getFunctionNode($name, $line) { - $args = $this->parseArguments(); switch ($name) { case 'parent': + $args = $this->parseArguments(); if (!count($this->parser->getBlockStack())) { throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename()); } @@ -310,8 +310,9 @@ class Twig_ExpressionParser return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line); case 'block': - return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line); + return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line); case 'attribute': + $args = $this->parseArguments(); if (count($args) < 2) { throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename()); } @@ -320,7 +321,7 @@ class Twig_ExpressionParser default: if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { $arguments = new Twig_Node_Expression_Array(array(), $line); - foreach ($args as $n) { + foreach ($this->parseArguments() as $n) { $arguments->addElement($n); } @@ -330,6 +331,7 @@ class Twig_ExpressionParser return $node; } + $args = $this->parseArguments(true); $class = $this->getFunctionNodeClass($name, $line); return new $class($name, $args, $line); @@ -416,7 +418,7 @@ class Twig_ExpressionParser if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) { $arguments = new Twig_Node(); } else { - $arguments = $this->parseArguments(); + $arguments = $this->parseArguments(true); } $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); @@ -433,17 +435,40 @@ class Twig_ExpressionParser return $node; } - public function parseArguments() + /** + * Parses arguments. + * + * @param Boolean $namedArguments Whether to allow named arguments or not + * @param Boolean $definition Whether we are parsing arguments for a function definition + */ + public function parseArguments($namedArguments = false, $definition = false) { $args = array(); $stream = $this->parser->getStream(); - $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis'); + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) { if (!empty($args)) { $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); } - $args[] = $this->parseExpression(); + + $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()); + } + $name = $value->getAttribute('name'); + $value = $definition ? $this->parsePrimaryExpression() : $this->parseExpression(); + } + + 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'); diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index d2781b6..78bcc8f 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -267,7 +267,7 @@ class Twig_Extension_Core extends Twig_Extension $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); $arguments = null; if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { - $arguments = $parser->getExpressionParser()->parseArguments(); + $arguments = $parser->getExpressionParser()->parseArguments(true); } $class = $this->getTestNodeClass($parser, $name, $node->getLine()); @@ -305,18 +305,18 @@ class Twig_Extension_Core extends Twig_Extension /** * Cycles over a value. * - * @param ArrayAccess|array $values An array or an ArrayAccess instance - * @param integer $i The cycle value + * @param ArrayAccess|array $values An array or an ArrayAccess instance + * @param integer $position The cycle position * * @return string The next value in the cycle */ -function twig_cycle($values, $i) +function twig_cycle($values, $position) { if (!is_array($values) && !$values instanceof ArrayAccess) { return $values; } - return $values[$i % count($values)]; + return $values[$position % count($values)]; } /** @@ -410,7 +410,7 @@ function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $ * Returns a new date object modified * *
- *   {{ post.published_at|modify("-1day")|date("m/d/Y") }}
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
  * 
* * @param Twig_Environment $env A Twig_Environment instance diff --git a/lib/Twig/Filter.php b/lib/Twig/Filter.php index 1a4806c..90a62d3 100644 --- a/lib/Twig/Filter.php +++ b/lib/Twig/Filter.php @@ -15,7 +15,7 @@ * @package twig * @author Fabien Potencier */ -abstract class Twig_Filter implements Twig_FilterInterface +abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface { protected $options; protected $arguments = array(); @@ -27,6 +27,7 @@ abstract class Twig_Filter implements Twig_FilterInterface 'needs_context' => false, 'pre_escape' => null, 'preserves_safety' => null, + 'callable' => null, ), $options); } @@ -72,4 +73,9 @@ abstract class Twig_Filter implements Twig_FilterInterface { return $this->options['pre_escape']; } + + public function getCallable() + { + return $this->options['callable']; + } } diff --git a/lib/Twig/Filter/Function.php b/lib/Twig/Filter/Function.php index 1de078b..59af50d 100644 --- a/lib/Twig/Filter/Function.php +++ b/lib/Twig/Filter/Function.php @@ -21,6 +21,8 @@ class Twig_Filter_Function extends Twig_Filter public function __construct($function, array $options = array()) { + $options['callable'] = $function; + parent::__construct($options); $this->function = $function; diff --git a/lib/Twig/Filter/Method.php b/lib/Twig/Filter/Method.php index a680f61..0f5b27e 100644 --- a/lib/Twig/Filter/Method.php +++ b/lib/Twig/Filter/Method.php @@ -22,6 +22,8 @@ class Twig_Filter_Method extends Twig_Filter public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + parent::__construct($options); $this->extension = $extension; diff --git a/lib/Twig/FilterCallableInterface.php b/lib/Twig/FilterCallableInterface.php new file mode 100644 index 0000000..86f0419 --- /dev/null +++ b/lib/Twig/FilterCallableInterface.php @@ -0,0 +1,21 @@ + + */ +interface Twig_FilterCallableInterface +{ + public function getCallable(); +} diff --git a/lib/Twig/Function.php b/lib/Twig/Function.php index cd7643f..0a13441 100644 --- a/lib/Twig/Function.php +++ b/lib/Twig/Function.php @@ -15,7 +15,7 @@ * @package twig * @author Fabien Potencier */ -abstract class Twig_Function implements Twig_FunctionInterface +abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface { protected $options; protected $arguments = array(); @@ -25,6 +25,7 @@ abstract class Twig_Function implements Twig_FunctionInterface $this->options = array_merge(array( 'needs_environment' => false, 'needs_context' => false, + 'callable' => null, ), $options); } @@ -60,4 +61,9 @@ abstract class Twig_Function implements Twig_FunctionInterface return array(); } + + public function getCallable() + { + return $this->options['callable']; + } } diff --git a/lib/Twig/Function/Function.php b/lib/Twig/Function/Function.php index 3237d8c..e102479 100644 --- a/lib/Twig/Function/Function.php +++ b/lib/Twig/Function/Function.php @@ -22,6 +22,8 @@ class Twig_Function_Function extends Twig_Function public function __construct($function, array $options = array()) { + $options['callable'] = $function; + parent::__construct($options); $this->function = $function; diff --git a/lib/Twig/Function/Method.php b/lib/Twig/Function/Method.php index 8838618..d0f296d 100644 --- a/lib/Twig/Function/Method.php +++ b/lib/Twig/Function/Method.php @@ -23,6 +23,8 @@ class Twig_Function_Method extends Twig_Function public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + parent::__construct($options); $this->extension = $extension; diff --git a/lib/Twig/FunctionCallableInterface.php b/lib/Twig/FunctionCallableInterface.php new file mode 100644 index 0000000..fc54308 --- /dev/null +++ b/lib/Twig/FunctionCallableInterface.php @@ -0,0 +1,21 @@ + + */ +interface Twig_FunctionCallableInterface +{ + public function getCallable(); +} diff --git a/lib/Twig/Node/Expression/Call.php b/lib/Twig/Node/Expression/Call.php index d24680c..0c69d51 100644 --- a/lib/Twig/Node/Expression/Call.php +++ b/lib/Twig/Node/Expression/Call.php @@ -10,7 +10,7 @@ */ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression { - public function compileArguments(Twig_Compiler $compiler) + protected function compileArguments(Twig_Compiler $compiler) { $compiler->raw('('); @@ -48,7 +48,11 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression } if ($this->hasNode('arguments') && null !== $this->getNode('arguments')) { - foreach ($this->getNode('arguments') as $node) { + $callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null; + + $arguments = $this->getArguments($callable, $this->getNode('arguments')); + + foreach ($arguments as $node) { if (!$first) { $compiler->raw(', '); } @@ -59,4 +63,83 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression $compiler->raw(')'); } + + protected function getArguments($callable, $arguments) + { + $parameters = array(); + $named = false; + foreach ($arguments as $name => $node) { + if (!is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } + $parameters[$name] = $node; + } + + if (!$named) { + return $parameters; + } + + if (!$callable) { + throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name'))); + } + + // manage named arguments + if (is_array($callable)) { + $r = new \ReflectionMethod($callable[0], $callable[1]); + } elseif (is_object($callable) && !$callable instanceof \Closure) { + $r = new \ReflectionObject($callable); + $r = $r->getMethod('__invoke'); + } else { + $r = new \ReflectionFunction($callable); + } + + $definition = $r->getParameters(); + if ($this->hasNode('node')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + array_shift($definition); + } + if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + array_shift($definition); + } + } + + $arguments = array(); + $pos = 0; + foreach ($definition as $param) { + $name = $this->normalizeName($param->name); + + if (array_key_exists($name, $parameters)) { + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + } elseif (array_key_exists($pos, $parameters)) { + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + ++$pos; + } elseif ($param->isDefaultValueAvailable()) { + $arguments[] = new Twig_Node_Expression_Constant($param->getDefaultValue(), -1); + } elseif ($param->isOptional()) { + break; + } else { + throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + } + + foreach (array_keys($parameters) as $name) { + throw new Twig_Error_Syntax(sprintf('Unknown argument "%s" for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + + return $arguments; + } + + protected function normalizeName($name) + { + return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name)); + } } diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index 45d2c93..ea7f4a6 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -18,13 +18,19 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call public function compile(Twig_Compiler $compiler) { - $filter = $compiler->getEnvironment()->getFilter($this->getNode('filter')->getAttribute('value')); + $name = $this->getNode('filter')->getAttribute('value'); + $filter = $compiler->getEnvironment()->getFilter($name); $compiler->raw($filter->compile()); + $this->setAttribute('name', $name); + $this->setAttribute('type', 'filter'); $this->setAttribute('needs_environment', $filter->needsEnvironment()); $this->setAttribute('needs_context', $filter->needsContext()); $this->setAttribute('arguments', $filter->getArguments()); + if ($filter instanceof Twig_FilterCallableInterface) { + $this->setAttribute('callable', $filter->getCallable()); + } $this->compileArguments($compiler); } diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index 58461e4..8f2be6e 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -17,13 +17,19 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression_Call public function compile(Twig_Compiler $compiler) { - $function = $compiler->getEnvironment()->getFunction($this->getAttribute('name')); + $name = $this->getAttribute('name'); + $function = $compiler->getEnvironment()->getFunction($name); $compiler->raw($function->compile()); + $this->setAttribute('name', $name); + $this->setAttribute('type', 'function'); $this->setAttribute('needs_environment', $function->needsEnvironment()); $this->setAttribute('needs_context', $function->needsContext()); $this->setAttribute('arguments', $function->getArguments()); + if ($function instanceof Twig_FunctionCallableInterface) { + $this->setAttribute('callable', $function->getCallable()); + } $this->compileArguments($compiler); } diff --git a/lib/Twig/Node/Expression/Test.php b/lib/Twig/Node/Expression/Test.php index b41858c..45bb45b 100644 --- a/lib/Twig/Node/Expression/Test.php +++ b/lib/Twig/Node/Expression/Test.php @@ -19,8 +19,15 @@ class Twig_Node_Expression_Test extends Twig_Node_Expression_Call { $name = $this->getAttribute('name'); $testMap = $compiler->getEnvironment()->getTests(); + $test = $testMap[$name]; - $compiler->raw($testMap[$name]->compile()); + $this->setAttribute('name', $name); + $this->setAttribute('type', 'test'); + if ($test instanceof Twig_TestCallableInterface) { + $this->setAttribute('callable', $test->getCallable()); + } + + $compiler->raw($test->compile()); $this->compileArguments($compiler); } diff --git a/lib/Twig/Test.php b/lib/Twig/Test.php new file mode 100644 index 0000000..06d4f12 --- /dev/null +++ b/lib/Twig/Test.php @@ -0,0 +1,34 @@ + + */ +abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface +{ + protected $options; + protected $arguments = array(); + + public function __construct(array $options = array()) + { + $this->options = array_merge(array( + 'callable' => null, + ), $options); + } + + public function getCallable() + { + return $this->options['callable']; + } +} diff --git a/lib/Twig/Test/Function.php b/lib/Twig/Test/Function.php index 1240a0f..50f561f 100644 --- a/lib/Twig/Test/Function.php +++ b/lib/Twig/Test/Function.php @@ -15,12 +15,16 @@ * @package twig * @author Fabien Potencier */ -class Twig_Test_Function implements Twig_TestInterface +class Twig_Test_Function extends Twig_Test { protected $function; - public function __construct($function) + public function __construct($function, array $options = array()) { + $options['callable'] = $function; + + parent::__construct($options); + $this->function = $function; } diff --git a/lib/Twig/Test/Method.php b/lib/Twig/Test/Method.php index a8d1d9b..20dbf82 100644 --- a/lib/Twig/Test/Method.php +++ b/lib/Twig/Test/Method.php @@ -15,13 +15,17 @@ * @package twig * @author Fabien Potencier */ -class Twig_Test_Method implements Twig_TestInterface +class Twig_Test_Method extends Twig_Test { protected $extension; protected $method; - public function __construct(Twig_ExtensionInterface $extension, $method) + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) { + $options['callable'] = array($extension, $method); + + parent::__construct($options); + $this->extension = $extension; $this->method = $method; } diff --git a/lib/Twig/Test/Node.php b/lib/Twig/Test/Node.php index 47a978e..d9d47eb 100644 --- a/lib/Twig/Test/Node.php +++ b/lib/Twig/Test/Node.php @@ -15,12 +15,14 @@ * @package twig * @author Fabien Potencier */ -class Twig_Test_Node implements Twig_TestInterface +class Twig_Test_Node extends Twig_Test { protected $class; - public function __construct($class) + public function __construct($class, array $options = array()) { + parent::__construct($options); + $this->class = $class; } diff --git a/lib/Twig/TestCallableInterface.php b/lib/Twig/TestCallableInterface.php new file mode 100644 index 0000000..3f12290 --- /dev/null +++ b/lib/Twig/TestCallableInterface.php @@ -0,0 +1,21 @@ + + */ +interface Twig_TestCallableInterface +{ + public function getCallable(); +} diff --git a/test/Twig/Tests/ExpressionParserTest.php b/test/Twig/Tests/ExpressionParserTest.php index 88d6d59..e3fb732 100644 --- a/test/Twig/Tests/ExpressionParserTest.php +++ b/test/Twig/Tests/ExpressionParserTest.php @@ -216,6 +216,28 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase } /** + * @expectedException Twig_Error_Syntax + */ + public function testAttributeCallDoesNotSupportNamedArguments() + { + $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false)); + $parser = new Twig_Parser($env); + + $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 The function "cycl" does not exist. Did you mean "cycle" in "index" at line 1 */ diff --git a/test/Twig/Tests/Fixtures/filters/date_namedargs.test b/test/Twig/Tests/Fixtures/filters/date_namedargs.test new file mode 100644 index 0000000..6ca2049 --- /dev/null +++ b/test/Twig/Tests/Fixtures/filters/date_namedargs.test @@ -0,0 +1,15 @@ +--TEST-- +"date" filter +--TEMPLATE-- +{{ date|date(format='d/m/Y H:i:s P', timezone='America/Chicago') }} +{{ date|date(timezone='America/Chicago', format='d/m/Y H:i:s P') }} +{{ date|date(timezone='America/Chicago', 'd/m/Y H:i:s P') }} +{{ date|date('d/m/Y H:i:s P', timezone='America/Chicago') }} +--DATA-- +date_default_timezone_set('UTC'); +return array('date' => mktime(13, 45, 0, 10, 4, 2010)) +--EXPECT-- +04/10/2010 08:45:00 -05:00 +04/10/2010 08:45:00 -05:00 +04/10/2010 08:45:00 -05:00 +04/10/2010 08:45:00 -05:00 diff --git a/test/Twig/Tests/Fixtures/filters/reverse.test b/test/Twig/Tests/Fixtures/filters/reverse.test index 3c5f410..7948ac4 100644 --- a/test/Twig/Tests/Fixtures/filters/reverse.test +++ b/test/Twig/Tests/Fixtures/filters/reverse.test @@ -4,9 +4,15 @@ {{ [1, 2, 3, 4]|reverse|join('') }} {{ '1234évènement'|reverse }} {{ arr|reverse|join('') }} +{{ {'a': 'c', 'b': 'a'}|reverse()|join(',') }} +{{ {'a': 'c', 'b': 'a'}|reverse(preserveKeys=true)|join(glue=',') }} +{{ {'a': 'c', 'b': 'a'}|reverse(preserve_keys=true)|join(glue=',') }} --DATA-- return array('arr' => new ArrayObject(array(1, 2, 3, 4))) --EXPECT-- 4321 tnemenèvé4321 4321 +a,c +a,c +a,c diff --git a/test/Twig/Tests/Fixtures/functions/date_namedargs.test b/test/Twig/Tests/Fixtures/functions/date_namedargs.test new file mode 100644 index 0000000..b9dd9e3 --- /dev/null +++ b/test/Twig/Tests/Fixtures/functions/date_namedargs.test @@ -0,0 +1,11 @@ +--TEST-- +"date" function +--TEMPLATE-- +{{ date(date, "America/New_York")|date('d/m/Y H:i:s P', false) }} +{{ date(timezone="America/New_York", date=date)|date('d/m/Y H:i:s P', false) }} +--DATA-- +date_default_timezone_set('UTC'); +return array('date' => mktime(13, 45, 0, 10, 4, 2010)) +--EXPECT-- +04/10/2010 09:45:00 -04:00 +04/10/2010 09:45:00 -04:00 diff --git a/test/Twig/Tests/Fixtures/functions/range.test b/test/Twig/Tests/Fixtures/functions/range.test new file mode 100644 index 0000000..e0377c8 --- /dev/null +++ b/test/Twig/Tests/Fixtures/functions/range.test @@ -0,0 +1,8 @@ +--TEST-- +"range" function +--TEMPLATE-- +{{ range(low=0+1, high=10+0, step=2)|join(',') }} +--DATA-- +return array() +--EXPECT-- +1,3,5,7,9 diff --git a/test/Twig/Tests/Node/Expression/FilterTest.php b/test/Twig/Tests/Node/Expression/FilterTest.php index 48241d4..86cb834 100644 --- a/test/Twig/Tests/Node/Expression/FilterTest.php +++ b/test/Twig/Tests/Node/Expression/FilterTest.php @@ -49,9 +49,65 @@ class Twig_Tests_Node_Expression_FilterTest extends Twig_Test_NodeTestCase $tests[] = array($node, 'twig_number_format_filter($this->env, strtoupper("foo"), 2, ".", ",")'); } + // named arguments + $date = new Twig_Node_Expression_Constant(0, 1); + $node = $this->createFilter($date, 'date', array( + 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1), + 'format' => new Twig_Node_Expression_Constant('d/m/Y H:i:s P', 1), + )); + $tests[] = array($node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")'); + + // skip an optional argument + $date = new Twig_Node_Expression_Constant(0, 1); + $node = $this->createFilter($date, 'date', array( + 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1), + )); + $tests[] = array($node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")'); + + // underscores vs camelCase for named arguments + $string = new Twig_Node_Expression_Constant('abc', 1); + $node = $this->createFilter($string, 'reverse', array( + 'preserve_keys' => new Twig_Node_Expression_Constant(true, 1), + )); + $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)'); + $node = $this->createFilter($string, 'reverse', array( + 'preserveKeys' => new Twig_Node_Expression_Constant(true, 1), + )); + $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)'); + return $tests; } + /** + * @expectedException Twig_Error_Syntax + * @expectedExceptionMessage Unknown argument "foobar" for filter "date". + */ + public function testCompileWithWrongNamedArgumentName() + { + $date = new Twig_Node_Expression_Constant(0, 1); + $node = $this->createFilter($date, 'date', array( + 'foobar' => new Twig_Node_Expression_Constant('America/Chicago', 1), + )); + + $compiler = $this->getCompiler(); + $compiler->compile($node); + } + + /** + * @expectedException Twig_Error_Syntax + * @expectedExceptionMessage Value for argument "from" is required for filter "replace". + */ + public function testCompileWithMissingNamedArgument() + { + $value = new Twig_Node_Expression_Constant(0, 1); + $node = $this->createFilter($value, 'replace', array( + 'to' => new Twig_Node_Expression_Constant('foo', 1), + )); + + $compiler = $this->getCompiler(); + $compiler->compile($node); + } + protected function createFilter($node, $name, array $arguments = array()) { $name = new Twig_Node_Expression_Constant($name, 1); diff --git a/test/Twig/Tests/Node/Expression/FunctionTest.php b/test/Twig/Tests/Node/Expression/FunctionTest.php index b5ddf33..50f41f5 100644 --- a/test/Twig/Tests/Node/Expression/FunctionTest.php +++ b/test/Twig/Tests/Node/Expression/FunctionTest.php @@ -67,6 +67,13 @@ class Twig_Tests_Node_Expression_FunctionTest extends Twig_Test_NodeTestCase $node = $this->createFunction('foobar', array(new Twig_Node_Expression_Constant('bar', 1))); $tests[] = array($node, 'foobar($this->env, $context, "bar")', $environment); + // named arguments + $node = $this->createFunction('date', array( + 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1), + 'date' => new Twig_Node_Expression_Constant(0, 1), + )); + $tests[] = array($node, 'twig_date_converter($this->env, 0, "America/Chicago")'); + return $tests; } -- 1.7.2.5