diff --git a/CHANGELOG b/CHANGELOG index dda9d5191..2a20109f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Allow arrow functions everywhere * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) * Add support for triggering deprecations for future operator precedence changes * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence diff --git a/doc/templates.rst b/doc/templates.rst index 764c5cfda..54f26c4b6 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -960,6 +960,32 @@ The following operators don't fit into any of the other categories: Support for expanding the arguments of a function call was introduced in Twig 3.15. +* ``=>``: The arrow operator allows the creation of functions. A function is + made of arguments (use parentheses for multiple arguments) and an arrow + (``=>``) followed by an expression to execute. The expression has access to + all passed arguments. Arrow functions are supported as arguments for filters, + functions, tests, macros, and method calls. + + For instance, the built-in ``map``, ``reduce``, ``sort``, ``filter``, and + ``find`` filters accept arrow functions as arguments: + + .. code-block:: twig + + {{ people|map(p => p.first_name)|join(', ') }} + + Arrow functions can be stored in variables: + + .. code-block:: twig + + {% set first_name_fn = (p) => p.first_name %} + + {{ people|map(first_name_fn)|join(', ') }} + + .. versionadded:: 3.15 + + Arrow function support for functions, macros, and method calls was added in + Twig 3.15 (filters and tests were already supported). + Operators ~~~~~~~~~ diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 987d64e1f..4e570ff76 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -87,9 +87,13 @@ class ExpressionParser } } - public function parseExpression($precedence = 0, $allowArrow = false) + public function parseExpression($precedence = 0) { - if ($allowArrow && $arrow = $this->parseArrow()) { + if (func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + + if ($arrow = $this->parseArrow()) { return $arrow; } @@ -108,7 +112,7 @@ class ExpressionParser } else { $previous = $this->setDeprecationCheck(true); try { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); } finally { $this->setDeprecationCheck($previous); } @@ -672,7 +676,7 @@ class ExpressionParser if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $this->parseArguments(true, false, true); + $arguments = $this->parseArguments(true); } $filter = $this->getFilter($token->getValue(), $token->getLine()); @@ -708,8 +712,12 @@ class ExpressionParser * * @throws SyntaxError */ - public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false) + public function parseArguments($namedArguments = false, $definition = false) { + if (func_num_args() > 2) { + trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + $args = []; $stream = $this->parser->getStream(); @@ -731,11 +739,11 @@ class ExpressionParser } else { if ($stream->nextIf(Token::SPREAD_TYPE)) { $hasSpread = true; - $value = new SpreadUnary($this->parseExpression(0, $allowArrow), $stream->getCurrent()->getLine()); + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); } elseif ($hasSpread) { throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } else { - $value = $this->parseExpression(0, $allowArrow); + $value = $this->parseExpression(); } } @@ -753,7 +761,7 @@ class ExpressionParser throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); } } else { - $value = $this->parseExpression(0, $allowArrow); + $value = $this->parseExpression(); } } diff --git a/tests/Fixtures/macros/arrow_as_arg.test b/tests/Fixtures/macros/arrow_as_arg.test new file mode 100644 index 000000000..5bae34652 --- /dev/null +++ b/tests/Fixtures/macros/arrow_as_arg.test @@ -0,0 +1,19 @@ +--TEST-- +macro +--TEMPLATE-- +{% set people = [ + {first: "Bob", last: "Smith"}, + {first: "Alice", last: "Dupond"}, +] %} + +{% set first_name_fn = (p) => p.first %} + +{{ _self.display_people(people, first_name_fn) }} + +{% macro display_people(people, fn) %} + {{ people|map(fn)|join(', ') }} +{% endmacro %} +--DATA-- +return [] +--EXPECT-- +Bob, Alice