diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 66acddf61..a2c258591 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -80,7 +80,7 @@ class ExpressionParser } elseif (isset($op['callable'])) { $expr = $op['callable']($this->parser, $expr); } else { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); $class = $op['class']; $expr = new $class($expr, $expr1, $token->getLine()); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 88cd75458..4eed76b7e 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -23,6 +23,8 @@ use Twig\Node\Expression\Binary\EqualBinary; use Twig\Node\Expression\Binary\FloorDivBinary; use Twig\Node\Expression\Binary\GreaterBinary; use Twig\Node\Expression\Binary\GreaterEqualBinary; +use Twig\Node\Expression\Binary\HasEveryBinary; +use Twig\Node\Expression\Binary\HasSomeBinary; use Twig\Node\Expression\Binary\InBinary; use Twig\Node\Expression\Binary\LessBinary; use Twig\Node\Expression\Binary\LessEqualBinary; @@ -284,6 +286,8 @@ final class CoreExtension extends AbstractExtension 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -1678,6 +1682,32 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) return array_reduce($array, $arrow, $initial); } +function twig_array_some(Environment $env, $array, $arrow) +{ + twig_check_arrow_in_sandbox($env, $arrow, 'some', 'filter'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } + } + + return false; +} + +function twig_array_every(Environment $env, $array, $arrow) +{ + twig_check_arrow_in_sandbox($env, $arrow, 'every', 'filter'); + + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } + + return true; +} + function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) { if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php new file mode 100644 index 000000000..adfabd44c --- /dev/null +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -0,0 +1,33 @@ +raw('twig_array_every($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php new file mode 100644 index 000000000..270da3692 --- /dev/null +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -0,0 +1,33 @@ +raw('twig_array_some($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/tests/Fixtures/expressions/has_every.test b/tests/Fixtures/expressions/has_every.test new file mode 100644 index 000000000..dc43b95ca --- /dev/null +++ b/tests/Fixtures/expressions/has_every.test @@ -0,0 +1,19 @@ +--TEST-- +Twig supports the "has every" operator +--TEMPLATE-- +{% if [0, 2, 4] has every v => 0 == v % 2 %}Every{% else %}Not every{% endif %} items are even in array +{{ ([0, 2, 4] has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in array +{{ ({ a: 0, b: 2, c: 4 } has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in object +{{ ({ a: 0, b: 2, c: 4 } has every (v, k) => "d" > k)? 'Every' : 'Not every' }} keys are before "d" in object +{{ (it has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in iterator +{{ ([0, 1, 2] has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in array +--DATA-- +return ['it' => new \ArrayIterator([0, 2, 4])] +--EXPECT-- +Every items are even in array +Every items are even in array +Every items are even in object +Every keys are before "d" in object +Every items are even in iterator +Not every items are even in array + diff --git a/tests/Fixtures/expressions/has_some.test b/tests/Fixtures/expressions/has_some.test new file mode 100644 index 000000000..c5d75c77d --- /dev/null +++ b/tests/Fixtures/expressions/has_some.test @@ -0,0 +1,19 @@ +--TEST-- +Twig supports the "has some" operator +--TEMPLATE-- +{% if [1, 2, 3] has some v => 0 == v % 2 %}At least one{% else %}No{% endif %} item is even in array +{{ ([1, 2, 3] has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in array +{{ ({ a: 1, b: 2, c: 3 } has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in object +{{ ({ a: 1, b: 2, c: 3 } has some (v, k) => "b" == k)? 'At least one' : 'No' }} key is "b" in object +{{ (it has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in iterator +{{ ([1, 3, 5] has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in array +--DATA-- +return ['it' => new \ArrayIterator([1, 2, 3])] +--EXPECT-- +At least one item is even in array +At least one item is even in array +At least one item is even in object +At least one key is "b" in object +At least one item is even in iterator +No item is even in array +