Repository: twigphp/Twig (php)

Issue:
[Twig\Loader\ChainLoader] Constructor could accept iterable instead of array.
Proposal - Instead of current constructor:

```php
    /**
     * @param LoaderInterface[] $loaders
     */
    public function __construct(array $loaders = [])
    {
        foreach ($loaders as $loader) {
            $this->addLoader($loader);
        }
    }
```

replace it with:

```php
    /**
     * @param iterable<LoaderInterface> $loaders
     */
    public function __construct(iterable $loaders = [])
    {
        foreach ($loaders as $loader) {
            $this->addLoader($loader);
        }
    }
```

No BC break, allows to use `tagged_iterator` in SF autowiring for creating our own Twig environment (beside templating delivered with SF app by default).

WDYT?

Pull request description:
ChainLoader constructor should accept iterable instead of array
Closes #4200

Patch:
diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php
index 163c029f..8393711f 100644
--- a/src/Loader/ChainLoader.php
+++ b/src/Loader/ChainLoader.php
@@ -21,22 +21,33 @@ use Twig\Source;
  */
 final class ChainLoader implements LoaderInterface
 {
+    /**
+     * @var \Traversable<LoaderInterface>|LoaderInterface[]
+     */
+    private $loaders;
+
+    /**
+     * @var array<string, bool>
+     */
     private $hasSourceCache = [];
-    private $loaders = [];
 
     /**
-     * @param LoaderInterface[] $loaders
+     * @param iterable<LoaderInterface> $loaders
      */
-    public function __construct(array $loaders = [])
+    public function __construct(iterable $loaders = [])
     {
-        foreach ($loaders as $loader) {
-            $this->addLoader($loader);
-        }
+        $this->loaders = $loaders;
     }
 
     public function addLoader(LoaderInterface $loader): void
     {
-        $this->loaders[] = $loader;
+        $current = $this->loaders;
+
+        $this->loaders = (static function () use ($current, $loader): \Generator {
+            yield from $current;
+            yield $loader;
+        })();
+
         $this->hasSourceCache = [];
     }
 
@@ -45,13 +56,18 @@ final class ChainLoader implements LoaderInterface
      */
     public function getLoaders(): array
     {
+        if (!\is_array($this->loaders)) {
+            $this->loaders = \iterator_to_array($this->loaders, false);
+        }
+
         return $this->loaders;
     }
 
     public function getSourceContext(string $name): Source
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }
@@ -72,7 +88,7 @@ final class ChainLoader implements LoaderInterface
             return $this->hasSourceCache[$name];
         }
 
-        foreach ($this->loaders as $loader) {
+        foreach ($this->getLoaders() as $loader) {
             if ($loader->exists($name)) {
                 return $this->hasSourceCache[$name] = true;
             }
@@ -84,7 +100,8 @@ final class ChainLoader implements LoaderInterface
     public function getCacheKey(string $name): string
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }
@@ -102,7 +119,8 @@ final class ChainLoader implements LoaderInterface
     public function isFresh(string $name, int $time): bool
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }


Test patch:
diff --git a/tests/Loader/ChainTest.php b/tests/Loader/ChainTest.php
index faaaebe3..52d6d4c7 100644
--- a/tests/Loader/ChainTest.php
+++ b/tests/Loader/ChainTest.php
@@ -72,10 +72,30 @@ class ChainTest extends TestCase
 
     public function testAddLoader()
     {
-        $loader = new ChainLoader();
-        $loader->addLoader(new ArrayLoader(['foo' => 'bar']));
-
-        $this->assertEquals('bar', $loader->getSourceContext('foo')->getCode());
+        $fooLoader = new ArrayLoader(['foo' => 'foo:code']);
+        $barLoader = new ArrayLoader(['bar' => 'bar:code']);
+        $bazLoader = new ArrayLoader(['baz' => 'baz:code']);
+        $quxLoader = new ArrayLoader(['qux' => 'qux:code']);
+
+        $loader = new ChainLoader((static function () use ($fooLoader, $barLoader): \Generator {
+            yield $fooLoader;
+            yield $barLoader;
+        })());
+
+        $loader->addLoader($bazLoader);
+        $loader->addLoader($quxLoader);
+
+        $this->assertEquals('foo:code', $loader->getSourceContext('foo')->getCode());
+        $this->assertEquals('bar:code', $loader->getSourceContext('bar')->getCode());
+        $this->assertEquals('baz:code', $loader->getSourceContext('baz')->getCode());
+        $this->assertEquals('qux:code', $loader->getSourceContext('qux')->getCode());
+
+        $this->assertEquals([
+            $fooLoader,
+            $barLoader,
+            $bazLoader,
+            $quxLoader,
+        ], $loader->getLoaders());
     }
 
     public function testExists()