Skip to content

Commit 489d869

Browse files
committed
PHPLIB-262: Implement iterator for BSON files
1 parent df6df1c commit 489d869

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

src/Model/BSONIterator.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
/*
3+
* Copyright 2018 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 MongoDB\Exception\UnexpectedValueException;
21+
use MongoDB\Model\BSONDocument;
22+
use Iterator;
23+
24+
/**
25+
* Iterator for BSON documents.
26+
*/
27+
class BSONIterator implements Iterator
28+
{
29+
private $buffer;
30+
private $bufferLength;
31+
private $current;
32+
private $key = 0;
33+
private $position = 0;
34+
private $options;
35+
36+
const BSON_SIZE = 4;
37+
38+
/**
39+
* Constructs a BSON Iterator.
40+
*
41+
* Supported options:
42+
*
43+
* * typeMap (array): Type map for BSON deserialization.
44+
*
45+
* @internal
46+
* @see http://php.net/manual/en/function.mongodb.bson-tophp.php
47+
* @param string $data Concatenated, valid, BSON-encoded documents
48+
* @param array $options Iterator options
49+
* @throws InvalidArgumentException for parameter/option parsing errors
50+
*/
51+
public function __construct($data, array $options = [])
52+
{
53+
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
54+
throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
55+
}
56+
57+
if ( ! isset($options['typeMap'])) {
58+
$options['typeMap'] = [];
59+
}
60+
61+
$this->buffer = $data;
62+
$this->bufferLength = strlen($data);
63+
$this->options = $options;
64+
}
65+
66+
/**
67+
* @see http://php.net/iterator.current
68+
* @return mixed
69+
*/
70+
public function current()
71+
{
72+
return $this->current;
73+
}
74+
75+
/**
76+
* @see http://php.net/iterator.key
77+
* @return mixed
78+
*/
79+
public function key()
80+
{
81+
return $this->key;
82+
}
83+
84+
/**
85+
* @see http://php.net/iterator.next
86+
* @return void
87+
*/
88+
public function next()
89+
{
90+
$this->key++;
91+
$this->current = null;
92+
$this->advance();
93+
}
94+
95+
/**
96+
* @see http://php.net/iterator.rewind
97+
* @return void
98+
*/
99+
public function rewind()
100+
{
101+
$this->key = 0;
102+
$this->position = 0;
103+
$this->current = null;
104+
$this->advance();
105+
}
106+
107+
/**
108+
* @see http://php.net/iterator.valid
109+
* @return boolean
110+
*/
111+
public function valid()
112+
{
113+
return $this->current !== null;
114+
}
115+
116+
private function advance()
117+
{
118+
if ($this->position === $this->bufferLength) {
119+
return;
120+
}
121+
122+
if (($this->bufferLength - $this->position) < self::BSON_SIZE) {
123+
throw new UnexpectedValueException(sprintf('Expected at least %d bytes; %d remaining', self::BSON_SIZE, $this->bufferLength - $this->position));
124+
}
125+
126+
list(,$documentLength) = unpack('V', substr($this->buffer, $this->position, self::BSON_SIZE));
127+
128+
if (($this->bufferLength - $this->position) < $documentLength) {
129+
throw new UnexpectedValueException(sprintf('Expected %d bytes; %d remaining', $documentLength, $this->bufferLength - $this->position));
130+
}
131+
132+
$this->current = \MongoDB\BSON\toPHP(substr($this->buffer, $this->position, $documentLength), $this->options['typeMap']);
133+
$this->position += $documentLength;
134+
}
135+
}

