diff --git a/doc/advanced.rst b/doc/advanced.rst index 0e3be9092b..165b41c380 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -224,6 +224,23 @@ through your filter:: $filter = new Twig_SimpleFilter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html'))); +Variadic Filters +~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.19 + Support for variadic filters was added in Twig 1.19. + +If you want to pass a variable number of positional or named arguments to the filter, +set the ``is_variadic`` option to ``true``; Twig will pass the array of arbitrary arguments +as the last argument to the filter call that is defined as an array with an empty default value:: + + $filter = new Twig_SimpleFilter('thumbnail', function ($file, array $options = array()) { + ... + }, array('is_variadic' => true)); + +The named arguments passed to the variadic filter cannot be checked if they are valid or not +as if they are not valid, they will end up in the option array. + Dynamic Filters ~~~~~~~~~~~~~~~ @@ -331,6 +348,10 @@ The ``node`` sub-node will contain an expression of ``my_value``. Node-based tests also have access to the ``arguments`` node. This node will contain the various other arguments that have been provided to your test. +If you want to pass a variable number of positional or named arguments to the test, +set the ``is_variadic`` option to ``true``. Tests also support dynamic name feature +as filters and functions. + Tags ---- diff --git a/doc/templates.rst b/doc/templates.rst index ee18682bc1..8ba1c7c492 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -238,6 +238,21 @@ case positional arguments must always come before named arguments: Each function and filter documentation page has a section where the names of all arguments are listed when supported. +Variadic Arguments +------------------ + +.. versionadded:: 1.19 + Support for variadic arguments was added in Twig 1.19. + +The variadic filter, function or test can accept any arbitrary positional or named arguments: + +.. code-block:: jinja + + {{ "path/to/image.png"|thumbnail(size=[32, 32], mode=outbound, quality=90, format="jpg") }} + +The named arguments passed to the variadic filter, function or test cannot be checked if they are valid or not +as if they are not valid, they will end up in the option array. + Control Structure ----------------- diff --git a/lib/Twig/Node/Expression/Call.php b/lib/Twig/Node/Expression/Call.php index 998160b40c..ee82343ded 100644 --- a/lib/Twig/Node/Expression/Call.php +++ b/lib/Twig/Node/Expression/Call.php @@ -106,12 +106,19 @@ protected function getArguments($callable, $arguments) $parameters[$name] = $node; } - if (!$named) { + $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); + if (!$named && !$isVariadic) { return $parameters; } if (!$callable) { - throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $callType, $callName)); + if ($named) { + $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); + } else { + $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); + } + + throw new LogicException($message); } // manage named arguments @@ -141,6 +148,22 @@ protected function getArguments($callable, $arguments) array_shift($definition); } } + if ($isVariadic) { + $argument = end($definition); + if ($argument && $argument->isArray() && $argument->isDefaultValueAvailable() && array() === $argument->getDefaultValue()) { + array_pop($definition); + } else { + $callableName = $r->name; + if ($r->getDeclaringClass()) { + $callableName = $r->getDeclaringClass()->name.'::'.$callableName; + } + + throw new LogicException(sprintf( + 'The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = array()".', + $callableName, $callType, $callName + )); + } + } $arguments = array(); $names = array(); @@ -185,6 +208,23 @@ protected function getArguments($callable, $arguments) } } + if ($isVariadic) { + $arbitraryArguments = new Twig_Node_Expression_Array(array(), -1); + foreach ($parameters as $key => $value) { + if (is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $arbitraryArguments->addElement($value, new Twig_Node_Expression_Constant($key, -1)); + } + unset($parameters[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + if (!empty($parameters)) { $unknownParameter = null; foreach ($parameters as $parameter) { diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index 207b062a3a..a906232ea2 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -30,6 +30,9 @@ public function compile(Twig_Compiler $compiler) if ($filter instanceof Twig_FilterCallableInterface || $filter instanceof Twig_SimpleFilter) { $this->setAttribute('callable', $filter->getCallable()); } + if ($filter instanceof Twig_SimpleFilter) { + $this->setAttribute('is_variadic', $filter->isVariadic()); + } $this->compileCallable($compiler); } diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index 3e1f6b5590..7326ede269 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -29,6 +29,9 @@ public function compile(Twig_Compiler $compiler) if ($function instanceof Twig_FunctionCallableInterface || $function instanceof Twig_SimpleFunction) { $this->setAttribute('callable', $function->getCallable()); } + if ($function instanceof Twig_SimpleFunction) { + $this->setAttribute('is_variadic', $function->isVariadic()); + } $this->compileCallable($compiler); } diff --git a/lib/Twig/Node/Expression/Test.php b/lib/Twig/Node/Expression/Test.php index 639f501a18..c0358c8bf9 100644 --- a/lib/Twig/Node/Expression/Test.php +++ b/lib/Twig/Node/Expression/Test.php @@ -26,6 +26,9 @@ public function compile(Twig_Compiler $compiler) if ($test instanceof Twig_TestCallableInterface || $test instanceof Twig_SimpleTest) { $this->setAttribute('callable', $test->getCallable()); } + if ($test instanceof Twig_SimpleTest) { + $this->setAttribute('is_variadic', $test->isVariadic()); + } $this->compileCallable($compiler); } diff --git a/lib/Twig/SimpleFilter.php b/lib/Twig/SimpleFilter.php index febe5550af..5d6d27bf2b 100644 --- a/lib/Twig/SimpleFilter.php +++ b/lib/Twig/SimpleFilter.php @@ -28,6 +28,7 @@ public function __construct($name, $callable, array $options = array()) $this->options = array_merge(array( 'needs_environment' => false, 'needs_context' => false, + 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'pre_escape' => null, @@ -91,4 +92,9 @@ public function getPreEscape() { return $this->options['pre_escape']; } + + public function isVariadic() + { + return $this->options['is_variadic']; + } } diff --git a/lib/Twig/SimpleFunction.php b/lib/Twig/SimpleFunction.php index d02c3164e6..8085f5788d 100644 --- a/lib/Twig/SimpleFunction.php +++ b/lib/Twig/SimpleFunction.php @@ -28,6 +28,7 @@ public function __construct($name, $callable, array $options = array()) $this->options = array_merge(array( 'needs_environment' => false, 'needs_context' => false, + 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => 'Twig_Node_Expression_Function', @@ -81,4 +82,9 @@ public function getSafe(Twig_Node $functionArgs) return array(); } + + public function isVariadic() + { + return $this->options['is_variadic']; + } } diff --git a/lib/Twig/SimpleTest.php b/lib/Twig/SimpleTest.php index 225459c9f1..87b093548d 100644 --- a/lib/Twig/SimpleTest.php +++ b/lib/Twig/SimpleTest.php @@ -25,6 +25,7 @@ public function __construct($name, $callable, array $options = array()) $this->name = $name; $this->callable = $callable; $this->options = array_merge(array( + 'is_variadic' => false, 'node_class' => 'Twig_Node_Expression_Test', ), $options); } @@ -43,4 +44,9 @@ public function getNodeClass() { return $this->options['node_class']; } + + public function isVariadic() + { + return $this->options['is_variadic']; + } } diff --git a/test/Twig/Tests/Node/Expression/CallTest.php b/test/Twig/Tests/Node/Expression/CallTest.php index 2f7a1e728f..43afcd2922 100644 --- a/test/Twig/Tests/Node/Expression/CallTest.php +++ b/test/Twig/Tests/Node/Expression/CallTest.php @@ -84,6 +84,16 @@ public function testGetArgumentsForStaticMethod() $this->assertEquals(array('arg1'), $node->getArguments(__CLASS__.'::customStaticFunction', array('arg1' => 'arg1'))); } + /** + * @expectedException LogicException + * @expectedExceptionMessage The last parameter of "Twig_Tests_Node_Expression_CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = array()". + */ + public function testResolveArgumentsWithMissingParameterForArbitraryArguments() + { + $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'foo', 'is_variadic' => true)); + $node->getArguments(array($this, 'customFunctionWithArbitraryArguments'), array()); + } + public static function customStaticFunction($arg1, $arg2 = 'default', $arg3 = array()) { } @@ -91,6 +101,10 @@ public static function customStaticFunction($arg1, $arg2 = 'default', $arg3 = ar public function customFunction($arg1, $arg2 = 'default', $arg3 = array()) { } + + public function customFunctionWithArbitraryArguments() + { + } } class Twig_Tests_Node_Expression_Call extends Twig_Node_Expression_Call diff --git a/test/Twig/Tests/Node/Expression/FilterTest.php b/test/Twig/Tests/Node/Expression/FilterTest.php index c787255395..e822e2fac0 100644 --- a/test/Twig/Tests/Node/Expression/FilterTest.php +++ b/test/Twig/Tests/Node/Expression/FilterTest.php @@ -25,6 +25,10 @@ public function testConstructor() public function getTests() { + $environment = new Twig_Environment(); + $environment->addFilter(new Twig_SimpleFilter('bar', 'bar', array('needs_environment' => true))); + $environment->addFilter(new Twig_SimpleFilter('barbar', 'twig_tests_filter_barbar', array('needs_context' => true, 'is_variadic' => true))); + $tests = array(); $expr = new Twig_Node_Expression_Constant('foo', 1); @@ -69,6 +73,31 @@ public function getTests() $tests[] = array($node, 'call_user_func_array($this->env->getFilter(\'anonymous\')->getCallable(), array("foo"))'); } + // needs environment + $node = $this->createFilter($string, 'bar'); + $tests[] = array($node, 'bar($this->env, "abc")', $environment); + + $node = $this->createFilter($string, 'bar', array(new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'bar($this->env, "abc", "bar")', $environment); + + // arbitrary named arguments + $node = $this->createFilter($string, 'barbar'); + $tests[] = array($node, 'twig_tests_filter_barbar($context, "abc")', $environment); + + $node = $this->createFilter($string, 'barbar', array('foo' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_filter_barbar($context, "abc", null, null, array("foo" => "bar"))', $environment); + + $node = $this->createFilter($string, 'barbar', array('arg2' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_filter_barbar($context, "abc", null, "bar")', $environment); + + $node = $this->createFilter($string, 'barbar', array( + new Twig_Node_Expression_Constant('1', 1), + new Twig_Node_Expression_Constant('2', 1), + new Twig_Node_Expression_Constant('3', 1), + 'foo' => new Twig_Node_Expression_Constant('bar', 1), + )); + $tests[] = array($node, 'twig_tests_filter_barbar($context, "abc", "1", "2", array(0 => "3", "foo" => "bar"))', $environment); + return $tests; } @@ -119,3 +148,7 @@ protected function getEnvironment() return parent::getEnvironment(); } } + +function twig_tests_filter_barbar($context, $string, $arg1 = null, $arg2 = null, array $args = array()) +{ +} diff --git a/test/Twig/Tests/Node/Expression/FunctionTest.php b/test/Twig/Tests/Node/Expression/FunctionTest.php index a296c60a24..35edfe13bd 100644 --- a/test/Twig/Tests/Node/Expression/FunctionTest.php +++ b/test/Twig/Tests/Node/Expression/FunctionTest.php @@ -28,6 +28,7 @@ public function getTests() $environment->addFunction(new Twig_SimpleFunction('bar', 'bar', array('needs_environment' => true))); $environment->addFunction(new Twig_SimpleFunction('foofoo', 'foofoo', array('needs_context' => true))); $environment->addFunction(new Twig_SimpleFunction('foobar', 'foobar', array('needs_environment' => true, 'needs_context' => true))); + $environment->addFunction(new Twig_SimpleFunction('barbar', 'twig_tests_function_barbar', array('is_variadic' => true))); $tests = array(); @@ -62,6 +63,24 @@ public function getTests() )); $tests[] = array($node, 'twig_date_converter($this->env, 0, "America/Chicago")'); + // arbitrary named arguments + $node = $this->createFunction('barbar'); + $tests[] = array($node, 'twig_tests_function_barbar()', $environment); + + $node = $this->createFunction('barbar', array('foo' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_function_barbar(null, null, array("foo" => "bar"))', $environment); + + $node = $this->createFunction('barbar', array('arg2' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_function_barbar(null, "bar")', $environment); + + $node = $this->createFunction('barbar', array( + new Twig_Node_Expression_Constant('1', 1), + new Twig_Node_Expression_Constant('2', 1), + new Twig_Node_Expression_Constant('3', 1), + 'foo' => new Twig_Node_Expression_Constant('bar', 1), + )); + $tests[] = array($node, 'twig_tests_function_barbar("1", "2", array(0 => "3", "foo" => "bar"))', $environment); + // function as an anonymous function if (PHP_VERSION_ID >= 50300) { $node = $this->createFunction('anonymous', array(new Twig_Node_Expression_Constant('foo', 1))); @@ -85,3 +104,7 @@ protected function getEnvironment() return parent::getEnvironment(); } } + +function twig_tests_function_barbar($arg1 = null, $arg2 = null, array $args = array()) +{ +} diff --git a/test/Twig/Tests/Node/Expression/TestTest.php b/test/Twig/Tests/Node/Expression/TestTest.php index e62a8afddb..47a6889ac7 100644 --- a/test/Twig/Tests/Node/Expression/TestTest.php +++ b/test/Twig/Tests/Node/Expression/TestTest.php @@ -25,6 +25,9 @@ public function testConstructor() public function getTests() { + $environment = new Twig_Environment(); + $environment->addTest(new Twig_SimpleTest('barbar', 'twig_tests_test_barbar', array('is_variadic' => true, 'need_context' => true))); + $tests = array(); $expr = new Twig_Node_Expression_Constant('foo', 1); @@ -37,6 +40,25 @@ public function getTests() $tests[] = array($node, 'call_user_func_array($this->env->getTest(\'anonymous\')->getCallable(), array("foo", "foo"))'); } + // arbitrary named arguments + $string = new Twig_Node_Expression_Constant('abc', 1); + $node = $this->createTest($string, 'barbar'); + $tests[] = array($node, 'twig_tests_test_barbar("abc")', $environment); + + $node = $this->createTest($string, 'barbar', array('foo' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_test_barbar("abc", null, null, array("foo" => "bar"))', $environment); + + $node = $this->createTest($string, 'barbar', array('arg2' => new Twig_Node_Expression_Constant('bar', 1))); + $tests[] = array($node, 'twig_tests_test_barbar("abc", null, "bar")', $environment); + + $node = $this->createTest($string, 'barbar', array( + new Twig_Node_Expression_Constant('1', 1), + new Twig_Node_Expression_Constant('2', 1), + new Twig_Node_Expression_Constant('3', 1), + 'foo' => new Twig_Node_Expression_Constant('bar', 1), + )); + $tests[] = array($node, 'twig_tests_test_barbar("abc", "1", "2", array(0 => "3", "foo" => "bar"))', $environment); + return $tests; } @@ -54,3 +76,7 @@ protected function getEnvironment() return parent::getEnvironment(); } } + +function twig_tests_test_barbar($string, $arg1 = null, $arg2 = null, array $args = array()) +{ +}