Repository: twigphp/Twig (php)

Issue:
Support for closures or at least `Closure::__invoke`
Calling a closure is not supported in twig right now, any variable passed as `\Closure` cannot be called directly as Twig looks up the function in global functions (and extensions), the alternative is to call it via `__invoke` method on closure.

https://3v4l.org/EBR2Y (`__invoke` method does exist on Closures and work the same way as `$closure()` works).

Unfortunately this doesn't work in twig as `twig_get_attribute` uses `get_class_methods` to determine available methods on an object and fallbacks to `__call` if it exists and php  being php `get_class_methods($closure)` doesn't return `__invoke` https://3v4l.org/VKjAF :man_shrugging:  even though it exists and [is documented](https://www.php.net/manual/en/class.closure.php).

I think this can be fixed by checking if `$object` is instance of \Closure and then allow calling `__invoke` or is there a better way ?

I am happy to contribute a PR if it makes sense.

Pull request description:
adds support for invoking closures
- fixes #3848

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index d0584fcd..f8e055b9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,8 @@
-# 3.17.2 (2024-XX-XX)
-
- * n/a
-
-# 3.17.1 (2024-12-12)
+# 3.17.1 (2024-XX-XX)
 
  * Fix the null coalescing operator when the test returns null
  * Fix the Elvis operator when used as '? :' instead of '?:'
+ * Support for invoking closures
 
 # 3.17.0 (2024-12-10)
 
diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst
index b8c1e27d..5f86fb5b 100644
--- a/doc/functions/html_cva.rst
+++ b/doc/functions/html_cva.rst
@@ -20,8 +20,8 @@ function where you define ``base`` classes that should always be present and the
 
     {# templates/alert.html.twig #}
     {% set alert = html_cva(
-        base: 'alert',
-        variants: {
+        base='alert',
+        variants={
             color: {
                 blue: 'bg-blue',
                 red: 'bg-red',
@@ -84,8 +84,8 @@ when multiple other variant conditions are met:
 .. code-block:: html+twig
 
     {% set alert = html_cva(
-        base: 'alert',
-        variants: {
+        base='alert',
+        variants={
             color: {
                 blue: 'bg-blue',
                 red: 'bg-red',
@@ -97,7 +97,7 @@ when multiple other variant conditions are met:
                 lg: 'text-lg',
             }
         },
-        compoundVariants: [{
+        compoundVariants=[{
             // if color = red AND size = (md or lg), add the `font-bold` class
             color: ['red'],
             size: ['md', 'lg'],
@@ -128,8 +128,8 @@ If no variants match, you can define a default set of classes to apply:
 .. code-block:: html+twig
 
     {% set alert = html_cva(
-        base: 'alert',
-        variants: {
+        base='alert',
+        variants={
             color: {
                 blue: 'bg-blue',
                 red: 'bg-red',
@@ -146,7 +146,7 @@ If no variants match, you can define a default set of classes to apply:
                 lg: 'rounded-lg',
             }
         },
-        defaultVariant: {
+        defaultVariant={
             rounded: 'md',
         }
     ) %}
diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php
index b60944c4..01e72856 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -1740,6 +1740,10 @@ final class CoreExtension extends AbstractExtension
 
             static $propertyCheckers = [];
 
+            if ($object instanceof \Closure && '__invoke' === $item) {
+                return $isDefinedTest ? true : $object();
+            }
+
             if (isset($object->$item)
                 || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item)
             ) {
@@ -1777,6 +1781,9 @@ final class CoreExtension extends AbstractExtension
         // precedence: getXxx() > isXxx() > hasXxx()
         if (!isset($cache[$class])) {
             $methods = get_class_methods($object);
+            if ($object instanceof \Closure) {
+                $methods[] = '__invoke';
+            }
             sort($methods);
             $lcMethods = array_map('strtolower', $methods);
             $classCache = [];


Test patch:
diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php
index 4d248996..eb0e2db7 100644
--- a/tests/TemplateTest.php
+++ b/tests/TemplateTest.php
@@ -394,6 +394,10 @@ class TemplateTest extends TestCase
             [true, ['foo' => 'bar'], $arrayAccess, 'vars', [], $anyType],
         ]);
 
+        // test for Closure::__invoke()
+        $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $anyType];
+        $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $methodType];
+
         // tests when input is not an array or object
         $tests = array_merge($tests, [
             [false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a int variable ("42") in "index.twig".'],