Skip to content

Commit ca4cc97

Browse files
committed
Merge pull request #424
2 parents 85f772c + 5ea6f35 commit ca4cc97

File tree

7 files changed

+306
-10
lines changed

7 files changed

+306
-10
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{ "name": "Derick Rethans", "email": "[email protected]" }
1111
],
1212
"require": {
13-
"php": ">=5.4",
13+
"php": ">=5.5",
1414
"ext-hash": "*",
1515
"ext-json": "*",
1616
"ext-mongodb": "^1.3.0"

src/Model/CachingIterator.php

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
/*
3+
* Copyright 2017 MongoDB, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace MongoDB\Model;
19+
20+
use Countable;
21+
use Generator;
22+
use Iterator;
23+
use Traversable;
24+
25+
/**
26+
* Iterator for wrapping a Traversable and caching its results.
27+
*
28+
* By caching results, this iterators allows a Traversable to be counted and
29+
* rewound multiple times, even if the wrapped object does not natively support
30+
* those operations (e.g. MongoDB\Driver\Cursor).
31+
*
32+
* @internal
33+
*/
34+
class CachingIterator implements Countable, Iterator
35+
{
36+
private $items = [];
37+
private $iterator;
38+
private $iteratorAdvanced = false;
39+
private $iteratorExhausted = false;
40+
41+
/**
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+
*
50+
* @param Traversable $traversable
51+
*/
52+
public function __construct(Traversable $traversable)
53+
{
54+
$this->iterator = $this->wrapTraversable($traversable);
55+
$this->storeCurrentItem();
56+
}
57+
58+
/**
59+
* @see http://php.net/countable.count
60+
* @return integer
61+
*/
62+
public function count()
63+
{
64+
$this->exhaustIterator();
65+
66+
return count($this->items);
67+
}
68+
69+
/**
70+
* @see http://php.net/iterator.current
71+
* @return mixed
72+
*/
73+
public function current()
74+
{
75+
return current($this->items);
76+
}
77+
78+
/**
79+
* @see http://php.net/iterator.mixed
80+
* @return mixed
81+
*/
82+
public function key()
83+
{
84+
return key($this->items);
85+
}
86+
87+
/**
88+
* @see http://php.net/iterator.next
89+
* @return void
90+
*/
91+
public function next()
92+
{
93+
if ( ! $this->iteratorExhausted) {
94+
$this->iterator->next();
95+
$this->storeCurrentItem();
96+
}
97+
98+
next($this->items);
99+
}
100+
101+
/**
102+
* @see http://php.net/iterator.rewind
103+
* @return void
104+
*/
105+
public function rewind()
106+
{
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+
114+
reset($this->items);
115+
}
116+
117+
/**
118+
*
119+
* @see http://php.net/iterator.valid
120+
* @return boolean
121+
*/
122+
public function valid()
123+
{
124+
return $this->key() !== null;
125+
}
126+
127+
/**
128+
* Ensures that the inner iterator is fully consumed and cached.
129+
*/
130+
private function exhaustIterator()
131+
{
132+
while ( ! $this->iteratorExhausted) {
133+
$this->next();
134+
}
135+
}
136+
137+
/**
138+
* Stores the current item in the cache.
139+
*/
140+
private function storeCurrentItem()
141+
{
142+
$key = $this->iterator->key();
143+
144+
if ($key === null) {
145+
return;
146+
}
147+
148+
$this->items[$key] = $this->iterator->current();
149+
}
150+
151+
/**
152+
* Wraps the Traversable with a Generator.
153+
*
154+
* @param Traversable $traversable
155+
* @return Generator
156+
*/
157+
private function wrapTraversable(Traversable $traversable)
158+
{
159+
foreach ($traversable as $key => $value) {
160+
yield $key => $value;
161+
$this->iteratorAdvanced = true;
162+
}
163+
164+
$this->iteratorExhausted = true;
165+
}
166+
}

src/Operation/ListCollections.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use MongoDB\Driver\Server;
2323
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2424
use MongoDB\Exception\InvalidArgumentException;
25+
use MongoDB\Model\CachingIterator;
2526
use MongoDB\Model\CollectionInfoCommandIterator;
2627
use MongoDB\Model\CollectionInfoIterator;
2728
use MongoDB\Model\CollectionInfoLegacyIterator;
@@ -107,7 +108,7 @@ private function executeCommand(Server $server)
107108
$cursor = $server->executeCommand($this->databaseName, new Command($cmd));
108109
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
109110

110-
return new CollectionInfoCommandIterator($cursor);
111+
return new CollectionInfoCommandIterator(new CachingIterator($cursor));
111112
}
112113

