Commit ca4cc976 authored by Jeremy Mikola's avatar Jeremy Mikola

Merge pull request #424

parents 85f772c2 5ea6f35d
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
{ "name": "Derick Rethans", "email": "github@derickrethans.nl" } { "name": "Derick Rethans", "email": "github@derickrethans.nl" }
], ],
"require": { "require": {
"php": ">=5.4", "php": ">=5.5",
"ext-hash": "*", "ext-hash": "*",
"ext-json": "*", "ext-json": "*",
"ext-mongodb": "^1.3.0" "ext-mongodb": "^1.3.0"
......
<?php
/*
* Copyright 2017 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Model;
use Countable;
use Generator;
use Iterator;
use Traversable;
/**
* Iterator for wrapping a Traversable and caching its results.
*
* By caching results, this iterators allows a Traversable to be counted and
* rewound multiple times, even if the wrapped object does not natively support
* those operations (e.g. MongoDB\Driver\Cursor).
*
* @internal
*/
class CachingIterator implements Countable, Iterator
{
private $items = [];
private $iterator;
private $iteratorAdvanced = false;
private $iteratorExhausted = false;
/**
* Constructor.
*
* Initialize the iterator and stores the first item in the cache. This
* effectively rewinds the Traversable and the wrapping Generator, which
* will execute up to its first yield statement. Additionally, this mimics
* behavior of the SPL iterators and allows users to omit an explicit call
* to rewind() before using the other methods.
*
* @param Traversable $traversable
*/
public function __construct(Traversable $traversable)
{
$this->iterator = $this->wrapTraversable($traversable);
$this->storeCurrentItem();
}
/**
* @see http://php.net/countable.count
* @return integer
*/
public function count()
{
$this->exhaustIterator();
return count($this->items);
}
/**
* @see http://php.net/iterator.current
* @return mixed
*/
public function current()
{
return current($this->items);
}
/**
* @see http://php.net/iterator.mixed
* @return mixed
*/
public function key()
{
return key($this->items);
}
/**
* @see http://php.net/iterator.next
* @return void
*/
public function next()
{
if ( ! $this->iteratorExhausted) {
$this->iterator->next();
$this->storeCurrentItem();
}
next($this->items);
}
/**
* @see http://php.net/iterator.rewind
* @return void
*/
public function rewind()
{
/* If the iterator has advanced, exhaust it now so that future iteration
* can rely on the cache.
*/
if ($this->iteratorAdvanced) {
$this->exhaustIterator();
}
reset($this->items);
}
/**
*
* @see http://php.net/iterator.valid
* @return boolean
*/
public function valid()
{
return $this->key() !== null;
}
/**
* Ensures that the inner iterator is fully consumed and cached.
*/
private function exhaustIterator()
{
while ( ! $this->iteratorExhausted) {
$this->next();
}
}
/**
* Stores the current item in the cache.
*/
private function storeCurrentItem()
{
$key = $this->iterator->key();
if ($key === null) {
return;
}
$this->items[$key] = $this->iterator->current();
}
/**
* Wraps the Traversable with a Generator.
*
* @param Traversable $traversable
* @return Generator
*/
private function wrapTraversable(Traversable $traversable)
{
foreach ($traversable as $key => $value) {
yield $key => $value;
$this->iteratorAdvanced = true;
}
$this->iteratorExhausted = true;
}
}
...@@ -22,6 +22,7 @@ use MongoDB\Driver\Query; ...@@ -22,6 +22,7 @@ use MongoDB\Driver\Query;
use MongoDB\Driver\Server; use MongoDB\Driver\Server;
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Model\CachingIterator;
use MongoDB\Model\CollectionInfoCommandIterator; use MongoDB\Model\CollectionInfoCommandIterator;
use MongoDB\Model\CollectionInfoIterator; use MongoDB\Model\CollectionInfoIterator;
use MongoDB\Model\CollectionInfoLegacyIterator; use MongoDB\Model\CollectionInfoLegacyIterator;
...@@ -107,7 +108,7 @@ class ListCollections implements Executable ...@@ -107,7 +108,7 @@ class ListCollections implements Executable
$cursor = $server->executeCommand($this->databaseName, new Command($cmd)); $cursor = $server->executeCommand($this->databaseName, new Command($cmd));
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
return new CollectionInfoCommandIterator($cursor); return new CollectionInfoCommandIterator(new CachingIterator($cursor));
} }
/** /**
...@@ -138,6 +139,6 @@ class ListCollections implements Executable ...@@ -138,6 +139,6 @@ class ListCollections implements Executable
$cursor = $server->executeQuery($this->databaseName . '.system.namespaces', new Query($filter, $options)); $cursor = $server->executeQuery($this->databaseName . '.system.namespaces', new Query($filter, $options));
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
return new CollectionInfoLegacyIterator($cursor); return new CollectionInfoLegacyIterator(new CachingIterator($cursor));
} }
} }
...@@ -22,6 +22,7 @@ use MongoDB\Driver\Query; ...@@ -22,6 +22,7 @@ use MongoDB\Driver\Query;
use MongoDB\Driver\Server; use MongoDB\Driver\Server;
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Model\CachingIterator;
use MongoDB\Model\IndexInfoIterator; use MongoDB\Model\IndexInfoIterator;
use MongoDB\Model\IndexInfoIteratorIterator; use MongoDB\Model\IndexInfoIteratorIterator;
use EmptyIterator; use EmptyIterator;
...@@ -114,7 +115,7 @@ class ListIndexes implements Executable ...@@ -114,7 +115,7 @@ class ListIndexes implements Executable
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
return new IndexInfoIteratorIterator($cursor); return new IndexInfoIteratorIterator(new CachingIterator($cursor));
} }
/** /**
...@@ -136,6 +137,6 @@ class ListIndexes implements Executable ...@@ -136,6 +137,6 @@ class ListIndexes implements Executable
$cursor = $server->executeQuery($this->databaseName . '.system.indexes', new Query($filter, $options)); $cursor = $server->executeQuery($this->databaseName . '.system.indexes', new Query($filter, $options));
$cursor->setTypeMap(['root' => 'array', 'document' => 'array']); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']);
return new IndexInfoIteratorIterator($cursor); return new IndexInfoIteratorIterator(new CachingIterator($cursor));
} }
} }
<?php
namespace MongoDB\Tests\Model;
use MongoDB\Model\CachingIterator;
use Exception;
class CachingIteratorTest extends \PHPUnit_Framework_TestCase
{
/**
* Sanity check for all following tests.
*
* @expectedException \Exception
* @expectedExceptionMessage Cannot traverse an already closed generator
*/
public function testTraversingGeneratorConsumesIt()
{
$iterator = $this->getTraversable([1, 2, 3]);
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
}
public function testConstructorRewinds()
{
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
$this->assertTrue($iterator->valid());
$this->assertSame(0, $iterator->key());
$this->assertSame(1, $iterator->current());
}
public function testIteration()
{
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
$expectedKey = 0;
$expectedItem = 1;
foreach ($iterator as $key => $item) {
$this->assertSame($expectedKey++, $key);
$this->assertSame($expectedItem++, $item);
}
$this->assertFalse($iterator->valid());
}
public function testIterationWithEmptySet()
{
$iterator = new CachingIterator($this->getTraversable([]));
$iterator->rewind();
$this->assertFalse($iterator->valid());
}
public function testPartialIterationDoesNotExhaust()
{
$traversable = $this->getTraversableThatThrows([1, 2, new Exception]);
$iterator = new CachingIterator($traversable);
$expectedKey = 0;
$expectedItem = 1;
foreach ($iterator as $key => $item) {
$this->assertSame($expectedKey++, $key);
$this->assertSame($expectedItem++, $item);
if ($key === 1) {
break;
}
}
$this->assertTrue($iterator->valid());
}
public function testRewindAfterPartialIteration()
{
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
$iterator->rewind();
$this->assertTrue($iterator->valid());
$this->assertSame(0, $iterator->key());
$this->assertSame(1, $iterator->current());
$iterator->next();
$this->assertSame([1, 2, 3], iterator_to_array($iterator));
}
public function testCount()
{
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
$this->assertCount(3, $iterator);
}
public function testCountAfterPartialIteration()
{
$iterator = new CachingIterator($this->getTraversable([1, 2, 3]));
$iterator->rewind();
$this->assertTrue($iterator->valid());
$this->assertSame(0, $iterator->key());
$this->assertSame(1, $iterator->current());
$iterator->next();
$this->assertCount(3, $iterator);
}
public function testCountWithEmptySet()
{
$iterator = new CachingIterator($this->getTraversable([]));
$this->assertCount(0, $iterator);
}
private function getTraversable($items)
{
foreach ($items as $item) {
yield $item;
}
}
private function getTraversableThatThrows($items)
{
foreach ($items as $item) {
if ($item instanceof Exception) {
throw $item;
} else {
yield $item;
}
}
}
}
...@@ -20,8 +20,9 @@ class ListCollectionsFunctionalTest extends FunctionalTestCase ...@@ -20,8 +20,9 @@ class ListCollectionsFunctionalTest extends FunctionalTestCase
$this->assertEquals(1, $writeResult->getInsertedCount()); $this->assertEquals(1, $writeResult->getInsertedCount());
$operation = new ListCollections($this->getDatabaseName(), ['filter' => ['name' => $this->getCollectionName()]]); $operation = new ListCollections($this->getDatabaseName(), ['filter' => ['name' => $this->getCollectionName()]]);
// Convert the CollectionInfoIterator to an array since we cannot rewind its cursor $collections = $operation->execute($server);
$collections = iterator_to_array($operation->execute($server));
$this->assertInstanceOf('MongoDB\Model\CollectionInfoIterator', $collections);
$this->assertCount(1, $collections); $this->assertCount(1, $collections);
......
...@@ -22,9 +22,6 @@ class ListIndexesFunctionalTest extends FunctionalTestCase ...@@ -22,9 +22,6 @@ class ListIndexesFunctionalTest extends FunctionalTestCase
$this->assertInstanceOf('MongoDB\Model\IndexInfoIterator', $indexes); $this->assertInstanceOf('MongoDB\Model\IndexInfoIterator', $indexes);
// Convert the CursorInfoIterator to an array since we cannot rewind its cursor
$indexes = iterator_to_array($indexes);
$this->assertCount(1, $indexes); $this->assertCount(1, $indexes);
foreach ($indexes as $index) { foreach ($indexes as $index) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment