Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChainIterableIterator
96.97% covered (success)
96.97%
32 / 33
85.71% covered (warning)
85.71%
6 / 7
17
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 count
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 valid
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 current
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 key
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 next
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/iterators.
7 *
8 * This source file is subject to the license that is bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/iterators
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Iterator;
20
21use Countable;
22use Iterator;
23
24/**
25 * An iterator that chains multiple iterable sources together into a single unified iterator.
26 *
27 * This iterator SHALL accept any number of iterable values (arrays, Traversables, or Iterators)
28 * and iterate over them in order. When the current iterator is exhausted, it proceeds to the next.
29 *
30 * The class MUST ensure all incoming values are wrapped as \Iterator instances, either natively
31 * or by converting Traversables or arrays using standard SPL iterators.
32 *
33 * Example usage:
34 *
35 * ```php
36 * $it = new ChainIterableIterator([1, 2], new ArrayIterator([3, 4]));
37 * foreach ($it as $value) {
38 *     echo $value;
39 * }
40 * // Output: 1234
41 * ```
42 *
43 * @since 1.1.0
44 */
45 class ChainIterableIterator implements Iterator, Countable
46{
47    /**
48     * @var Iterator[] a list of iterators chained in sequence
49     */
50    private array $iterators;
51
52    /**
53     * @var int the index of the currently active iterator
54     */
55    private int $currentIndex = 0;
56
57    /**
58     * @var int the normalized sequential key across all chained iterators
59     */
60    private int $position = 0;
61
62    /**
63     * Constructs a ChainIterableIterator with one or more iterable sources.
64     *
65     * Each iterable SHALL be normalized to a \Iterator instance using:
66     * - \ArrayIterator for arrays
67     * - \IteratorIterator for Traversable objects
68     * - Directly used if already an \Iterator
69     *
70     * @param iterable ...$iterables One or more iterable data sources to chain.
71     */
72    public function __construct(iterable ...$iterables)
73    {
74        $this->iterators = array_map(
75            static fn(iterable $iterable): IterableIterator => new IterableIterator($iterable),
76            $iterables
77        );
78    }
79
80    /**
81     * Counts the total number of elements across all chained iterators.
82     *
83     * This method iterates through each underlying iterator and sums their counts.
84     *
85     * @return int the total count of elements in all chained iterators
86     */
87    public function count(): int
88    {
89        return array_reduce(
90            $this->iterators,
91            static fn(int $carry, IterableIterator $iterator): int => $carry + $iterator->count(),
92            0
93        );
94    }
95
96    /**
97     * Rewinds all underlying iterators and resets the position.
98     *
99     * Each chained iterator SHALL be rewound to its beginning.
100     *
101     * @return void
102     */
103    public function rewind(): void
104    {
105        $this->currentIndex = 0;
106        $this->position = 0;
107        foreach ($this->iterators as $iterable) {
108            $iterable->rewind();
109        }
110
111        // Após o rewind, reposiciona no primeiro iterador válido
112        while (
113            isset($this->iterators[$this->currentIndex])
114            && ! $this->iterators[$this->currentIndex]->valid()
115        ) {
116            ++$this->currentIndex;
117        }
118    }
119
120    /**
121     * Checks whether the current position is valid across chained iterators.
122     *
123     * Iteration continues until the current iterator is valid or all are exhausted.
124     *
125     * @return bool TRUE if there are more elements to iterate; FALSE otherwise
126     */
127    public function valid(): bool
128    {
129        if (! isset($this->iterators[$this->currentIndex])) {
130            return false;
131        }
132
133        return $this->iterators[$this->currentIndex]->valid();
134    }
135
136    /**
137     * Returns the current element from the active iterator.
138     *
139     * @return mixed|null the current element or NULL if iteration is invalid
140     */
141    public function current(): mixed
142    {
143        if (! $this->valid()) {
144            return null;
145        }
146
147        return $this->iterators[$this->currentIndex]->current();
148    }
149
150    /**
151     * @return int|null
152     */
153    public function key(): ?int
154    {
155        if (! $this->valid()) {
156            return null;
157        }
158
159        return $this->position;
160    }
161
162    /**
163     * Moves the pointer of the active iterator forward.
164     *
165     * @return void
166     */
167    public function next(): void
168    {
169        if (! isset($this->iterators[$this->currentIndex])) {
170            return;
171        }
172
173        $this->iterators[$this->currentIndex]->next();
174
175        while (
176            isset($this->iterators[$this->currentIndex])
177            && ! $this->iterators[$this->currentIndex]->valid()
178        ) {
179            ++$this->currentIndex;
180        }
181
182        if ($this->valid()) {
183            ++$this->position;
184        }
185    }
186}