Skip to content

Commit

Permalink
feature #1699 Added variadic filters, tests, and functions (hason)
Browse files Browse the repository at this point in the history
This PR was merged into the 1.x branch.

Discussion
----------

Added variadic filters, tests, and functions

Replaces #1317

```php
$env->addFunction(new Twig_SimpleFunction('foo', function ($a1, array $args = array()) {
}, array('is_variadic' => true));
```
```jinja
{{ foo(1, 2, a="a", b="b") }}
{# foo(1, array(0 => 2, "a" => "a", "b" => "b")); #}
```

Commits
-------

c78656a Added variadic filters, tests, and functions
  • Loading branch information
fabpot committed Jul 5, 2015
2 parents 056bb5d + c78656a commit e007c17
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 2 deletions.
21 changes: 21 additions & 0 deletions doc/advanced.rst
Expand Up @@ -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
~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -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
----

Expand Down
15 changes: 15 additions & 0 deletions doc/templates.rst
Expand Up @@ -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
-----------------

Expand Down
44 changes: 42 additions & 2 deletions lib/Twig/Node/Expression/Call.php
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Twig/Node/Expression/Filter.php
Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/Twig/Node/Expression/Function.php
Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/Twig/Node/Expression/Test.php
Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions lib/Twig/SimpleFilter.php
Expand Up @@ -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,
Expand Down Expand Up @@ -91,4 +92,9 @@ public function getPreEscape()
{
return $this->options['pre_escape'];
}

public function isVariadic()
{
return $this->options['is_variadic'];
}
}
6 changes: 6 additions & 0 deletions lib/Twig/SimpleFunction.php
Expand Up @@ -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',
Expand Down Expand Up @@ -81,4 +82,9 @@ public function getSafe(Twig_Node $functionArgs)

return array();
}

public function isVariadic()
{
return $this->options['is_variadic'];
}
}
6 changes: 6 additions & 0 deletions lib/Twig/SimpleTest.php
Expand Up @@ -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);
}
Expand All @@ -43,4 +44,9 @@ public function getNodeClass()
{
return $this->options['node_class'];
}

public function isVariadic()
{
return $this->options['is_variadic'];
}
}
14 changes: 14 additions & 0 deletions test/Twig/Tests/Node/Expression/CallTest.php
Expand Up @@ -84,13 +84,27 @@ 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())
{
}

public function customFunction($arg1, $arg2 = 'default', $arg3 = array())
{
}

public function customFunctionWithArbitraryArguments()
{
}
}

class Twig_Tests_Node_Expression_Call extends Twig_Node_Expression_Call
Expand Down
33 changes: 33 additions & 0 deletions test/Twig/Tests/Node/Expression/FilterTest.php
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -119,3 +148,7 @@ protected function getEnvironment()
return parent::getEnvironment();
}
}

function twig_tests_filter_barbar($context, $string, $arg1 = null, $arg2 = null, array $args = array())
{
}
23 changes: 23 additions & 0 deletions test/Twig/Tests/Node/Expression/FunctionTest.php
Expand Up @@ -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();

Expand Down Expand Up @@ -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)));
Expand All @@ -85,3 +104,7 @@ protected function getEnvironment()
return parent::getEnvironment();
}
}

function twig_tests_function_barbar($arg1 = null, $arg2 = null, array $args = array())
{
}
26 changes: 26 additions & 0 deletions test/Twig/Tests/Node/Expression/TestTest.php
Expand Up @@ -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);
Expand All @@ -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;
}

Expand All @@ -54,3 +76,7 @@ protected function getEnvironment()
return parent::getEnvironment();
}
}

function twig_tests_test_barbar($string, $arg1 = null, $arg2 = null, array $args = array())
{
}

0 comments on commit e007c17

Please sign in to comment.