Unverified Commit 703401d0 authored by Andreas Braun's avatar Andreas Braun

Merge pull request #658

parents 5e503d0f 502bb853
...@@ -19,4 +19,10 @@ source: ...@@ -19,4 +19,10 @@ source:
ref: session ref: session
post: | post: |
.. versionadded:: 1.3 .. versionadded:: 1.3
---
source:
file: apiargs-MongoDBCollection-common-option.yaml
ref: typeMap
post: |
.. versionadded:: 1.5
... ...
<?php
/*
* Copyright 2016-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 ArrayIterator;
use MongoDB\Exception\BadMethodCallException;
/**
* Iterator for applying a type map to documents in inline command results.
*
* @internal
*/
class TypeMapArrayIterator extends ArrayIterator
{
private $typeMap;
/**
* Constructor.
*
* @param array $documents
* @param array $typeMap
*/
public function __construct(array $documents = [], array $typeMap)
{
parent::__construct($documents);
$this->typeMap = $typeMap;
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.append
* @param mixed $value
* @throws BadMethodCallException
*/
public function append($value)
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.asort
* @throws BadMethodCallException
*/
public function asort()
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Return the current element with the type map applied to it.
*
* @see http://php.net/arrayiterator.current
* @return array|object
*/
public function current()
{
return \MongoDB\apply_type_map_to_document(parent::current(), $this->typeMap);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.ksort
* @throws BadMethodCallException
*/
public function ksort()
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.natcasesort
* @throws BadMethodCallException
*/
public function natcasesort()
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.natsort
* @throws BadMethodCallException
*/
public function natsort()
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Return the value from the provided offset with the type map applied.
*
* @see http://php.net/arrayiterator.offsetget
* @param mixed $offset
* @return array|object
*/
public function offsetGet($offset)
{
return \MongoDB\apply_type_map_to_document(parent::offsetGet($offset), $this->typeMap);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.offsetset
* @param mixed $index
* @param mixed $value
* @throws BadMethodCallException
*/
public function offsetSet($index, $value)
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.offsetunset
* @param mixed $index
* @throws BadMethodCallException
*/
public function offsetUnset($index)
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.uasort
* @param callable $cmp_function
* @throws BadMethodCallException
*/
public function uasort($cmp_function)
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
/**
* Not supported.
*
* @see http://php.net/arrayiterator.uksort
* @param callable $cmp_function
* @throws BadMethodCallException
*/
public function uksort($cmp_function)
{
throw BadMethodCallException::classIsImmutable(__CLASS__);
}
}
...@@ -27,7 +27,6 @@ use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; ...@@ -27,7 +27,6 @@ use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException; use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Exception\UnsupportedException; use MongoDB\Exception\UnsupportedException;
use MongoDB\Model\TypeMapArrayIterator;
use ArrayIterator; use ArrayIterator;
use stdClass; use stdClass;
use Traversable; use Traversable;
...@@ -280,16 +279,16 @@ class Aggregate implements Executable ...@@ -280,16 +279,16 @@ class Aggregate implements Executable
return $cursor; return $cursor;
} }
if (isset($this->options['typeMap'])) {
$cursor->setTypeMap(\MongoDB\create_field_path_type_map($this->options['typeMap'], 'result.$'));
}
$result = current($cursor->toArray()); $result = current($cursor->toArray());
if ( ! isset($result->result) || ! is_array($result->result)) { if ( ! isset($result->result) || ! is_array($result->result)) {
throw new UnexpectedValueException('aggregate command did not return a "result" array'); throw new UnexpectedValueException('aggregate command did not return a "result" array');
} }
if (isset($this->options['typeMap'])) {
return new TypeMapArrayIterator($result->result, $this->options['typeMap']);
}
return new ArrayIterator($result->result); return new ArrayIterator($result->result);
} }
......
...@@ -69,6 +69,8 @@ class Distinct implements Executable, Explainable ...@@ -69,6 +69,8 @@ class Distinct implements Executable, Explainable
* *
* Sessions are not supported for server versions < 3.6. * Sessions are not supported for server versions < 3.6.
* *
* * typeMap (array): Type map for BSON deserialization.
*
* @param string $databaseName Database name * @param string $databaseName Database name
* @param string $collectionName Collection name * @param string $collectionName Collection name
* @param string $fieldName Field for which to return distinct values * @param string $fieldName Field for which to return distinct values
...@@ -102,6 +104,10 @@ class Distinct implements Executable, Explainable ...@@ -102,6 +104,10 @@ class Distinct implements Executable, Explainable
throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
} }
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
}
if (isset($options['readConcern']) && $options['readConcern']->isDefault()) { if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
unset($options['readConcern']); unset($options['readConcern']);
} }
...@@ -139,6 +145,11 @@ class Distinct implements Executable, Explainable ...@@ -139,6 +145,11 @@ class Distinct implements Executable, Explainable
} }
$cursor = $server->executeReadCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions()); $cursor = $server->executeReadCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions());
if (isset($this->options['typeMap'])) {
$cursor->setTypeMap(\MongoDB\create_field_path_type_map($this->options['typeMap'], 'values.$'));
}
$result = current($cursor->toArray()); $result = current($cursor->toArray());
if ( ! isset($result->values) || ! is_array($result->values)) { if ( ! isset($result->values) || ! is_array($result->values)) {
......
...@@ -216,21 +216,14 @@ class FindAndModify implements Executable, Explainable ...@@ -216,21 +216,14 @@ class FindAndModify implements Executable, Explainable
} }
$cursor = $server->executeWriteCommand($this->databaseName, new Command($this->createCommandDocument($server)), $this->createOptions()); $cursor = $server->executeWriteCommand($this->databaseName, new Command($this->createCommandDocument($server)), $this->createOptions());
$result = current($cursor->toArray());
if ( ! isset($result->value)) {
return null;
}
if ( ! is_object($result->value)) {
throw new UnexpectedValueException('findAndModify command did not return a "value" document');
}
if (isset($this->options['typeMap'])) { if (isset($this->options['typeMap'])) {
return \MongoDB\apply_type_map_to_document($result->value, $this->options['typeMap']); $cursor->setTypeMap(\MongoDB\create_field_path_type_map($this->options['typeMap'], 'value'));
} }
return $result->value; $result = current($cursor->toArray());
return isset($result->value) ? $result->value : null;
} }
public function getCommandDocument(Server $server) public function getCommandDocument(Server $server)
......
...@@ -28,7 +28,6 @@ use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; ...@@ -28,7 +28,6 @@ use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException; use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Exception\UnsupportedException; use MongoDB\Exception\UnsupportedException;
use MongoDB\Model\TypeMapArrayIterator;
use MongoDB\MapReduceResult; use MongoDB\MapReduceResult;
use ArrayIterator; use ArrayIterator;
use stdClass; use stdClass;
...@@ -267,6 +266,10 @@ class MapReduce implements Executable ...@@ -267,6 +266,10 @@ class MapReduce implements Executable
? $server->executeReadWriteCommand($this->databaseName, $command, $options) ? $server->executeReadWriteCommand($this->databaseName, $command, $options)
: $server->executeReadCommand($this->databaseName, $command, $options); : $server->executeReadCommand($this->databaseName, $command, $options);
if (isset($this->options['typeMap']) && ! $hasOutputCollection) {
$cursor->setTypeMap(\MongoDB\create_field_path_type_map($this->options['typeMap'], 'results.$'));
}
$result = current($cursor->toArray()); $result = current($cursor->toArray());
$getIterator = $this->createGetIteratorCallable($result, $server); $getIterator = $this->createGetIteratorCallable($result, $server);
...@@ -326,10 +329,6 @@ class MapReduce implements Executable ...@@ -326,10 +329,6 @@ class MapReduce implements Executable
$results = $result->results; $results = $result->results;
return function() use ($results) { return function() use ($results) {
if (isset($this->options['typeMap'])) {
return new TypeMapArrayIterator($results, $this->options['typeMap']);
}
return new ArrayIterator($results); return new ArrayIterator($results);
}; };
} }
......
...@@ -241,3 +241,46 @@ function recursive_copy($element) { ...@@ -241,3 +241,46 @@ function recursive_copy($element) {
return clone $element; return clone $element;
} }
/**
* Creates a type map to apply to a field type
*
* This is used in the Aggregate, Distinct, and FindAndModify operations to
* apply the root-level type map to the document that will be returned. It also
* replaces the root type with object for consistency within these operations
*
* An existing type map for the given field path will not be overwritten
*
* @internal
* @param array $typeMap The existing typeMap
* @param string $fieldPath The field path to apply the root type to
* @return array
*/
function create_field_path_type_map(array $typeMap, $fieldPath)
{
// If some field paths already exist, we prefix them with the field path we are assuming as the new root
if (isset($typeMap['fieldPaths']) && is_array($typeMap['fieldPaths'])) {
$fieldPaths = $typeMap['fieldPaths'];
$typeMap['fieldPaths'] = [];
foreach ($fieldPaths as $existingFieldPath => $type) {
$typeMap['fieldPaths'][$fieldPath . '.' . $existingFieldPath] = $type;
}
}
// If a root typemap was set, apply this to the field object
if (isset($typeMap['root'])) {
$typeMap['fieldPaths'][$fieldPath] = $typeMap['root'];
}
/* Special case if we want to convert an array, in which case we need to
* ensure that the field containing the array is exposed as an array,
* instead of the type given in the type map's array key. */
if (substr($fieldPath, -2, 2) === '.$') {
$typeMap['fieldPaths'][substr($fieldPath, 0, -2)] = 'array';
}
$typeMap['root'] = 'object';
return $typeMap;
}
...@@ -58,6 +58,40 @@ class FunctionsTest extends TestCase ...@@ -58,6 +58,40 @@ class FunctionsTest extends TestCase
'z' => new BSONArray([1, 2, 3]), 'z' => new BSONArray([1, 2, 3]),
]), ]),
], ],
[
[
'x' => 1,
'random' => [
'foo' => 'bar',
],
'value' => [
'bar' => 'baz',
'embedded' => [
'foo' => 'bar',
],
],
],
[
'root' => 'array',
'document' => 'stdClass',
'array' => 'array',
'fieldPaths' => [
'value' => 'array',
],
],
[
'x' => 1,
'random' => (object) [
'foo' => 'bar',
],
'value' => [
'bar' => 'baz',
'embedded' => (object) [
'foo' => 'bar',
],
],
],
]
]; ];
} }
...@@ -135,4 +169,70 @@ class FunctionsTest extends TestCase ...@@ -135,4 +169,70 @@ class FunctionsTest extends TestCase
[ ['replace' => 'collectionName'], false ], [ ['replace' => 'collectionName'], false ],
]; ];
} }
/**
* @dataProvider provideTypeMapValues
*/
public function testCreateFieldPathTypeMap(array $expected, array $typeMap, $fieldPath = 'field')
{
$this->assertEquals($expected, \MongoDB\create_field_path_type_map($typeMap, $fieldPath));
}
public function provideTypeMapValues()
{
return [
'No root type' => [
['document' => 'array', 'root' => 'object'],
['document' => 'array'],
],
'No field path' => [
['root' => 'object', 'fieldPaths' => ['field' => 'array']],
['root' => 'array'],
],
'Field path exists' => [
['root' => 'object', 'fieldPaths' => ['field' => 'array', 'field.field' => 'object']],
['root' => 'array', 'fieldPaths' => ['field' => 'object']],
],
'Nested field path' => [
['root' => 'object', 'fieldPaths' => ['field' => 'object', 'field.nested' => 'array']],
['root' => 'object', 'fieldPaths' => ['nested' => 'array']],
],
'Array field path converted to array' => [
[
'root' => 'object',
'array' => 'MongoDB\Model\BSONArray',
'fieldPaths' => [
'field' => 'array',
'field.$' => 'object',
'field.$.nested' => 'array',
]
],
[
'root' => 'object',
'array' => 'MongoDB\Model\BSONArray',
'fieldPaths' => [
'nested' => 'array',
]
],
'field.$',
],
'Array field path without root key' => [
[
'root' => 'object',
'array' => 'MongoDB\Model\BSONArray',
'fieldPaths' => [
'field' => 'array',
'field.$.nested' => 'array',
]
],
[
'array' => 'MongoDB\Model\BSONArray',
'fieldPaths' => [
'nested' => 'array',
]
],
'field.$',
],
];
}
} }
<?php
namespace MongoDB\Tests\Model;
use MongoDB\Exception\BadMethodCallException;
use MongoDB\Model\TypeMapArrayIterator;
use MongoDB\Tests\TestCase;
class TypeMapArrayIteratorTest extends TestCase
{
public function testCurrentAppliesTypeMap()
{
$document = [
'array' => [1, 2, 3],
'object' => ['foo' => 'bar'],
];
$typeMap = [
'root' => 'object',
'document' => 'object',
'array' => 'array',
];
$iterator = new TypeMapArrayIterator([$document], $typeMap);
$expectedDocument = (object) [
'array' => [1, 2, 3],
'object' => (object) ['foo' => 'bar'],
];
$iterator->rewind();
$this->assertEquals($expectedDocument, $iterator->current());
}
public function testOffsetGetAppliesTypeMap()
{
$document = [
'array' => [1, 2, 3],
'object' => ['foo' => 'bar'],
];
$typeMap = [
'root' => 'object',
'document' => 'object',
'array' => 'array',
];
$iterator = new TypeMapArrayIterator([$document], $typeMap);
$expectedDocument = (object) [
'array' => [1, 2, 3],
'object' => (object) ['foo' => 'bar'],
];
$iterator->rewind();
$this->assertEquals($expectedDocument, $iterator->offsetGet(0));
}
/**
* @dataProvider provideMutateMethods
*/
public function testMutateMethodsCannotBeCalled($method, $args)
{
$document = [
'array' => [1, 2, 3],
'object' => ['foo' => 'bar'],
];
$typeMap = [
'root' => 'object',
'document' => 'object',
'array' => 'array',
];
$iterator = new TypeMapArrayIterator([$document], $typeMap);
$iterator->rewind();
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage(TypeMapArrayIterator::class . ' is immutable');
call_user_func_array([$iterator, $method], $args);
}
public function provideMutateMethods()
{
return [
['append', [['x' => 1]]],
['asort', []],
['ksort', []],
['natcasesort', []],
['natsort', []],
['offsetSet', [0, ['x' => 1]]],
['offsetUnset', [0]],
['uasort', [function($a, $b) { return 0; }]],
['uksort', [function($a, $b) { return 0; }]],
];
}
}
...@@ -305,6 +305,13 @@ class AggregateFunctionalTest extends FunctionalTestCase ...@@ -305,6 +305,13 @@ class AggregateFunctionalTest extends FunctionalTestCase
['_id' => 3, 'x' => (object) ['foo' => 'bar']], ['_id' => 3, 'x' => (object) ['foo' => 'bar']],
], ],
], ],
[
['root' => 'array', 'document' => 'stdClass', 'fieldPaths' => ['x' => 'array']],
[
['_id' => 1, 'x' => ['foo' => 'bar']],
['_id' => 3, 'x' => ['foo' => 'bar']],
],
],
]; ];
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace MongoDB\Tests\Operation; namespace MongoDB\Tests\Operation;
use MongoDB\Driver\BulkWrite;
use MongoDB\Operation\Distinct; use MongoDB\Operation\Distinct;
use MongoDB\Tests\CommandObserver; use MongoDB\Tests\CommandObserver;
use stdClass; use stdClass;
...@@ -51,4 +52,89 @@ class DistinctFunctionalTest extends FunctionalTestCase ...@@ -51,4 +52,89 @@ class DistinctFunctionalTest extends FunctionalTestCase
} }
); );
} }
/**
* @dataProvider provideTypeMapOptionsAndExpectedDocuments
*/
public function testTypeMapOption(array $typeMap, array $expectedDocuments)
{
$bulkWrite = new BulkWrite(['ordered' => true]);
$bulkWrite->insert([
'x' => (object) ['foo' => 'bar'],
]);
$bulkWrite->insert([
'x' => 4,
]);
$bulkWrite->insert([
'x' => (object) ['foo' => ['foo' => 'bar']],
]);
$this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
$distinct = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', [], ['typeMap' => $typeMap]);
$values = $distinct->execute($this->getPrimaryServer());
/* This sort callable sorts all scalars to the front of the list. All
* non-scalar values are sorted by running json_encode on them and
* comparing their string representations.
*/
$sort = function ($a, $b) {
if (is_scalar($a) && ! is_scalar($b)) {
return -1;
}
if (! is_scalar($a)) {
if (is_scalar($b)) {
return 1;
}
$a = json_encode($a);
$b = json_encode($b);
}
return $a < $b ? -1 : 1;
};
usort($expectedDocuments, $sort);
usort($values, $sort);
$this->assertEquals($expectedDocuments, $values);
}
public function provideTypeMapOptionsAndExpectedDocuments()
{
return [
'No type map' => [
['root' => 'array', 'document' => 'array'],
[
['foo' => 'bar'],
4,
['foo' => ['foo' => 'bar']],
],
],
'array/array' => [
['root' => 'array', 'document' => 'array'],
[
['foo' => 'bar'],
4,
['foo' => ['foo' => 'bar']],
],
],
'object/array' => [
['root' => 'object', 'document' => 'array'],
[
(object) ['foo' => 'bar'],
4,
(object) ['foo' => ['foo' => 'bar']],
],
],
'array/stdClass' => [
['root' => 'array', 'document' => 'stdClass'],
[
['foo' => 'bar'],
4,
['foo' => (object) ['foo' => 'bar']],
],
],
];
}
} }
...@@ -49,6 +49,10 @@ class DistinctTest extends TestCase ...@@ -49,6 +49,10 @@ class DistinctTest extends TestCase
$options[][] = ['session' => $value]; $options[][] = ['session' => $value];
} }
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['typeMap' => $value];
}
return $options; return $options;
} }
} }
...@@ -164,6 +164,10 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase ...@@ -164,6 +164,10 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase
['root' => BSONDocument::class, 'document' => 'object'], ['root' => BSONDocument::class, 'document' => 'object'],
new BSONDocument(['_id' => 1, 'x' => (object) ['foo' => 'bar']]), new BSONDocument(['_id' => 1, 'x' => (object) ['foo' => 'bar']]),
], ],
[
['root' => 'array', 'document' => 'stdClass', 'fieldPaths' => ['x' => 'array']],
['_id' => 1, 'x' => ['foo' => 'bar']],
],
]; ];
} }
......
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