Repository: twigphp/Twig (php)

Issue:
Twig\Markup doesn't convert to string when used as object key
When using a `Twig\Markup` as an object key it doesn't get converted to a string automatically.

```twig
{% set someClasses %}
  {{ html_classes(...) }}
{% endset %}

<div class="{{ html_classes({
  (someClasses): someCondition,
}) }}"></div>
```

In a twigfiddle the property doesn't get added at all ([Here's an example](https://twigfiddle.com/9f614g/2)). In my local CraftCMS installation this throws a `Illegal offset type` error instead.

After looking at the source code of `Twig\Markup` I found that I can call `__toString` to make it work:

```twig
<div class="{{ html_classes({
  (someClasses.__toString()): someCondition
}) }}"></div>
```

By now I found out that it makes a lot more sense and solves this entire problem to define `someClasses` like this:

```twig
{% set someClasses = html_classes(...) %}
```

But I wanted to point that out in case this should work nonetheless.

Pull request:
Stringify dynamic mapping keys
Closes #3696

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index ef2f219b..c029bb8e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 # 3.15.0 (2024-XX-XX)
 
+ * Support Markup instances (and any other \Stringable) as dynamic mapping keys
  * Deprecate the `sandbox` tag
  * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options)
 
diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php
index 5f8b0f63..9b6c55c2 100644
--- a/src/Node/Expression/ArrayExpression.php
+++ b/src/Node/Expression/ArrayExpression.php
@@ -98,6 +98,9 @@ class ArrayExpression extends AbstractExpression
                 ++$nextIndex;
             } else {
                 $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
+                if ($pair['key'] instanceof NameExpression) {
+                    $pair['key']->setAttribute('stringify', true);
+                }
 
                 if ($nextIndex !== $key) {
                     if (\is_int($key)) {
diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php
index 286aa5ae..a3f42cd0 100644
--- a/src/Node/Expression/NameExpression.php
+++ b/src/Node/Expression/NameExpression.php
@@ -24,7 +24,7 @@ class NameExpression extends AbstractExpression
 
     public function __construct(string $name, int $lineno)
     {
-        parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
+        parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false, 'stringify' => false], $lineno);
     }
 
     public function compile(Compiler $compiler): void
@@ -54,6 +54,9 @@ class NameExpression extends AbstractExpression
         } elseif (isset($this->specialVars[$name])) {
             $compiler->raw($this->specialVars[$name]);
         } elseif ($this->getAttribute('always_defined')) {
+            if ($this->getAttribute('stringify')) {
+                $compiler->raw(' (string)');
+            }
             $compiler
                 ->raw('$context[')
                 ->string($name)
@@ -61,6 +64,9 @@ class NameExpression extends AbstractExpression
             ;
         } else {
             if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
+                if ($this->getAttribute('stringify')) {
+                    $compiler->raw(' (string)');
+                }
                 $compiler
                     ->raw('($context[')
                     ->string($name)
@@ -72,7 +78,13 @@ class NameExpression extends AbstractExpression
                     ->string($name)
                     ->raw(']) || array_key_exists(')
                     ->string($name)
-                    ->raw(', $context) ? $context[')
+                    ->raw(', $context) ?')
+                ;
+                if ($this->getAttribute('stringify')) {
+                    $compiler->raw(' (string)');
+                }
+                $compiler
+                    ->raw(' $context[')
                     ->string($name)
                     ->raw('] : (function () { throw new RuntimeError(\'Variable ')
                     ->string($name)


Test patch:
diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test
index 35579dc1..1de76e93 100644
--- a/tests/Fixtures/expressions/array.test
+++ b/tests/Fixtures/expressions/array.test
@@ -34,7 +34,8 @@ Twig supports array notation
 {# keys can be any expression #}
 {% set a = 1 %}
 {% set b = "foo" %}
-{% set ary = { (a): 'a', (b): 'b', 'c': 'c', (a ~ b): 'd' } %}
+{% set markup_instance %}fooe{% endset %}
+{% set ary = { (a): 'a', (b): 'b', 'c': 'c', (a ~ b): 'd', (markup_instance): 'e' } %}
 {{ ary|keys|join(',') }}
 {{ ary|join(',') }}
 
@@ -65,8 +66,8 @@ FOO,BAR,
 
 1,2
 
-1,foo,c,1foo
-a,b,c,d
+1,foo,c,1foo,fooe
+a,b,c,d,e
 
 b
 
@@ -95,8 +96,8 @@ FOO,BAR,
 
 1,2
 
-1,foo,c,1foo
-a,b,c,d
+1,foo,c,1foo,fooe
+a,b,c,d,e
 
 b