Skip to content

Commit 2c689a2

Browse files
committed
PHPLIB-138: Support typeMap option for aggregate and find operations
1 parent 330e1e3 commit 2c689a2

File tree

9 files changed

+225
-34
lines changed

9 files changed

+225
-34
lines changed

src/Collection.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ public function __toString()
145145
* returned; otherwise, an ArrayIterator is returned, which wraps the
146146
* "result" array from the command response document.
147147
*
148+
* Note: BSON deserialization of inline aggregation results (i.e. not using
149+
* a command cursor) does not yet support a custom type map
150+
* (depends on: https://jira.mongodb.org/browse/PHPC-314).
151+
*
148152
* @see Aggregate::__construct() for supported options
149153
* @param array $pipeline List of pipeline operations
150154
* @param array $options Command options
@@ -169,6 +173,10 @@ public function aggregate(array $pipeline, array $options = [])
169173
$options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
170174
}
171175

176+
if ( ! isset($options['typeMap'])) {
177+
$options['typeMap'] = $this->typeMap;
178+
}
179+
172180
$operation = new Aggregate($this->databaseName, $this->collectionName, $pipeline, $options);
173181
$server = $this->manager->selectServer($options['readPreference']);
174182

@@ -397,6 +405,10 @@ public function find($filter = [], array $options = [])
397405
$options['readPreference'] = $this->readPreference;
398406
}
399407

408+
if ( ! isset($options['typeMap'])) {
409+
$options['typeMap'] = $this->typeMap;
410+
}
411+
400412
$operation = new Find($this->databaseName, $this->collectionName, $filter, $options);
401413
$server = $this->manager->selectServer($options['readPreference']);
402414

@@ -422,6 +434,10 @@ public function findOne($filter = [], array $options = [])
422434
$options['readPreference'] = $this->readPreference;
423435
}
424436

437+
if ( ! isset($options['typeMap'])) {
438+
$options['typeMap'] = $this->typeMap;
439+
}
440+
425441
$operation = new FindOne($this->databaseName, $this->collectionName, $filter, $options);
426442
$server = $this->manager->selectServer($options['readPreference']);
427443

