Skip to content

Commit f17156b

Browse files
committed
PHPLIB-81: CachingIterator fixes for rewind and count
Ensure that a call to CachingIterator::rewind() no longer exhausts results up front, which would impose a BufferedIterator design. Initializing the cache to an empty array also ensures that we can count an empty result set.
1 parent 5594619 commit f17156b

File tree

2 files changed

+83
-3
lines changed

2 files changed

+83
-3
lines changed

src/Model/CachingIterator.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,20 @@
3333
*/
3434
class CachingIterator implements Countable, Iterator
3535
{
36-
private $items;
36+
private $items = [];
3737
private $iterator;
38+
private $iteratorAdvanced = false;
3839
private $iteratorExhausted = false;
3940

4041
/**
42+
* Constructor.
43+
*
44+
* Initialize the iterator and stores the first item in the cache. This
45+
* effectively rewinds the Traversable and the wrapping Generator, which
46+
* will execute up to its first yield statement. Additionally, this mimics
47+
* behavior of the SPL iterators and allows users to omit an explicit call
48+
* to rewind() before using the other methods.
49+
*
4150
* @param Traversable $traversable
4251
*/
4352
public function __construct(Traversable $traversable)
@@ -95,7 +104,13 @@ public function next()
95104
*/
96105
public function rewind()
97106
{
98-
$this->exhaustIterator();
107+
/* If the iterator has advanced, exhaust it now so that future iteration
108+
* can rely on the cache.
109+
*/
110+
if ($this->iteratorAdvanced) {
111+
$this->exhaustIterator();
112+
}
113+
99114
reset($this->items);
100115
}
101116

@@ -143,6 +158,7 @@ private function wrapTraversable(Traversable $traversable)
143158
{
144159
foreach ($traversable as $key => $value) {
145160
yield $key => $value;
161+
$this->iteratorAdvanced = true;
146162
}
147163

148164
$this->iteratorExhausted = true;

tests/Model/CachingIteratorTest.php

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Tests\Model;
44

55
use MongoDB\Model\CachingIterator;
6+
use Exception;
67

78
class CachingIteratorTest extends \PHPUnit_Framework_TestCase
89
{
@@ -19,6 +20,15 @@ public function testTraversingGeneratorConsumesIt()
1920
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
2021
}
2122

23+
public function testConstructorRewinds()
24+
{
25+
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
26+
27+
$this->assertTrue($iterator->valid());
28+
$this->assertSame(0, $iterator->key());
29+
$this->assertSame(1, $iterator->current());
30+
}
31+
2232
public function testIteration()
2333
{
2434
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
@@ -34,13 +44,44 @@ public function testIteration()
3444
$this->assertFalse($iterator->valid());
3545
}
3646

47+
public function testIterationWithEmptySet()
48+
{
49+
$iterator = new CachingIterator($this->getTraversable([]));
50+
51+
$iterator->rewind();
52+
$this->assertFalse($iterator->valid());
53+
}
54+
55+
public function testPartialIterationDoesNotExhaust()
56+
{
57+
$traversable = $this->getTraversableThatThrows([1, 2, new Exception]);
58+
$iterator = new CachingIterator($traversable);
59+
60+
$expectedKey = 0;
61+
$expectedItem = 1;
62+
63+
foreach ($iterator as $key => $item) {
64+
$this->assertSame($expectedKey++, $key);
65+
$this->assertSame($expectedItem++, $item);
66+
67+
if ($key === 1) {
68+
break;
69+
}
70+
}
71+
72+
$this->assertTrue($iterator->valid());
73+
}
74+
3775
public function testRewindAfterPartialIteration()
3876
{
3977
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
4078

79+
$iterator->rewind();
80+
$this->assertTrue($iterator->valid());
81+
$this->assertSame(0, $iterator->key());
4182
$this->assertSame(1, $iterator->current());
42-
$iterator->next();
4383

84+
$iterator->next();
4485
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
4586
}
4687

@@ -53,14 +94,37 @@ public function testCount()
5394
public function testCountAfterPartialIteration()
5495
{
5596
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
97+
98+
$iterator->rewind();
99+
$this->assertTrue($iterator->valid());
100+
$this->assertSame(0, $iterator->key());
101+
$this->assertSame(1, $iterator->current());
102+
56103
$iterator->next();
57104
$this->assertCount(3, $iterator);
58105
}
59106

107+
public function testCountWithEmptySet()
108+
{
109+
$iterator = new CachingIterator($this->getTraversable([]));
110+
$this->assertCount(0, $iterator);
111+
}
112+
60113
private function getTraversable($items)
61114
{
62115
foreach ($items as $item) {
63116
yield $item;
64117
}
65118
}
119+
120+
private function getTraversableThatThrows($items)
121+
{
122+
foreach ($items as $item) {
123+
if ($item instanceof Exception) {
124+
throw $item;
125+
} else {
126+
yield $item;
127+
}
128+
}
129+
}
66130
}

0 commit comments

Comments
 (0)