added "filter", "map", and "reduce" filters

This commit is contained in:
Fabien Potencier
2019-05-03 08:28:45 +02:00
parent 5d37a3260a
commit b30bce1139
14 changed files with 274 additions and 8 deletions
+1
View File
@@ -1,5 +1,6 @@
* 1.41.0 (2019-XX-XX)
* added "filter", "map", and "reduce" filters (and support for arrow functions)
* fixed partial output leak when a PHP fatal error occurs
* optimized context access on PHP 7.4
+38
View File
@@ -0,0 +1,38 @@
``filter``
=========
.. versionadded:: 1.41
The ``filter`` filter was added in Twig 1.41.
The ``filter`` filter filters elements of a sequence or a mapping using an arrow
function. The arrow function receives the value of the sequence or mapping:
.. code-block:: twig
{% set sizes = [34, 36, 38, 40, 42] %}
{% for v in sizes|filter(|v| => v > 38) -%}
{{ v }}
{% endfor %}
{# output 40 42 #}
{% set sizes = {
xs: 34,
s: 36,
m: 38,
l: 40,
xl: 42,
} %}
{% for k, v in sizes|filter(|v| => v > 38) -%}
{{ k }} = {{ v }}
{% endfor %}
{# output l = 40 xl = 42 #}
Note that the arrow function has access to the current context.
Arguments
---------
* ``array``: The sequence or mapping
* ``arrow``: The arrow function
+3
View File
@@ -12,6 +12,7 @@ Filters
date_modify
default
escape
filter
first
format
join
@@ -20,10 +21,12 @@ Filters
last
length
lower
map
merge
nl2br
number_format
raw
reduce
replace
reverse
round
+2 -2
View File
@@ -19,7 +19,7 @@ define it with the optional first parameter:
{{ [1, 2, 3]|join('|') }}
{# outputs 1|2|3 #}
A second parameter can also be provided that will be the separator used between
the last two items of the sequence:
@@ -27,7 +27,7 @@ the last two items of the sequence:
{{ [1, 2, 3]|join(', ', ' and ') }}
{# outputs 1, 2 and 3 #}
Arguments
---------
+26
View File
@@ -0,0 +1,26 @@
``map``
=======
.. versionadded:: 1.41
The ``map`` filter was added in Twig 1.41.
The ``map`` filter applies an arrow function to the elements of a sequence or a
mapping. The arrow function receives the value of the sequence or mapping:
.. code-block:: twig
{% set people = [
{first: "Bob", last: "Smith"},
{first: "Alice", last: "Dupond"},
] %}
{{ people|map(|p| => "#{p.first} #{p.last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}
Note that the arrow function has access to the current context.
Arguments
---------
* ``array``: The sequence or mapping
* ``arrow``: The arrow function
+33
View File
@@ -0,0 +1,33 @@
``reduce``
=========
.. versionadded:: 1.41
The ``reduce`` filter was added in Twig 1.41.
The ``reduce`` filter iteratively reduces a sequence or a mapping to a single
value using an arrow function, so as to reduce it to a single value. The arrow
function receives the return value of the previous iteration and the current
value of the sequence or mapping:
.. code-block:: twig
{% set numbers = [1, 2, 3] %}
{{ numbers|reduce(|carry, v| => carry + v) }}
{# output 6 #}
The ``reduce`` filter takes an ``initial`` value as a second argument:
.. code-block:: twig
{{ numbers|reduce(|carry, v| => carry + v, 10) }}
{# output 16 #}
Note that the arrow function has access to the current context.
Arguments
---------
* ``array``: The sequence or mapping
* ``arrow``: The arrow function
* ``initial``: The initial value
+42 -5
View File
@@ -14,6 +14,7 @@ namespace Twig;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\BlockReferenceExpression;
@@ -68,8 +69,12 @@ class ExpressionParser
}
}
public function parseExpression($precedence = 0)
public function parseExpression($precedence = 0, $allowArrow = false)
{
if ($allowArrow && $arrow = $this->parseArrow()) {
return $arrow;
}
$expr = $this->getPrimary();
$token = $this->parser->getCurrentToken();
while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
@@ -98,6 +103,38 @@ class ExpressionParser
return $expr;
}
/**
* @return ArrowFunctionExpression|null
*/
private function parseArrow()
{
$stream = $this->parser->getStream();
$token = $this->parser->getCurrentToken();
$line = $token->getLine();
if (!$token->test(Token::PUNCTUATION_TYPE, '|')) {
return null;
}
$stream->next();
$names = [];
while (true) {
$token = $this->parser->getCurrentToken();
if (!$token->test(Token::NAME_TYPE)) {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext());
}
$names[] = $token->getValue();
$stream->next();
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
$stream->expect(Token::PUNCTUATION_TYPE, '|');
$stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), $names, $line);
}
protected function getPrimary()
{
$token = $this->parser->getCurrentToken();
@@ -499,7 +536,7 @@ class ExpressionParser
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = new Node();
} else {
$arguments = $this->parseArguments(true);
$arguments = $this->parseArguments(true, false, true);
}
$class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
@@ -526,7 +563,7 @@ class ExpressionParser
*
* @throws SyntaxError
*/
public function parseArguments($namedArguments = false, $definition = false)
public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
{
$args = [];
$stream = $this->parser->getStream();
@@ -541,7 +578,7 @@ class ExpressionParser
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
$value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else {
$value = $this->parseExpression();
$value = $this->parseExpression(0, $allowArrow);
}
$name = null;
@@ -558,7 +595,7 @@ class ExpressionParser
throw new SyntaxError(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext());
}
} else {
$value = $this->parseExpression();
$value = $this->parseExpression(0, $allowArrow);
}
}
+18
View File
@@ -194,6 +194,9 @@ class CoreExtension extends AbstractExtension
new TwigFilter('sort', 'twig_sort_filter'),
new TwigFilter('merge', 'twig_array_merge'),
new TwigFilter('batch', 'twig_array_batch'),
new TwigFilter('filter', 'twig_array_filter'),
new TwigFilter('map', 'twig_array_map'),
new TwigFilter('reduce', 'twig_array_reduce'),
// string/array filters
new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
@@ -1682,4 +1685,19 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
return $result;
}
function twig_array_filter($array, $arrow)
{
return array_filter($array, $arrow);
}
function twig_array_map($array, $arrow)
{
return array_map($arrow, $array);
}
function twig_array_reduce($array, $arrow, $initial = null)
{
return array_reduce($array, $arrow, $initial);
}
}
+6 -1
View File
@@ -333,8 +333,13 @@ class Lexer implements \Twig_LexerInterface
}
}
// arrow function
if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
$this->pushToken(Token::ARROW_TYPE, '=>');
$this->moveCursor('=>');
}
// operators
if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
$this->moveCursor($match[0]);
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression;
use Twig\Compiler;
/**
* Represents an arrow function.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ArrowFunctionExpression extends AbstractExpression
{
public function __construct(AbstractExpression $expr, array $names, $lineno, $tag = null)
{
parent::__construct(['expr' => $expr], ['names' => $names], $lineno, $tag);
}
public function compile(Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->raw('function (')
;
foreach ($this->getAttribute('names') as $i => $name) {
if ($i) {
$compiler->raw(', ');
}
$compiler->raw('$__'.$name.'__');
}
$compiler
->raw(') use ($context) { ')
;
foreach ($this->getAttribute('names') as $name) {
$compiler->raw('$context["'.$name.'"] = $__'.$name.'__; ');
}
$compiler
->raw('return ')
->subcompile($this->getNode('expr'))
->raw('; }')
;
}
}
+6
View File
@@ -38,6 +38,7 @@ class Token
const PUNCTUATION_TYPE = 9;
const INTERPOLATION_START_TYPE = 10;
const INTERPOLATION_END_TYPE = 11;
const ARROW_TYPE = 12;
/**
* @param int $type The type of the token
@@ -157,6 +158,9 @@ class Token
case self::INTERPOLATION_END_TYPE:
$name = 'INTERPOLATION_END_TYPE';
break;
case self::ARROW_TYPE:
$name = 'ARROW_TYPE';
break;
default:
throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
}
@@ -200,6 +204,8 @@ class Token
return 'begin of string interpolation';
case self::INTERPOLATION_END_TYPE:
return 'end of string interpolation';
case self::ARROW_TYPE:
return 'arrow function';
default:
throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
}
@@ -0,0 +1,18 @@
--TEST--
"filter" filter
--TEMPLATE--
{% set offset = 3 %}
{% for k, v in [1, 5, 3, 4, 5]|filter(|v| => v > offset) -%}
{{ k }} = {{ v }}
{% endfor %}
{% for k, v in {a: 1, b: 2, c: 5, d: 2}|filter(|v| => v > offset) -%}
{{ k }} = {{ v }}
{% endfor %}
--DATA--
return []
--EXPECT--
1 = 5
3 = 4
4 = 5
c = 5
+18
View File
@@ -0,0 +1,18 @@
--TEST--
"map" filter
--TEMPLATE--
{% set offset = 3 %}
{% for k, v in [1, 2]|map(|item| => item + 2 ) -%}
{{ k }} = {{ v }}
{% endfor %}
{% for k, v in {a: 1, b: 2}|map(|item| => item ~ "*" ) -%}
{{ k }} = {{ v }}
{% endfor %}
--DATA--
return []
--EXPECT--
0 = 3
1 = 4
a = 1*
b = 2*
@@ -0,0 +1,10 @@
--TEST--
"reduce" filter
--TEMPLATE--
{% set offset = 3 %}
{{ [1, -1, 4]|reduce(|carry, item| => carry + item + offset, 10) }}
--DATA--
return []
--EXPECT--
23