Repository: twigphp/Twig (php)

Issue:
Arrow function are not allowed outside filters
Arrow functions currently can only be used in a filter as per ExpressionParser.
This prevents multiple functionalities such as
- passing arrows to an include context so it can forward it to an actual filter;
- passing arrows to a macro or a function;
- defining and setting a callable so it can be used in subsequent expressions;
How to use arrow functions as callable?
I'd be really great if Twig arrow functions can be passed as arguments.

e.g. [Laravel collections unique method](https://laravel.com/docs/8.x/collections#method-unique) accepts a callable.

I'd like to do something like this:

```twig
{{ mycollection.unique(item => item.id) }}
```

Is that possible? Or an RFC?

Pull request description:
Allow arrow functions everywhere
Closes #3192
Closes #3402

Arrow functions are now supported for all Twig callables (filters, functions, and tests), macro arguments, and method call arguments.

Arrow functions can also be stored in variables.


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index dda9d519..2a20109f 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 764c5cfd..54f26c4b 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 987d64e1..4e570ff7 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();
                 }
             }
 


Test patch:
diff --git a/tests/Fixtures/macros/arrow_as_arg.test b/tests/Fixtures/macros/arrow_as_arg.test
new file mode 100644
index 00000000..5bae3465
--- /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