bug #4713 Fix cycle() with non-countable ArrayAccess+Traversable objects (yoeunes)

This PR was merged into the 3.x branch.

Discussion
----------

Fix cycle() with non-countable ArrayAccess+Traversable objects

When using `cycle()` with an object that implements `\ArrayAccess` and `\Traversable` but is not `\Countable`, the function currently returns the object instance immediately after triggering the deprecation notice.

This prevents the value from being converted to an array, causing the cycle logic to fail or return the object itself.

This PR removes the early return to ensure `self::toArray()` is called, allowing these objects to be cycled correctly as expected.

**How to test**

```php
$seq = new class implements \ArrayAccess, \IteratorAggregate {
    public function offsetExists($offset): bool { return true; }
    public function offsetGet($offset): mixed { return 'val'; }
    public function offsetSet($offset, $value): void {}
    public function offsetUnset($offset): void {}
    public function getIterator(): \Traversable { yield 'odd'; yield 'even'; }
};

// Should return 'odd', currently returns the $seq object
$result = CoreExtension::cycle($seq, 0);
```

Related to #4241

Commits
-------

473653d19b [Core] Fix cycle() with non-countable ArrayAccess+Traversable objects
This commit is contained in:
Fabien Potencier
2025-11-25 07:55:14 +01:00
2 changed files with 42 additions and 3 deletions
+1 -3
View File
@@ -417,10 +417,8 @@ final class CoreExtension extends AbstractExtension
trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__);
return $values;
$values = self::toArray($values, false);
}
$values = self::toArray($values, false);
}
if (!$count = \count($values)) {
+41
View File
@@ -21,6 +21,7 @@ namespace Twig\Tests\Extension;
*/
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
@@ -31,6 +32,8 @@ use Twig\Sandbox\SecurityPolicy;
class CoreTest extends TestCase
{
use ExpectDeprecationTrait;
/**
* @dataProvider provideCycleCases
*/
@@ -407,6 +410,44 @@ class CoreTest extends TestCase
{
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
}
/**
* @group legacy
*/
public function testCycleWithArrayAccessAndTraversableButNotCountable()
{
$this->expectDeprecation('Since twig/twig 3.12: Passing a non-countable sequence of values to "Twig\Extension\CoreExtension::cycle()" is deprecated.');
$seq = new class implements \ArrayAccess, \IteratorAggregate {
public function offsetExists($offset): bool
{
return true;
}
public function offsetGet($offset): mixed
{
return 'val';
}
public function offsetSet($offset, $value): void
{
}
public function offsetUnset($offset): void
{
}
public function getIterator(): \Traversable
{
yield 'odd';
yield 'even';
}
};
$result = CoreExtension::cycle($seq, 0);
$this->assertEquals('odd', $result, 'cycle should return the first item from the traversable sequence, not the sequence itself.');
}
}
final class CoreTestIteratorAggregate implements \IteratorAggregate