tests/Model/BSONIteratorTest.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Model;
4+
5+
use MongoDB\Exception\UnexpectedValueException;
6+
use MongoDB\Model\BSONArray;
7+
use MongoDB\Model\BSONDocument;
8+
use MongoDB\Model\BSONIterator;
9+
use MongoDB\Tests\TestCase;
10+
use stdClass;
11+
12+
class BSONIteratorTest extends TestCase
13+
{
14+
/**
15+
* @dataProvider provideTypeMapOptionsAndExpectedDocuments
16+
*/
17+
public function testValidValues(array $typeMap = null, $binaryString, array $expectedDocuments)
18+
{
19+
$bsonIt = new BSONIterator($binaryString, ['typeMap' => $typeMap]);
20+
21+
$results = iterator_to_array($bsonIt);
22+
23+
$this->assertEquals($expectedDocuments, $results);
24+
}
25+
26+
public function provideTypeMapOptionsAndExpectedDocuments()
27+
{
28+
return [
29+
[
30+
null,
31+
implode(array_map(
32+
'MongoDB\BSON\fromPHP',
33+
[
34+
['_id' => 1, 'x' => ['foo' => 'bar']],
35+
['_id' => 3, 'x' => ['foo' => 'bar']],
36+
]
37+
)),
38+
[
39+
(object) ['_id' => 1, 'x' => (object) ['foo' => 'bar']],
40+
(object) ['_id' => 3, 'x' => (object) ['foo' => 'bar']],
41+
]
42+
],
43+
[
44+
['root' => 'array', 'document' => 'array'],
45+
implode(array_map(
46+
'MongoDB\BSON\fromPHP',
47+
[
48+
['_id' => 1, 'x' => ['foo' => 'bar']],
49+
['_id' => 3, 'x' => ['foo' => 'bar']],
50+
]
51+
)),
52+
[
53+
['_id' => 1, 'x' => ['foo' => 'bar']],
54+
['_id' => 3, 'x' => ['foo' => 'bar']],
55+
]
56+
],
57+
[
58+
['root' => 'object', 'document' => 'array'],
59+
implode(array_map(
60+
'MongoDB\BSON\fromPHP',
61+
[
62+
['_id' => 1, 'x' => ['foo' => 'bar']],
63+
['_id' => 3, 'x' => ['foo' => 'bar']],
64+
]
65+
)),
66+
[
67+
(object) ['_id' => 1, 'x' => ['foo' => 'bar']],
68+
(object) ['_id' => 3, 'x' => ['foo' => 'bar']],
69+
]
70+
],
71+
[
72+
['root' => 'array', 'document' => 'stdClass'],
73+
implode(array_map(
74+
'MongoDB\BSON\fromPHP',
75+
[
76+
['_id' => 1, 'x' => ['foo' => 'bar']],
77+
['_id' => 3, 'x' => ['foo' => 'bar']],
78+
]
79+
)),
80+
[
81+
['_id' => 1, 'x' => (object) ['foo' => 'bar']],
82+
['_id' => 3, 'x' => (object) ['foo' => 'bar']],
83+
]
84+
],
85+
];
86+
}
87+
88+
public function testCannotReadLengthFromFirstDocument()
89+
{
90+
$binaryString = substr(\MongoDB\BSON\fromPHP([]), 0, 3);
91+
92+
$bsonIt = new BSONIterator($binaryString);
93+
94+
$this->expectException(UnexpectedValueException::class);
95+
$this->expectExceptionMessage('Expected at least 4 bytes; 3 remaining');
96+
$bsonIt->rewind();
97+
}
98+
99+
public function testCannotReadLengthFromSubsequentDocument()
100+
{
101+
$binaryString = \MongoDB\BSON\fromPHP([]) . substr(\MongoDB\BSON\fromPHP([]), 0, 3);
102+
103+
$bsonIt = new BSONIterator($binaryString);
104+
$bsonIt->rewind();
105+
106+
$this->expectException(UnexpectedValueException::class);
107+
$this->expectExceptionMessage('Expected at least 4 bytes; 3 remaining');
108+
$bsonIt->next();
109+
}
110+
111+
public function testCannotReadFirstDocument()
112+
{
113+
$binaryString = substr(\MongoDB\BSON\fromPHP([]), 0, 4);
114+
115+
$bsonIt = new BSONIterator($binaryString);
116+
117+
$this->expectException(UnexpectedValueException::class);
118+
$this->expectExceptionMessage('Expected 5 bytes; 4 remaining');
119+
$bsonIt->rewind();
120+
}
121+
122+
public function testCannotReadSecondDocument()
123+
{
124+
$binaryString = \MongoDB\BSON\fromPHP([]) . substr(\MongoDB\BSON\fromPHP([]), 0, 4);
125+
126+
$bsonIt = new BSONIterator($binaryString);
127+
$bsonIt->rewind();
128+
129+
$this->expectException(UnexpectedValueException::class);
130+
$this->expectExceptionMessage('Expected 5 bytes; 4 remaining');
131+
$bsonIt->next();
132+
}
133+
}

0 commit comments

Comments
 (0)