Commit 489d8698 authored by Katherine Walker's avatar Katherine Walker

PHPLIB-262: Implement iterator for BSON files

parent df6df1c5
<?php
/*
* Copyright 2018 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 MongoDB\Exception\UnexpectedValueException;
use MongoDB\Model\BSONDocument;
use Iterator;
/**
* Iterator for BSON documents.
*/
class BSONIterator implements Iterator
{
private $buffer;
private $bufferLength;
private $current;
private $key = 0;
private $position = 0;
private $options;
const BSON_SIZE = 4;
/**
* Constructs a BSON Iterator.
*
* Supported options:
*
* * typeMap (array): Type map for BSON deserialization.
*
* @internal
* @see http://php.net/manual/en/function.mongodb.bson-tophp.php
* @param string $data Concatenated, valid, BSON-encoded documents
* @param array $options Iterator options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function __construct($data, array $options = [])
{
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
}
if ( ! isset($options['typeMap'])) {
$options['typeMap'] = [];
}
$this->buffer = $data;
$this->bufferLength = strlen($data);
$this->options = $options;
}
/**
* @see http://php.net/iterator.current
* @return mixed
*/
public function current()
{
return $this->current;
}
/**
* @see http://php.net/iterator.key
* @return mixed
*/
public function key()
{
return $this->key;
}
/**
* @see http://php.net/iterator.next
* @return void
*/
public function next()
{
$this->key++;
$this->current = null;
$this->advance();
}
/**
* @see http://php.net/iterator.rewind
* @return void
*/
public function rewind()
{
$this->key = 0;
$this->position = 0;
$this->current = null;
$this->advance();
}
/**
* @see http://php.net/iterator.valid
* @return boolean
*/
public function valid()
{
return $this->current !== null;
}
private function advance()
{
if ($this->position === $this->bufferLength) {
return;
}
if (($this->bufferLength - $this->position) < self::BSON_SIZE) {
throw new UnexpectedValueException(sprintf('Expected at least %d bytes; %d remaining', self::BSON_SIZE, $this->bufferLength - $this->position));
}
list(,$documentLength) = unpack('V', substr($this->buffer, $this->position, self::BSON_SIZE));
if (($this->bufferLength - $this->position) < $documentLength) {
throw new UnexpectedValueException(sprintf('Expected %d bytes; %d remaining', $documentLength, $this->bufferLength - $this->position));
}
$this->current = \MongoDB\BSON\toPHP(substr($this->buffer, $this->position, $documentLength), $this->options['typeMap']);
$this->position += $documentLength;
}
}
<?php
namespace MongoDB\Tests\Model;
use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use MongoDB\Model\BSONIterator;
use MongoDB\Tests\TestCase;
use stdClass;
class BSONIteratorTest extends TestCase
{
/**
* @dataProvider provideTypeMapOptionsAndExpectedDocuments
*/
public function testValidValues(array $typeMap = null, $binaryString, array $expectedDocuments)
{
$bsonIt = new BSONIterator($binaryString, ['typeMap' => $typeMap]);
$results = iterator_to_array($bsonIt);
$this->assertEquals($expectedDocuments, $results);
}
public function provideTypeMapOptionsAndExpectedDocuments()
{
return [
[
null,
implode(array_map(
'MongoDB\BSON\fromPHP',
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
]
)),
[
(object) ['_id' => 1, 'x' => (object) ['foo' => 'bar']],
(object) ['_id' => 3, 'x' => (object) ['foo' => 'bar']],
]
],
[
['root' => 'array', 'document' => 'array'],
implode(array_map(
'MongoDB\BSON\fromPHP',
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
]
)),
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
]
],
[
['root' => 'object', 'document' => 'array'],
implode(array_map(
'MongoDB\BSON\fromPHP',
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
]
)),
[
(object) ['_id' => 1, 'x' => ['foo' => 'bar']],
(object) ['_id' => 3, 'x' => ['foo' => 'bar']],
]
],
[
['root' => 'array', 'document' => 'stdClass'],
implode(array_map(
'MongoDB\BSON\fromPHP',
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
]
)),
[
['_id' => 1, 'x' => (object) ['foo' => 'bar']],
['_id' => 3, 'x' => (object) ['foo' => 'bar']],
]
],
];
}
public function testCannotReadLengthFromFirstDocument()
{
$binaryString = substr(\MongoDB\BSON\fromPHP([]), 0, 3);
$bsonIt = new BSONIterator($binaryString);
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Expected at least 4 bytes; 3 remaining');
$bsonIt->rewind();
}
public function testCannotReadLengthFromSubsequentDocument()
{
$binaryString = \MongoDB\BSON\fromPHP([]) . substr(\MongoDB\BSON\fromPHP([]), 0, 3);
$bsonIt = new BSONIterator($binaryString);
$bsonIt->rewind();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Expected at least 4 bytes; 3 remaining');
$bsonIt->next();
}
public function testCannotReadFirstDocument()
{
$binaryString = substr(\MongoDB\BSON\fromPHP([]), 0, 4);
$bsonIt = new BSONIterator($binaryString);
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Expected 5 bytes; 4 remaining');
$bsonIt->rewind();
}
public function testCannotReadSecondDocument()
{
$binaryString = \MongoDB\BSON\fromPHP([]) . substr(\MongoDB\BSON\fromPHP([]), 0, 4);
$bsonIt = new BSONIterator($binaryString);
$bsonIt->rewind();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Expected 5 bytes; 4 remaining');
$bsonIt->next();
}
}
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