Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.29% covered (warning)
89.29%
25 / 28
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
LookaheadIterator
89.29% covered (warning)
89.29%
25 / 28
60.00% covered (warning)
60.00%
3 / 5
13.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 lookAhead
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 lookBehind
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 next
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
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 InvalidArgumentException;
22use LimitIterator;
23use OutOfBoundsException;
24
25/**
26 * An iterator that allows peeking at the next value(s) and stepping back to the previous value(s)
27 * without advancing the iteration.
28 *
29 * This iterator extends `IteratorIterator` and provides methods `peek()` and `prev()`
30 * to inspect the next and previous values **without modifying** the current iteration state.
31 *
32 * ## Usage Example:
33 *
34 * @example Using LookaheadIterator
35 * ```php
36 * use FastForward\Iterator\LookaheadIterator;
37 * use ArrayIterator;
38 *
39 * $data = new ArrayIterator(['A', 'B', 'C', 'D']);
40 * $lookaheadIterator = new LookaheadIterator($data);
41 *
42 * foreach ($lookaheadIterator as $value) {
43 *     echo "Current: " . var_export($value, true) . " | Next: " . var_export($lookaheadIterator->peek(), true) . " | Prev: " . var_export($lookaheadIterator->prev(), true) . "\n";
44 * }
45 * // Outputs:
46 * // Current: 'A' | Next: 'B' | Prev: null
47 * // Current: 'B' | Next: 'C' | Prev: 'A'
48 * // Current: 'C' | Next: 'D' | Prev: 'B'
49 * // Current: 'D' | Next: null | Prev: 'C'
50 * ```
51 *
52 * @since 1.1.0
53 */
54class LookaheadIterator extends CountableIteratorIterator
55{
56    /**
57     * @var int the current iterator position
58     */
59    private int $position = 0;
60
61    /**
62     * @var LimitIterator A separate instance of the iterator used for peeking.
63     *
64     * This iterator ensures that calling `lookAhead()` and `lookBehind()` does not affect the main iterator's position.
65     */
66    private  LimitIterator $peekableInnerIterator;
67
68    /**
69     * Initializes the LookaheadIterator.
70     *
71     * @param iterable $iterator the iterator to wrap
72     */
73    public function __construct(iterable $iterator)
74    {
75        parent::__construct(new IterableIterator($iterator));
76        $this->peekableInnerIterator = new LimitIterator(self::getInnerIterator());
77    }
78
79    /**
80     * Retrieves the next value(s) without advancing the iterator.
81     *
82     * If `$count` is specified, an array of the next `$count` values will be returned.
83     *
84     * @param int $count the number of upcoming values to peek at (default: 1)
85     *
86     * @return mixed the next value, an array of upcoming values, or `null` if no further elements exist
87     *
88     * @throws InvalidArgumentException if `$count` is less than 1
89     */
90    public function lookAhead(int $count = 1): mixed
91    {
92        if ($count < 1) {
93            throw new InvalidArgumentException('Peek count must be at least 1.');
94        }
95
96        try {
97            $peekIterator = new LimitIterator($this->peekableInnerIterator, $this->position + 1, $count);
98            $result       = iterator_to_array($peekIterator, false);
99
100            // Reset the peek iterator to avoid side effects
101            $this->peekableInnerIterator->seek($this->position);
102        } catch (OutOfBoundsException) {
103            return null;
104        }
105
106        return 1 === $count || [] === $result ? ($result[0] ?? null) : $result;
107    }
108
109    /**
110     * Retrieves the previous value(s) without moving the iterator backward.
111     *
112     * If `$count` is specified, an array of the previous `$count` values will be returned.
113     *
114     * @param int $count the number of previous values to retrieve (default: 1)
115     *
116     * @return mixed the previous value, an array of previous values, or `null` if no previous elements exist
117     *
118     * @throws InvalidArgumentException if `$count` is less than 1
119     */
120    public function lookBehind(int $count = 1): mixed
121    {
122        if ($count < 1) {
123            throw new InvalidArgumentException('Prev count must be at least 1.');
124        }
125
126        // Ensure we don't try to look back more than we've seen.
127        $lookBehind = min($count, $this->position);
128
129        if ($this->position - $lookBehind < 0) {
130            return null;
131        }
132
133        try {
134            // Get from the caching iterator, which maintains a history of seen elements.
135            $prevIterator = new LimitIterator($this->peekableInnerIterator, max(
136                0,
137                $this->position - $count
138            ), $lookBehind);
139            $result       = iterator_to_array($prevIterator, false);
140
141            // Reset the peek iterator to avoid side effects
142            $this->peekableInnerIterator->seek($this->position);
143        } catch (OutOfBoundsException) {
144            return null;
145        }
146
147        return 1 === $count ? ($result[0] ?? null) : $result;
148    }
149
150    /**
151     * Advances the iterator and updates the internal position counter.
152     *
153     * This method increments the internal position tracker and moves the iterator forward.
154     *
155     * @return void
156     */
157    public function next(): void
158    {
159        parent::next();
160        ++$this->position;
161    }
162
163    /**
164     * Resets the iterator to the first available element.
165     *
166     * This method rewinds both the main iterator and the peeking iterator,
167     * ensuring that both are in sync when restarting the iteration.
168     *
169     * @return void
170     */
171    public function rewind(): void
172    {
173        parent::rewind();
174        $this->position = 0;
175    }
176}