Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator

This commit is contained in:
Fabien Potencier
2026-09-18 17:33:44 +01:00
parent 8eb76e2b7e
commit 4aae99e92c
4 changed files with 44 additions and 13 deletions
+1
View File
@@ -3,6 +3,7 @@
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
* Add the macro name to the deprecation about omitting parentheses when calling a macro
* Deprecate cloning a `Twig\Environment` instance
* Fix array access with a `Stringable` key on subclasses of `ArrayObject` and `ArrayIterator`
# 3.29.0 (2026-09-18)
+4 -12
View File
@@ -131,14 +131,6 @@ final class CoreExtension extends AbstractExtension
'SplStack',
'WeakMap',
];
/**
* @internal
*/
public const STRINGABLE_KEY_ARRAY_ACCESS_CLASSES = [
'ArrayIterator',
'ArrayObject',
'RecursiveArrayIterator',
];
private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B";
@@ -1757,6 +1749,10 @@ final class CoreExtension extends AbstractExtension
if (Template::METHOD_CALL !== $type) {
$arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
if ($arrayItem instanceof \Stringable && ($object instanceof \ArrayObject || $object instanceof \ArrayIterator)) {
$arrayItem = (string) $arrayItem;
}
if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) {
try {
$env->getExtension(SandboxExtension::class)->getChecker()->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
@@ -1769,10 +1765,6 @@ final class CoreExtension extends AbstractExtension
}
}
if ($object instanceof \ArrayAccess && $arrayItem instanceof \Stringable && \in_array($object::class, self::STRINGABLE_KEY_ARRAY_ACCESS_CLASSES, true)) {
$arrayItem = (string) $arrayItem;
}
if (match (true) {
\is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object),
$object instanceof \ArrayAccess => $object->offsetExists($arrayItem),
+1 -1
View File
@@ -195,7 +195,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
$compiler
->raw('(('.$key.' = ')
->subcompile($attribute)
->raw(') instanceof \Stringable && (is_array('.$var.') || in_array('.$var.'::class, CoreExtension::STRINGABLE_KEY_ARRAY_ACCESS_CLASSES, true)) ? (string) '.$key.' : '.$key.')')
->raw(') instanceof \Stringable && (is_array('.$var.') || '.$var.' instanceof \ArrayObject || '.$var.' instanceof \ArrayIterator) ? (string) '.$key.' : '.$key.')')
;
}
+38
View File
@@ -352,6 +352,36 @@ class TemplateTest extends TestCase
}
}
/**
* @dataProvider getStringableKeySubclassArrayAccessContainers
*/
#[DataProvider('getStringableKeySubclassArrayAccessContainers')]
public function testStringableKeyIsCoercedForSubclassesOfInternalArrayAccess(bool $strict, bool $sandboxed, \ArrayAccess $data): void
{
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
'strict_variables' => $strict,
'autoescape' => false,
]);
$key = new TemplateStringableKey();
if ($sandboxed) {
// subclasses are not part of CoreExtension::ARRAY_LIKE_CLASSES, so the sandbox checks the key as a property
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [$data::class => ['string']], []), true));
}
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
$this->assertSame(1, $key->toStringCalls);
}
public static function getStringableKeySubclassArrayAccessContainers(): iterable
{
foreach (['lax' => false, 'strict' => true] as $mode => $strict) {
foreach (['unsandboxed' => false, 'sandboxed' => true] as $sandboxMode => $sandboxed) {
yield $mode.' '.$sandboxMode.' ArrayObject subclass' => [$strict, $sandboxed, new TemplateArrayObjectSubclass(['string' => 'value'])];
yield $mode.' '.$sandboxMode.' ArrayIterator subclass' => [$strict, $sandboxed, new TemplateArrayIteratorSubclass(['string' => 'value'])];
}
}
}
/**
* @dataProvider getStrictVariablesModes
*/
@@ -829,6 +859,14 @@ class TemplateForTest extends Template
}
}
final class TemplateArrayObjectSubclass extends \ArrayObject
{
}
final class TemplateArrayIteratorSubclass extends \ArrayIterator
{
}
final class TemplateStringableKey implements \Stringable
{
public int $toStringCalls = 0;