@@ -433,6 +449,9 @@ public function findOne($filter = [], array $options = [])
433449
*
434450
* The document to return may be null.
435451
*
452+
* Note: BSON deserialization of the returned document does not yet support
453+
* a custom type map (depends on: https://jira.mongodb.org/browse/PHPC-314).
454+
*
436455
* @see FindOneAndDelete::__construct() for supported options
437456
* @see http://docs.mongodb.org/manual/reference/command/findAndModify/
438457
* @param array|object $filter Query by which to filter documents
@@ -460,6 +479,9 @@ public function findOneAndDelete($filter, array $options = [])
460479
* returned. Specify FindOneAndReplace::RETURN_DOCUMENT_AFTER for the
461480
* "returnDocument" option to return the updated document.
462481
*
482+
* Note: BSON deserialization of the returned document does not yet support
483+
* a custom type map (depends on: https://jira.mongodb.org/browse/PHPC-314).
484+
*
463485
* @see FindOneAndReplace::__construct() for supported options
464486
* @see http://docs.mongodb.org/manual/reference/command/findAndModify/
465487
* @param array|object $filter Query by which to filter documents
@@ -488,6 +510,9 @@ public function findOneAndReplace($filter, $replacement, array $options = [])
488510
* returned. Specify FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the
489511
* "returnDocument" option to return the updated document.
490512
*
513+
* Note: BSON deserialization of the returned document does not yet support
514+
* a custom type map (depends on: https://jira.mongodb.org/browse/PHPC-314).
515+
*
491516
* @see FindOneAndReplace::__construct() for supported options
492517
* @see http://docs.mongodb.org/manual/reference/command/findAndModify/
493518
* @param array|object $filter Query by which to filter documents

src/Operation/Aggregate.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class Aggregate implements Executable
6060
*
6161
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
6262
*
63+
* * typeMap (array): Type map for BSON deserialization. This will be
64+
* applied to the returned Cursor (it is not sent to the server).
65+
*
66+
* This is not supported for inline aggregation results (i.e. useCursor
67+
* option is false or the server versions < 2.6).
68+
*
6369
* * useCursor (boolean): Indicates whether the command will request that
6470
* the server provide results using a cursor. The default is true.
6571
*
@@ -124,6 +130,10 @@ public function __construct($databaseName, $collectionName, array $pipeline, arr
124130
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
125131
}
126132

133+
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
134+
throw new InvalidArgumentTypeException('"typeMap" option', $options['typeMap'], 'array');
135+
}
136+
127137
if ( ! is_bool($options['useCursor'])) {
128138
throw new InvalidArgumentTypeException('"useCursor" option', $options['useCursor'], 'boolean');
129139
}
@@ -132,6 +142,10 @@ public function __construct($databaseName, $collectionName, array $pipeline, arr
132142
throw new InvalidArgumentException('"batchSize" option should not be used if "useCursor" is false');
133143
}
134144

145+
if (isset($options['typeMap']) && ! $options['useCursor']) {
146+
throw new InvalidArgumentException('"typeMap" option should not be used if "useCursor" is false');
147+
}
148+
135149
$this->databaseName = (string) $databaseName;
136150
$this->collectionName = (string) $collectionName;
137151
$this->pipeline = $pipeline;
@@ -154,6 +168,13 @@ public function execute(Server $server)
154168
$cursor = $server->executeCommand($this->databaseName, $command, $readPreference);
155169

156170
if ($isCursorSupported && $this->options['useCursor']) {
171+
/* The type map can only be applied to command cursors until
172+
* https://jira.mongodb.org/browse/PHPC-314 is implemented.
173+
*/
174+
if (isset($this->options['typeMap'])) {
175+
$cursor->setTypeMap($this->options['typeMap']);
176+
}
177+
157178
return $cursor;
158179
}
159180

src/Operation/Find.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class Find implements Executable
7979
* "$orderby" also exists in the modifiers document, this option will
8080
* take precedence.
8181
*
82+
* * typeMap (array): Type map for BSON deserialization. This will be
83+
* applied to the returned Cursor (it is not sent to the server).
84+
*
8285
* @param string $databaseName Database name
8386
* @param string $collectionName Collection name
8487
* @param array|object $filter Query by which to filter documents
@@ -155,6 +158,10 @@ public function __construct($databaseName, $collectionName, $filter, array $opti
155158
throw new InvalidArgumentTypeException('"sort" option', $options['sort'], 'array or object');
156159
}
157160

161+
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
162+
throw new InvalidArgumentTypeException('"typeMap" option', $options['typeMap'], 'array');
163+
}
164+
158165
$this->databaseName = (string) $databaseName;
159166
$this->collectionName = (string) $collectionName;
160167
$this->filter = $filter;
@@ -172,7 +179,13 @@ public function execute(Server $server)
172179
{
173180
$readPreference = isset($this->options['readPreference']) ? $this->options['readPreference'] : null;
174181

175-
return $server->executeQuery($this->databaseName . '.' . $this->collectionName, $this->createQuery(), $readPreference);
182+
$cursor = $server->executeQuery($this->databaseName . '.' . $this->collectionName, $this->createQuery(), $readPreference);
183+
184+
if (isset($this->options['typeMap'])) {
185+
$cursor->setTypeMap($this->options['typeMap']);
186+
}
187+
188+
return $cursor;
176189
}
177190

178191
/**

src/Operation/FindOne.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ class FindOne implements Executable
5959
*/
6060
public function __construct($databaseName, $collectionName, $filter, array $options = [])
6161
{
62-
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
63-
throw new InvalidArgumentTypeException('"typeMap" option', $options['typeMap'], 'array');
64-
}
65-
6662
$this->find = new Find(
6763
$databaseName,
6864
$collectionName,
@@ -83,11 +79,6 @@ public function __construct($databaseName, $collectionName, $filter, array $opti
8379
public function execute(Server $server)
8480
{
8581
$cursor = $this->find->execute($server);
86-
87-
if (isset($this->options['typeMap'])) {
88-
$cursor->setTypeMap($this->options['typeMap']);
89-
}
90-
9182
$document = current($cursor->toArray());
9283

9384
return ($document === false) ? null : $document;

tests/Operation/AggregateFunctionalTest.php

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,85 @@
22

33
namespace MongoDB\Tests\Operation;
44

5+
use MongoDB\Driver\BulkWrite;
56
use MongoDB\Operation\Aggregate;
67

78
class AggregateFunctionalTest extends FunctionalTestCase
89
{
10+
private static $wireVersionForCursor = 2;
11+
912
/**
1013
* @expectedException MongoDB\Driver\Exception\RuntimeException
1114
*/
1215
public function testUnrecognizedPipelineState()
1316
{
14-
$server = $this->getPrimaryServer();
1517
$operation = new Aggregate($this->getDatabaseName(), $this->getCollectionName(), [['$foo' => 1]]);
16-
$operation->execute($server);
18+
$operation->execute($this->getPrimaryServer());
19+
}
20+
21+
/**
22+
* @dataProvider provideTypeMapOptionsAndExpectedDocument
23+
*/
24+
public function testTypeMapOption(array $typeMap, array $expectedDocuments)
25+
{
26+
if ( ! \MongoDB\server_supports_feature($this->getPrimaryServer(), self::$wireVersionForCursor)) {
27+
$this->markTestSkipped('Command cursor is not supported');
28+
}
29+
30+
$this->createFixtures(3);
31+
32+
$pipeline = [['$match' => ['_id' => ['$ne' => 2]]]];
33+
$operation = new Aggregate($this->getDatabaseName(), $this->getCollectionName(), $pipeline, ['typeMap' => $typeMap]);
34+
$cursor = $operation->execute($this->getPrimaryServer());
35+
36+
$this->assertEquals($expectedDocuments, $cursor->toArray());
37+
}
38+
39+
public function provideTypeMapOptionsAndExpectedDocument()
40+
{
41+
return [
42+
[
43+
['root' => 'array', 'document' => 'array'],
44+
[
45+
['_id' => 1, 'x' => ['foo' => 'bar']],
46+
['_id' => 3, 'x' => ['foo' => 'bar']],
47+
],
48+
],
49+
[
50+
['root' => 'object', 'document' => 'array'],
51+
[
52+
(object) ['_id' => 1, 'x' => ['foo' => 'bar']],
53+
(object) ['_id' => 3, 'x' => ['foo' => 'bar']],
54+
],
55+
],
56+
[
57+
['root' => 'array', 'document' => 'stdClass'],
58+
[
59+
['_id' => 1, 'x' => (object) ['foo' => 'bar']],
60+
['_id' => 3, 'x' => (object) ['foo' => 'bar']],
61+
],
62+
],
63+
];
64+
}
65+
66+
/**
67+
* Create data fixtures.
68+
*
69+
* @param integer $n
70+
*/
71+
private function createFixtures($n)
72+
{
73+
$bulkWrite = new BulkWrite(['ordered' => true]);
74+
75+
for ($i = 1; $i <= $n; $i++) {
76+
$bulkWrite->insert([
77+
'_id' => $i,
78+
'x' => (object) ['foo' => 'bar'],
79+
]);
80+
}
81+
82+
$result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
83+
84+
$this->assertEquals($n, $result->getInsertedCount());
1785
}
1886
}

tests/Operation/AggregateTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public function provideInvalidConstructorOptions()
6161
$options[][] = ['readPreference' => $value];
6262
}
6363

64+
foreach ($this->getInvalidArrayValues() as $value) {
65+
$options[][] = ['typeMap' => $value];
66+
}
67+
6468
foreach ($this->getInvalidBooleanValues() as $value) {
6569
$options[][] = ['useCursor' => $value];
6670
}
@@ -81,4 +85,18 @@ public function testConstructorBatchSizeOptionRequiresUseCursor()
8185
['batchSize' => 100, 'useCursor' => false]
8286
);
8387
}
88+
89+
/**
90+
* @expectedException MongoDB\Exception\InvalidArgumentException
91+
* @expectedExceptionMessage "typeMap" option should not be used if "useCursor" is false
92+
*/
93+
public function testConstructorTypeMapOptionRequiresUseCursor()
94+
{
95+
new Aggregate(
96+
$this->getDatabaseName(),
97+
$this->getCollectionName(),
98+
[['$match' => ['x' => 1]]],
99+
['typeMap' => ['root' => 'array'], 'useCursor' => false]
100+
);
101+
}
84102
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Operation;
4+
5+
use MongoDB\Driver\BulkWrite;
6+
use MongoDB\Operation\Find;
7+
8+
class FindFunctionalTest extends FunctionalTestCase
9+
{
10+
/**
11+
* @dataProvider provideTypeMapOptionsAndExpectedDocuments
12+
*/
13+
public function testTypeMapOption(array $typeMap, array $expectedDocuments)
14+
{
15+
$this->createFixtures(3);
16+
17+
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], ['typeMap' => $typeMap]);
18+
$cursor = $operation->execute($this->getPrimaryServer());
19+
20+
$this->assertEquals($expectedDocuments, $cursor->toArray());
21+
}
22+
23+
public function provideTypeMapOptionsAndExpectedDocuments()
24+
{
25+
return [
26+
[
27+
['root' => 'array', 'document' => 'array'],
28+
[
29+
['_id' => 1, 'x' => ['foo' => 'bar']],
30+
['_id' => 2, 'x' => ['foo' => 'bar']],
31+
['_id' => 3, 'x' => ['foo' => 'bar']],
32+
],
33+
],
34+
[
35+
['root' => 'object', 'document' => 'array'],
36+
[
37+
(object) ['_id' => 1, 'x' => ['foo' => 'bar']],
38+
(object) ['_id' => 2, 'x' => ['foo' => 'bar']],
39+
(object) ['_id' => 3, 'x' => ['foo' => 'bar']],
40+
],
41+
],
42+
[
43+
['root' => 'array', 'document' => 'stdClass'],
44+
[
45+
['_id' => 1, 'x' => (object) ['foo' => 'bar']],
46+
['_id' => 2, 'x' => (object) ['foo' => 'bar']],
47+
['_id' => 3, 'x' => (object) ['foo' => 'bar']],
48+
],
49+
],
50+
];
51+
}
52+
53+
/**
54+
* Create data fixtures.
55+
*
56+
* @param integer $n
57+
*/
58+
private function createFixtures($n)
59+
{
60+
$bulkWrite = new BulkWrite(['ordered' => true]);
61+
62+
for ($i = 1; $i <= $n; $i++) {
63+
$bulkWrite->insert([
64+
'_id' => $i,
65+
'x' => (object) ['foo' => 'bar'],
66+
]);
67+
}
68+
69+
$result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
70+
71+
$this->assertEquals($n, $result->getInsertedCount());
72+
}
73+
}

tests/Operation/FindOneTest.php

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)