113114
/**
@@ -138,6 +139,6 @@ private function executeLegacy(Server $server)
138139
$cursor = $server->executeQuery($this->databaseName . '.system.namespaces', new Query($filter, $options));
139140
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
140141

141-
return new CollectionInfoLegacyIterator($cursor);
142+
return new CollectionInfoLegacyIterator(new CachingIterator($cursor));
142143
}
143144
}

src/Operation/ListIndexes.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use MongoDB\Driver\Server;
2323
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2424
use MongoDB\Exception\InvalidArgumentException;
25+
use MongoDB\Model\CachingIterator;
2526
use MongoDB\Model\IndexInfoIterator;
2627
use MongoDB\Model\IndexInfoIteratorIterator;
2728
use EmptyIterator;
@@ -114,7 +115,7 @@ private function executeCommand(Server $server)
114115

115116
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
116117

117-
return new IndexInfoIteratorIterator($cursor);
118+
return new IndexInfoIteratorIterator(new CachingIterator($cursor));
118119
}
119120

120121
/**
@@ -136,6 +137,6 @@ private function executeLegacy(Server $server)
136137
$cursor = $server->executeQuery($this->databaseName . '.system.indexes', new Query($filter, $options));
137138
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
138139

139-
return new IndexInfoIteratorIterator($cursor);
140+
return new IndexInfoIteratorIterator(new CachingIterator($cursor));
140141
}
141142
}

tests/Model/CachingIteratorTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Model;
4+
5+
use MongoDB\Model\CachingIterator;
6+
use Exception;
7+
8+
class CachingIteratorTest extends \PHPUnit_Framework_TestCase
9+
{
10+
/**
11+
* Sanity check for all following tests.
12+
*
13+
* @expectedException \Exception
14+
* @expectedExceptionMessage Cannot traverse an already closed generator
15+
*/
16+
public function testTraversingGeneratorConsumesIt()
17+
{
18+
$iterator = $this->getTraversable([1, 2, 3]);
19+
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
20+
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
21+
}
22+
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+
32+
public function testIteration()
33+
{
34+
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
35+
36+
$expectedKey = 0;
37+
$expectedItem = 1;
38+
39+
foreach ($iterator as $key => $item) {
40+
$this->assertSame($expectedKey++, $key);
41+
$this->assertSame($expectedItem++, $item);
42+
}
43+
44+
$this->assertFalse($iterator->valid());
45+
}
46+
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+
75+
public function testRewindAfterPartialIteration()
76+
{
77+
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
78+
79+
$iterator->rewind();
80+
$this->assertTrue($iterator->valid());
81+
$this->assertSame(0, $iterator->key());
82+
$this->assertSame(1, $iterator->current());
83+
84+
$iterator->next();
85+
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
86+
}
87+
88+
public function testCount()
89+
{
90+
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
91+
$this->assertCount(3, $iterator);
92+
}
93+
94+
public function testCountAfterPartialIteration()
95+
{
96+
$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+
103+
$iterator->next();
104+
$this->assertCount(3, $iterator);
105+
}
106+
107+
public function testCountWithEmptySet()
108+
{
109+
$iterator = new CachingIterator($this->getTraversable([]));
110+
$this->assertCount(0, $iterator);
111+
}
112+
113+
private function getTraversable($items)
114+
{
115+
foreach ($items as $item) {
116+
yield $item;
117+
}
118+
}
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+
}
130+
}

tests/Operation/ListCollectionsFunctionalTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ public function testListCollectionsForNewlyCreatedDatabase()
2020
$this->assertEquals(1, $writeResult->getInsertedCount());
2121

2222
$operation = new ListCollections($this->getDatabaseName(), ['filter' => ['name' => $this->getCollectionName()]]);
23-
// Convert the CollectionInfoIterator to an array since we cannot rewind its cursor
24-
$collections = iterator_to_array($operation->execute($server));
23+
$collections = $operation->execute($server);
24+
25+
$this->assertInstanceOf('MongoDB\Model\CollectionInfoIterator', $collections);
2526

2627
$this->assertCount(1, $collections);
2728

tests/Operation/ListIndexesFunctionalTest.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ public function testListIndexesForNewlyCreatedCollection()
2222

2323
$this->assertInstanceOf('MongoDB\Model\IndexInfoIterator', $indexes);
2424

25-
// Convert the CursorInfoIterator to an array since we cannot rewind its cursor
26-
$indexes = iterator_to_array($indexes);
27-
2825
$this->assertCount(1, $indexes);
2926

3027
foreach ($indexes as $index) {

0 commit comments

Comments
 (0)