mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-07 16:06:53 +00:00
added "filter", "map", and "reduce" filters
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
---------
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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('; }')
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user