From 9c108d5a5dd82aa26f57b504efe73bcaa2f53131 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 6 May 2015 20:43:51 -0600 Subject: [PATCH 01/24] Split UnexpectedTypeException for logic and runtime errors Invalid arguments and options constitute a logic error, so InvalidArgumentTypeException may be used. For runtime errors (e.g. server returned something we didn't expect), UnexpectedValueTypeException may be used. --- src/Exception/InvalidArgumentTypeException.php | 11 +++++++++++ src/Exception/UnexpectedValueTypeException.php | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/Exception/InvalidArgumentTypeException.php create mode 100644 src/Exception/UnexpectedValueTypeException.php diff --git a/src/Exception/InvalidArgumentTypeException.php b/src/Exception/InvalidArgumentTypeException.php new file mode 100644 index 000000000..9bc580c10 --- /dev/null +++ b/src/Exception/InvalidArgumentTypeException.php @@ -0,0 +1,11 @@ + Date: Sun, 14 Jun 2015 21:58:50 -0400 Subject: [PATCH 02/24] Create functions.php file for utility functions --- composer.json | 3 +- src/functions.php | 76 ++++++++++++++++++++++++++++++++++++++++++ tests/PedantryTest.php | 4 +++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/functions.php diff --git a/composer.json b/composer.json index 2fad9e0b5..71300a921 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "ext-mongodb": ">=0.6.0" }, "autoload": { - "psr-4": { "MongoDB\\": "src/" } + "psr-4": { "MongoDB\\": "src/" }, + "files": [ "src/functions.php" ] }, "extra": { "branch-alias": { diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 000000000..87c246ce9 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,76 @@ + $type) { + $name .= ($name != '' ? '_' : '') . $field . '_' . $type; + } + + return $name; +} + +/** + * Return whether the server supports a particular feature. + * + * @internal + * @param Server $server Server to check + * @param integer $feature Feature constant (i.e. wire protocol version) + * @return boolean + */ +function server_supports_feature(Server $server, $feature) +{ + $info = $server->getInfo(); + $maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0; + $minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0; + + return ($minWireVersion <= $feature && $maxWireVersion >= $feature); +} diff --git a/tests/PedantryTest.php b/tests/PedantryTest.php index 5ee136c62..f0f16361e 100644 --- a/tests/PedantryTest.php +++ b/tests/PedantryTest.php @@ -62,6 +62,10 @@ public function provideProjectClassNames() $files = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($srcDir)), '/\.php$/i'); foreach ($files as $file) { + if ($file->getFilename() === 'functions.php') { + continue; + } + $classNames[][] = 'MongoDB\\' . str_replace(DIRECTORY_SEPARATOR, '\\', substr($file->getRealPath(), strlen($srcDir) + 1, -4)); } From a079417e83b1d6c8a9a1a72aab3cfc2b74268401 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 10 Jun 2015 16:40:13 -0400 Subject: [PATCH 03/24] Executable interface for operations --- src/Operation/Executable.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Operation/Executable.php diff --git a/src/Operation/Executable.php b/src/Operation/Executable.php new file mode 100644 index 000000000..4ef501d4e --- /dev/null +++ b/src/Operation/Executable.php @@ -0,0 +1,21 @@ + Date: Wed, 10 Jun 2015 16:41:32 -0400 Subject: [PATCH 04/24] Extract Collection::aggregate() to an operation class --- src/Collection.php | 86 +-------- src/Operation/Aggregate.php | 171 ++++++++++++++++++ .../CrudSpec/AggregateFunctionalTest.php | 5 +- 3 files changed, 183 insertions(+), 79 deletions(-) create mode 100644 src/Operation/Aggregate.php diff --git a/src/Collection.php b/src/Collection.php index b55d1c5b1..fdc3ad951 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -15,6 +15,8 @@ use MongoDB\Model\IndexInfoIterator; use MongoDB\Model\IndexInfoIteratorIterator; use MongoDB\Model\IndexInput; +use MongoDB\Operation\Aggregate; +use Traversable; class Collection { @@ -78,79 +80,25 @@ public function __toString() } /** - * Runs an aggregation framework pipeline + * Executes an aggregation framework pipeline on the collection. * * Note: this method's return value depends on the MongoDB server version * and the "useCursor" option. If "useCursor" is true, a Cursor will be * returned; otherwise, an ArrayIterator is returned, which wraps the * "result" array from the command response document. * - * @see http://docs.mongodb.org/manual/reference/command/aggregate/ - * - * @param array $pipeline The pipeline to execute - * @param array $options Additional options - * @return Iterator + * @see Aggregate::__construct() for supported options + * @param array $pipeline List of pipeline operations + * @param array $options Command options + * @return Traversable */ public function aggregate(array $pipeline, array $options = array()) { $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); $server = $this->manager->selectServer($readPreference); + $operation = new Aggregate($this->dbname, $this->collname, $pipeline, $options); - if (FeatureDetection::isSupported($server, FeatureDetection::API_AGGREGATE_CURSOR)) { - $options = array_merge( - array( - /** - * Enables writing to temporary files. When set to true, aggregation stages - * can write data to the _tmp subdirectory in the dbPath directory. The - * default is false. - * - * @see http://docs.mongodb.org/manual/reference/command/aggregate/ - */ - 'allowDiskUse' => false, - /** - * The number of documents to return per batch. - * - * @see http://docs.mongodb.org/manual/reference/command/aggregate/ - */ - 'batchSize' => 0, - /** - * The maximum amount of time to allow the query to run. - * - * @see http://docs.mongodb.org/manual/reference/command/aggregate/ - */ - 'maxTimeMS' => 0, - /** - * Indicates if the results should be provided as a cursor. - * - * @see http://docs.mongodb.org/manual/reference/command/aggregate/ - */ - 'useCursor' => true, - ), - $options - ); - } - - $options = $this->_massageAggregateOptions($options); - $command = new Command(array( - 'aggregate' => $this->collname, - 'pipeline' => $pipeline, - ) + $options); - $cursor = $server->executeCommand($this->dbname, $command); - - if ( ! empty($options["cursor"])) { - return $cursor; - } - - $doc = current($cursor->toArray()); - - if ($doc["ok"]) { - return new \ArrayIterator(array_map( - function (\stdClass $document) { return (array) $document; }, - $doc["result"] - )); - } - - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** @@ -1168,22 +1116,6 @@ final protected function _generateCommandException($doc) return new RuntimeException("FIXME: Unknown error"); } - /** - * Internal helper for massaging aggregate options - * @internal - */ - protected function _massageAggregateOptions($options) - { - if ( ! empty($options["useCursor"])) { - $options["cursor"] = isset($options["batchSize"]) - ? array("batchSize" => (integer) $options["batchSize"]) - : new stdClass; - } - unset($options["useCursor"], $options["batchSize"]); - - return $options; - } - /** * Internal helper for massaging findandmodify options * @internal diff --git a/src/Operation/Aggregate.php b/src/Operation/Aggregate.php new file mode 100644 index 000000000..dbc1472c0 --- /dev/null +++ b/src/Operation/Aggregate.php @@ -0,0 +1,171 @@ += 2.6, this option allows users to turn off cursors if + * necessary to aid in mongod/mongos upgrades. + * + * @param string $databaseName Database name + * @param string $collectionName Collection name + * @param array $pipeline List of pipeline operations + * @param array $options Command options + * @throws InvalidArgumentException + */ + public function __construct($databaseName, $collectionName, array $pipeline, array $options = array()) + { + $options += array( + 'allowDiskUse' => false, + 'useCursor' => true, + ); + + if ( ! is_bool($options['allowDiskUse'])) { + throw new InvalidArgumentTypeException('"allowDiskUse" option', $options['allowDiskUse'], 'boolean'); + } + + if (isset($options['batchSize']) && ! is_integer($options['batchSize'])) { + throw new InvalidArgumentTypeException('"batchSize" option', $options['batchSize'], 'integer'); + } + + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { + throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); + } + + if ( ! is_bool($options['useCursor'])) { + throw new InvalidArgumentTypeException('"useCursor" option', $options['useCursor'], 'boolean'); + } + + if (isset($options['batchSize']) && ! $options['useCursor']) { + throw new InvalidArgumentException('"batchSize" option should not be used if "useCursor" is false'); + } + + $expectedIndex = 0; + + foreach ($pipeline as $i => $op) { + if ($i !== $expectedIndex) { + throw new InvalidArgumentException(sprintf('$pipeline is not a list (unexpected index: "%s")', $i)); + } + + if ( ! is_array($op) && ! is_object($op)) { + throw new InvalidArgumentTypeException(sprintf('$pipeline[%d]', $i), $op, 'array or object'); + } + + $expectedIndex += 1; + } + + $this->databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->pipeline = $pipeline; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return Traversable + */ + public function execute(Server $server) + { + $command = $this->createCommand($server); + $cursor = $server->executeCommand($this->databaseName, $command); + + if ($this->options['useCursor']) { + return $cursor; + } + + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + if ( ! isset($result['result']) || ! is_array($result['result'])) { + throw new UnexpectedValueException('aggregate command did not return a "result" array'); + } + + return new ArrayIterator(array_map( + function (stdClass $document) { return (array) $document; }, + $result['result'] + )); + } + + /** + * Create the aggregate command. + * + * @param Server $server + * @return Command + */ + private function createCommand(Server $server) + { + $cmd = array( + 'aggregate' => $this->collectionName, + 'pipeline' => $this->pipeline, + ); + + // Servers < 2.6 do not support any command options + if ( ! \MongoDB\server_supports_feature($server, self::$wireVersionForCursor)) { + return new Command($cmd); + } + + $cmd['allowDiskUse'] = $this->options['allowDiskUse']; + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + if ($this->options['useCursor']) { + $cmd['cursor'] = isset($this->options["batchSize"]) + ? array('batchSize' => $this->options["batchSize"]) + : new stdClass; + } + + return new Command($cmd); + } +} diff --git a/tests/Collection/CrudSpec/AggregateFunctionalTest.php b/tests/Collection/CrudSpec/AggregateFunctionalTest.php index bdacc989b..744088ec8 100644 --- a/tests/Collection/CrudSpec/AggregateFunctionalTest.php +++ b/tests/Collection/CrudSpec/AggregateFunctionalTest.php @@ -3,7 +3,6 @@ namespace MongoDB\Tests\Collection\CrudSpec; use MongoDB\Collection; -use MongoDB\FeatureDetection; use MongoDB\Driver\ReadPreference; /** @@ -13,6 +12,8 @@ */ class AggregateFunctionalTest extends FunctionalTestCase { + private static $wireVersionForOutOperator = 2; + public function setUp() { parent::setUp(); @@ -43,7 +44,7 @@ public function testAggregateWithOut() { $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - if ( ! FeatureDetection::isSupported($server, FeatureDetection::API_AGGREGATE_CURSOR)) { + if ( ! \MongoDB\server_supports_feature($server, self::$wireVersionForOutOperator)) { $this->markTestSkipped('$out aggregation pipeline operator is not supported'); } From 9746ccd2d55d1de64a84eccd132cb02087014ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 10 Jun 2015 16:42:30 -0400 Subject: [PATCH 05/24] Extract Collection::distinct() to an operation class --- src/Collection.php | 46 ++++------------- src/Operation/Distinct.php | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 src/Operation/Distinct.php diff --git a/src/Collection.php b/src/Collection.php index fdc3ad951..283f56722 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -16,6 +16,7 @@ use MongoDB\Model\IndexInfoIteratorIterator; use MongoDB\Model\IndexInput; use MongoDB\Operation\Aggregate; +use MongoDB\Operation\Distinct; use Traversable; class Collection @@ -356,30 +357,20 @@ public function deleteOne(array $filter) } /** - * Finds the distinct values for a specified field across the collection + * Finds the distinct values for a specified field across the collection. * - * @see http://docs.mongodb.org/manual/reference/command/distinct/ - * @see Collection::getDistinctOptions() for supported $options - * - * @param string $fieldName The fieldname to use - * @param array $filter The find query to execute - * @param array $options Additional options - * @return integer + * @see Distinct::__construct() for supported options + * @param string $fieldName Field for which to return distinct values + * @param array $filter Query by which to filter documents + * @param array $options Command options + * @return mixed[] */ public function distinct($fieldName, array $filter = array(), array $options = array()) { - $options = array_merge($this->getDistinctOptions(), $options); - $cmd = array( - "distinct" => $this->collname, - "key" => $fieldName, - "query" => (object) $filter, - ) + $options; + $operation = new Distinct($this->dbname, $this->collname, $fieldName, $filter, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - $doc = current($this->_runCommand($this->dbname, $cmd)->toArray()); - if ($doc["ok"]) { - return $doc["values"]; - } - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** @@ -657,23 +648,6 @@ public function getDatabaseName() return $this->dbname; } - /** - * Retrieves all distinct options with their default values. - * - * @return array of Collection::distinct() options - */ - public function getDistinctOptions() - { - return array( - /** - * The maximum amount of time to allow the query to run. The default is infinite. - * - * @see http://docs.mongodb.org/manual/reference/command/distinct/ - */ - "maxTimeMS" => 0, - ); - } - /** * Retrieves all findOneDelete options with their default values. * diff --git a/src/Operation/Distinct.php b/src/Operation/Distinct.php new file mode 100644 index 000000000..576627054 --- /dev/null +++ b/src/Operation/Distinct.php @@ -0,0 +1,100 @@ +databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->fieldName = (string) $fieldName; + $this->filter = $filter; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return mixed[] + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, $this->createCommand()); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + if ( ! isset($result['values']) || ! is_array($result['values'])) { + throw new UnexpectedValueException('distinct command did not return a "values" array'); + } + + return $result['values']; + } + + /** + * Create the distinct command. + * + * @return Command + */ + private function createCommand() + { + $cmd = array( + 'distinct' => $this->collectionName, + 'key' => $this->fieldName, + ); + + if ( ! empty($this->filter)) { + $cmd['query'] = (object) $this->filter; + } + + if (isset($this->options['maxTimeMS'])) { + $cmd[$option] = $this->options['maxTimeMS']; + } + + return new Command($cmd); + } +} From 5b7bb9a43ba24409e10e884606fa5427d2754799 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 10 Jun 2015 16:43:13 -0400 Subject: [PATCH 06/24] Extract Collection::createIndexes() to an operation class Additionally, this changes createIndexes() to no longer allow an empty array as input. The index management spec doesn't state that empty input must be accepted, so I'm not sure why we had that behavior. --- src/Collection.php | 69 +--------- src/Operation/CreateIndexes.php | 118 ++++++++++++++++++ .../IndexManagementFunctionalTest.php | 5 +- 3 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 src/Operation/CreateIndexes.php diff --git a/src/Collection.php b/src/Collection.php index 283f56722..81e034d2c 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -16,6 +16,7 @@ use MongoDB\Model\IndexInfoIteratorIterator; use MongoDB\Model\IndexInput; use MongoDB\Operation\Aggregate; +use MongoDB\Operation\CreateIndexes; use MongoDB\Operation\Distinct; use Traversable; @@ -262,8 +263,6 @@ public function count(array $filter = array(), array $options = array()) /** * Create a single index for the collection. * - * @see http://docs.mongodb.org/manual/reference/command/createIndexes/ - * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/ * @see Collection::createIndexes() * @param array|object $key Document containing fields mapped to values, * which denote order or an index type @@ -294,34 +293,16 @@ public function createIndex($key, array $options = array()) * * @see http://docs.mongodb.org/manual/reference/command/createIndexes/ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/ - * @param array $indexes List of index specifications + * @param array[] $indexes List of index specifications * @return string[] The names of the created indexes * @throws InvalidArgumentException if an index specification is invalid */ public function createIndexes(array $indexes) { - if (empty($indexes)) { - return array(); - } - - foreach ($indexes as $i => $index) { - if ( ! is_array($index)) { - throw new UnexpectedTypeException($index, 'array'); - } - - if ( ! isset($index['ns'])) { - $index['ns'] = $this->ns; - } - - $indexes[$i] = new IndexInput($index); - } - - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); - $server = $this->manager->selectServer($readPreference); + $operation = new CreateIndexes($this->dbname, $this->collname, $indexes); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return (FeatureDetection::isSupported($server, FeatureDetection::API_CREATEINDEXES_CMD)) - ? $this->createIndexesCommand($server, $indexes) - : $this->createIndexesLegacy($server, $indexes); + return $operation->execute($server); } /** @@ -1184,46 +1165,6 @@ protected function _update($filter, $update, $options) return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc); } - /** - * Create one or more indexes for the collection using the createIndexes - * command. - * - * @param Server $server - * @param IndexInput[] $indexes - * @return string[] The names of the created indexes - */ - private function createIndexesCommand(Server $server, array $indexes) - { - $command = new Command(array( - 'createIndexes' => $this->collname, - 'indexes' => $indexes, - )); - $server->executeCommand($this->dbname, $command); - - return array_map(function(IndexInput $index) { return (string) $index; }, $indexes); - } - - /** - * Create one or more indexes for the collection by inserting into the - * "system.indexes" collection (MongoDB <2.6). - * - * @param Server $server - * @param IndexInput[] $indexes - * @return string[] The names of the created indexes - */ - private function createIndexesLegacy(Server $server, array $indexes) - { - $bulk = new BulkWrite(true); - - foreach ($indexes as $index) { - $bulk->insert($index); - } - - $server->executeBulkWrite($this->dbname . '.system.indexes', $bulk); - - return array_map(function(IndexInput $index) { return (string) $index; }, $indexes); - } - /** * Returns information for all indexes for this collection using the * listIndexes command. diff --git a/src/Operation/CreateIndexes.php b/src/Operation/CreateIndexes.php new file mode 100644 index 000000000..858d31e8b --- /dev/null +++ b/src/Operation/CreateIndexes.php @@ -0,0 +1,118 @@ +indexes[] = new IndexInput($index); + } + + $this->databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + } + + /** + * Execute the operation. + * + * For servers < 2.6, this will actually perform an insert operation on the + * database's "system.indexes" collection. + * + * @see Executable::execute() + * @param Server $server + * @return string[] The names of the created indexes + */ + public function execute(Server $server) + { + if (\MongoDB\server_supports_feature($server, self::$wireVersionForCommand)) { + $this->executeCommand($server); + } else { + $this->executeLegacy($server); + } + + return array_map(function(IndexInput $index) { return (string) $index; }, $this->indexes); + } + + /** + * Create one or more indexes for the collection using the createIndexes + * command. + * + * @param Server $server + */ + private function executeCommand(Server $server) + { + $command = new Command(array( + 'createIndexes' => $this->collectionName, + 'indexes' => $this->indexes, + )); + + $cursor = $server->executeCommand($this->databaseName, $command); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + } + + /** + * Create one or more indexes for the collection by inserting into the + * "system.indexes" collection (MongoDB <2.6). + * + * @param Server $server + * @param IndexInput[] $indexes + */ + private function executeLegacy(Server $server) + { + $bulk = new BulkWrite(true); + + foreach ($this->indexes as $index) { + $bulk->insert($index); + } + + $server->executeBulkWrite($this->databaseName . '.system.indexes', $bulk); + } +} diff --git a/tests/Collection/IndexManagementFunctionalTest.php b/tests/Collection/IndexManagementFunctionalTest.php index d91f62918..a9fba6409 100644 --- a/tests/Collection/IndexManagementFunctionalTest.php +++ b/tests/Collection/IndexManagementFunctionalTest.php @@ -85,7 +85,10 @@ public function testCreateIndexes() }); } - public function testCreateIndexesWithEmptyInputIsNop() + /** + * @expectedException MongoDB\Exception\InvalidArgumentException + */ + public function testCreateIndexesRequiresAtLeastOneIndex() { $this->assertSame(array(), $this->collection->createIndexes(array())); } From e36a304c8dfdbe431538151b3e206db8226495f4 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Thu, 11 Jun 2015 16:33:59 -0400 Subject: [PATCH 07/24] Extract Collection::count() to an operation class --- src/Collection.php | 62 +++--------------- src/Operation/Count.php | 142 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 src/Operation/Count.php diff --git a/src/Collection.php b/src/Collection.php index 81e034d2c..55afcab71 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -17,6 +17,7 @@ use MongoDB\Model\IndexInput; use MongoDB\Operation\Aggregate; use MongoDB\Operation\CreateIndexes; +use MongoDB\Operation\Count; use MongoDB\Operation\Distinct; use Traversable; @@ -236,28 +237,19 @@ public function bulkWrite(array $ops, array $options = array()) } /** - * Counts all documents matching $filter - * If no $filter provided, returns the numbers of documents in the collection + * Gets the number of documents matching the filter. * - * @see http://docs.mongodb.org/manual/reference/command/count/ - * @see Collection::getCountOptions() for supported $options - * - * @param array $filter The find query to execute - * @param array $options Additional options + * @see Count::__construct() for supported options + * @param array $filter Query by which to filter documents + * @param array $options Command options * @return integer */ public function count(array $filter = array(), array $options = array()) { - $cmd = array( - "count" => $this->collname, - "query" => (object) $filter, - ) + $options; + $operation = new Count($this->dbname, $this->collname, $filter, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - $doc = current($this->_runCommand($this->dbname, $cmd)->toArray()); - if ($doc["ok"]) { - return (integer) $doc["n"]; - } - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** @@ -581,44 +573,6 @@ public function getCollectionName() return $this->collname; } - /** - * Retrieves all count options with their default values. - * - * @return array of Collection::count() options - */ - public function getCountOptions() - { - return array( - /** - * The index to use. - * - * @see http://docs.mongodb.org/manual/reference/command/count/ - */ - "hint" => "", // string or document - - /** - * The maximum number of documents to count. - * - * @see http://docs.mongodb.org/manual/reference/command/count/ - */ - "limit" => 0, - - /** - * The maximum amount of time to allow the query to run. - * - * @see http://docs.mongodb.org/manual/reference/command/count/ - */ - "maxTimeMS" => 0, - - /** - * The number of documents to skip before returning the documents. - * - * @see http://docs.mongodb.org/manual/reference/command/count/ - */ - "skip" => 0, - ); - } - /** * Return the database name. * diff --git a/src/Operation/Count.php b/src/Operation/Count.php new file mode 100644 index 000000000..a4ec031d9 --- /dev/null +++ b/src/Operation/Count.php @@ -0,0 +1,142 @@ +generateIndexName($options['hint']); + } + + if ( ! is_string($options['hint'])) { + throw new InvalidArgumentTypeException('"hint" option', $options['hint'], 'string or array or object'); + } + } + + if (isset($options['limit']) && ! is_integer($options['limit'])) { + throw new InvalidArgumentTypeException('"limit" option', $options['limit'], 'integer'); + } + + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { + throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); + } + + if (isset($options['skip']) && ! is_integer($options['skip'])) { + throw new InvalidArgumentTypeException('"skip" option', $options['skip'], 'integer'); + } + + $this->databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->filter = $filter; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return integer + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, $this->createCommand()); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + if ( ! isset($result['n']) || ! is_integer($result['n'])) { + throw new UnexpectedValueException('count command did not return an "n" integer'); + } + + return $result['n']; + } + + /** + * Create the count command. + * + * @return Command + */ + private function createCommand() + { + $cmd = array( + 'count' => $this->collectionName, + ); + + if ( ! empty($this->filter)) { + $cmd['query'] = (object) $this->filter; + } + + foreach (array('hint', 'limit', 'maxTimeMS', 'skip') as $option) { + if (isset($this->options[$option])) { + $cmd[$option] = $this->options[$option]; + } + } + + return new Command($cmd); + } + + /** + * Generates an index name from its key specification. + * + * @param array|object $key Document containing fields mapped to values, + * which denote order or an index type + * @return string + */ + private function generateIndexName($key) + { + $name = ''; + + foreach ($key as $field => $type) { + $name .= ($name != '' ? '_' : '') . $field . '_' . $type; + } + + return $name; + } +} From 02e651cb5fd07d5f591e07c8a1205b368a491da5 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Sun, 14 Jun 2015 22:04:16 -0400 Subject: [PATCH 08/24] Extract Collection findAndModify methods to operation classes --- src/Collection.php | 294 +++--------------- src/Operation/FindAndModify.php | 179 +++++++++++ src/Operation/FindOneAndDelete.php | 83 +++++ src/Operation/FindOneAndReplace.php | 122 ++++++++ src/Operation/FindOneAndUpdate.php | 122 ++++++++ .../FindOneAndReplaceFunctionalTest.php | 9 +- .../FindOneAndUpdateFunctionalTest.php | 9 +- 7 files changed, 556 insertions(+), 262 deletions(-) create mode 100644 src/Operation/FindAndModify.php create mode 100644 src/Operation/FindOneAndDelete.php create mode 100644 src/Operation/FindOneAndReplace.php create mode 100644 src/Operation/FindOneAndUpdate.php diff --git a/src/Collection.php b/src/Collection.php index 55afcab71..583b341fa 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -19,6 +19,9 @@ use MongoDB\Operation\CreateIndexes; use MongoDB\Operation\Count; use MongoDB\Operation\Distinct; +use MongoDB\Operation\FindOneAndDelete; +use MongoDB\Operation\FindOneAndReplace; +use MongoDB\Operation\FindOneAndUpdate; use Traversable; class Collection @@ -452,103 +455,63 @@ public function findOne(array $filter = array(), array $options = array()) /** * Finds a single document and deletes it, returning the original. * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - * @see Collection::getFindOneAndDelete() for supported $options + * The document to return may be null. * - * @param array $filter The $filter criteria to search for - * @param array $options Additional options - * @return array The original document + * @see FindOneAndDelete::__construct() for supported options + * @param array|object $filter Query by which to filter documents + * @param array $options Command options + * @return array|null */ - public function findOneAndDelete(array $filter, array $options = array()) + public function findOneAndDelete($filter, array $options = array()) { - $options = array_merge($this->getFindOneAndDeleteOptions(), $options); - $options = $this->_massageFindAndModifyOptions($options); - $cmd = array( - "findandmodify" => $this->collname, - "query" => $filter, - ) + $options; - - $doc = current($this->_runCommand($this->dbname, $cmd)->toArray()); - if ($doc["ok"]) { - return is_object($doc["value"]) ? (array) $doc["value"] : $doc["value"]; - } + $operation = new FindOneAndDelete($this->dbname, $this->collname, $filter, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** - * Finds a single document and replaces it, returning either the original or the replaced document - * By default, returns the original document. - * To return the new document set: - * $options = array("returnDocument" => Collection::FIND_ONE_AND_RETURN_AFTER); + * Finds a single document and replaces it, returning either the original or + * the replaced document. * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - * @see Collection::getFindOneAndReplace() for supported $options + * The document to return may be null. By default, the original document is + * returned. Specify FindOneAndReplace::RETURN_DOCUMENT_AFTER for the + * "returnDocument" option to return the updated document. * - * @param array $filter The $filter criteria to search for - * @param array $replacement The document to replace with - * @param array $options Additional options - * @return array + * @see FindOneAndReplace::__construct() for supported options + * @param array|object $filter Query by which to filter documents + * @param array|object $replacement Replacement document + * @param array $options Command options + * @return array|null */ - public function findOneAndReplace(array $filter, array $replacement, array $options = array()) + public function findOneAndReplace($filter, $replacement, array $options = array()) { - $firstKey = key($replacement); - if (isset($firstKey[0]) && $firstKey[0] == '$') { - throw new InvalidArgumentException("First key in \$replacement must NOT be a \$operator"); - } - - $options = array_merge($this->getFindOneAndReplaceOptions(), $options); - $options = $this->_massageFindAndModifyOptions($options, $replacement); - - $cmd = array( - "findandmodify" => $this->collname, - "query" => $filter, - ) + $options; - - $doc = current($this->_runCommand($this->dbname, $cmd)->toArray()); - if ($doc["ok"]) { - return $this->_massageFindAndModifyResult($doc, $options); - } + $operation = new FindOneAndReplace($this->dbname, $this->collname, $filter, $replacement, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** - * Finds a single document and updates it, returning either the original or the updated document - * By default, returns the original document. - * To return the new document set: - * $options = array("returnDocument" => Collection::FIND_ONE_AND_RETURN_AFTER); + * Finds a single document and updates it, returning either the original or + * the updated document. * + * The document to return may be null. By default, the original document is + * returned. Specify FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the + * "returnDocument" option to return the updated document. * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - * @see Collection::getFindOneAndUpdate() for supported $options - * - * @param array $filter The $filter criteria to search for - * @param array $update An array of update operators to apply to the document - * @param array $options Additional options - * @return array + * @see FindOneAndReplace::__construct() for supported options + * @param array|object $filter Query by which to filter documents + * @param array|object $update Update to apply to the matched document + * @param array $options Command options + * @return array|null */ - public function findOneAndUpdate(array $filter, array $update, array $options = array()) + public function findOneAndUpdate($filter, $update, array $options = array()) { - $firstKey = key($update); - if (!isset($firstKey[0]) || $firstKey[0] != '$') { - throw new InvalidArgumentException("First key in \$update must be a \$operator"); - } - - $options = array_merge($this->getFindOneAndUpdateOptions(), $options); - $options = $this->_massageFindAndModifyOptions($options, $update); - - $cmd = array( - "findandmodify" => $this->collname, - "query" => $filter, - ) + $options; - - $doc = current($this->_runCommand($this->dbname, $cmd)->toArray()); - if ($doc["ok"]) { - return $this->_massageFindAndModifyResult($doc, $options); - } + $operation = new FindOneAndUpdate($this->dbname, $this->collname, $filter, $update, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - throw $this->_generateCommandException($doc); + return $operation->execute($server); } /** @@ -583,133 +546,6 @@ public function getDatabaseName() return $this->dbname; } - /** - * Retrieves all findOneDelete options with their default values. - * - * @return array of Collection::findOneAndDelete() options - */ - public function getFindOneAndDeleteOptions() - { - return array( - - /** - * The maximum amount of time to allow the query to run. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "maxTimeMS" => 0, - - /** - * Limits the fields to return for all matching documents. - * - * @see http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results - */ - "projection" => array(), - - /** - * Determines which document the operation modifies if the query selects multiple documents. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "sort" => array(), - ); - } - - /** - * Retrieves all findOneAndReplace options with their default values. - * - * @return array of Collection::findOneAndReplace() options - */ - public function getFindOneAndReplaceOptions() - { - return array( - - /** - * The maximum amount of time to allow the query to run. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "maxTimeMS" => 0, - - /** - * Limits the fields to return for all matching documents. - * - * @see http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results - */ - "projection" => array(), - - /** - * When ReturnDocument.After, returns the replaced or inserted document rather than the original. - * Defaults to ReturnDocument.Before. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "returnDocument" => self::FIND_ONE_AND_RETURN_BEFORE, - - /** - * Determines which document the operation modifies if the query selects multiple documents. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "sort" => array(), - - /** - * When true, findAndModify creates a new document if no document matches the query. The - * default is false. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "upsert" => false, - ); - } - - /** - * Retrieves all findOneAndUpdate options with their default values. - * - * @return array of Collection::findOneAndUpdate() options - */ - public function getFindOneAndUpdateOptions() - { - return array( - - /** - * The maximum amount of time to allow the query to run. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "maxTimeMS" => 0, - - /** - * Limits the fields to return for all matching documents. - * - * @see http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results - */ - "projection" => array(), - - /** - * When ReturnDocument.After, returns the updated or inserted document rather than the original. - * Defaults to ReturnDocument.Before. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "returnDocument" => self::FIND_ONE_AND_RETURN_BEFORE, - - /** - * Determines which document the operation modifies if the query selects multiple documents. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "sort" => array(), - - /** - * When true, creates a new document if no document matches the query. The default is false. - * - * @see http://docs.mongodb.org/manual/reference/command/findAndModify/ - */ - "upsert" => false, - ); - } - /** * Retrieves all find options with their default values. * @@ -1025,56 +861,6 @@ final protected function _generateCommandException($doc) return new RuntimeException("FIXME: Unknown error"); } - /** - * Internal helper for massaging findandmodify options - * @internal - */ - final protected function _massageFindAndModifyOptions($options, $update = array()) - { - $ret = array( - "sort" => $options["sort"], - "new" => isset($options["returnDocument"]) ? $options["returnDocument"] == self::FIND_ONE_AND_RETURN_AFTER : false, - "fields" => $options["projection"], - "upsert" => isset($options["upsert"]) ? $options["upsert"] : false, - ); - if ($update) { - $ret["update"] = $update; - } else { - $ret["remove"] = true; - } - return $ret; - } - - /** - * Internal helper for massaging the findAndModify result. - * - * @internal - * @param array $result - * @param array $options - * @return array|null - */ - final protected function _massageFindAndModifyResult(array $result, array $options) - { - if ($result['value'] === null) { - return null; - } - - /* Prior to 3.0, findAndModify returns an empty document instead of null - * when an upsert is performed and the pre-modified document was - * requested. - */ - if ($options['upsert'] && ! $options['new'] && - isset($result['lastErrorObject']->updatedExisting) && - ! $result['lastErrorObject']->updatedExisting) { - - return null; - } - - return is_object($result["value"]) - ? (array) $result['value'] - : $result['value']; - } - /** * Constructs the Query Wire Protocol field 'flags' based on $options * provided to other helpers diff --git a/src/Operation/FindAndModify.php b/src/Operation/FindAndModify.php new file mode 100644 index 000000000..c880bc3ab --- /dev/null +++ b/src/Operation/FindAndModify.php @@ -0,0 +1,179 @@ + false, + 'remove' => false, + 'upsert' => false, + ); + + if (isset($options['fields']) && ! is_array($options['fields']) && ! is_object($options['fields'])) { + throw new InvalidArgumentTypeException('"fields" option', $options['fields'], 'array or object'); + } + + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { + throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); + } + + if ( ! is_bool($options['new'])) { + throw new InvalidArgumentTypeException('"new" option', $options['new'], 'boolean'); + } + + if (isset($options['query']) && ! is_array($options['query']) && ! is_object($options['query'])) { + throw new InvalidArgumentTypeException('"query" option', $options['query'], 'array or object'); + } + + if ( ! is_bool($options['remove'])) { + throw new InvalidArgumentTypeException('"remove" option', $options['remove'], 'boolean'); + } + + if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) { + throw new InvalidArgumentTypeException('"sort" option', $options['sort'], 'array or object'); + } + + if (isset($options['update']) && ! is_array($options['update']) && ! is_object($options['update'])) { + throw new InvalidArgumentTypeException('"update" option', $options['update'], 'array or object'); + } + + if ( ! is_bool($options['upsert'])) { + throw new InvalidArgumentTypeException('"upsert" option', $options['upsert'], 'boolean'); + } + + if ( ! (isset($options['update']) xor $options['remove'])) { + throw new InvalidArgumentException('The "remove" option must be true or an "update" document must be specified, but not both'); + } + + $this->databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array|null + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, $this->createCommand()); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + if ( ! isset($result['value'])) { + return null; + } + + /* Prior to 3.0, findAndModify returns an empty document instead of null + * when an upsert is performed and the pre-modified document was + * requested. + */ + if ($this->options['upsert'] && ! $this->options['new'] && + isset($result['lastErrorObject']->updatedExisting) && + ! $result['lastErrorObject']->updatedExisting) { + + return null; + } + + if ( ! is_object($result['value'])) { + throw new UnexpectedValueException('findAndModify command did not return a "value" document'); + } + + return (array) $result['value']; + } + + /** + * Create the findAndModify command. + * + * @return Command + */ + private function createCommand() + { + $cmd = array( + 'findAndModify' => $this->collectionName, + ); + + if ($this->options['remove']) { + $cmd['remove'] = true; + } else { + $cmd['new'] = $this->options['new']; + $cmd['upsert'] = $this->options['upsert']; + } + + foreach (array('fields', 'query', 'sort', 'update') as $option) { + if (isset($this->options[$option])) { + $cmd[$option] = (object) $this->options[$option]; + } + } + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + return new Command($cmd); + } +} diff --git a/src/Operation/FindOneAndDelete.php b/src/Operation/FindOneAndDelete.php new file mode 100644 index 000000000..6c841b84e --- /dev/null +++ b/src/Operation/FindOneAndDelete.php @@ -0,0 +1,83 @@ +findAndModify = new FindAndModify( + $databaseName, + $collectionName, + array( + 'fields' => isset($options['projection']) ? $options['projection'] : null, + 'maxTimeMS' => isset($options['maxTimeMS']) ? $options['maxTimeMS'] : null, + 'query' => $filter, + 'remove' => true, + 'sort' => isset($options['sort']) ? $options['sort'] : null, + ) + ); + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array|null + */ + public function execute(Server $server) + { + return $this->findAndModify->execute($server); + } +} diff --git a/src/Operation/FindOneAndReplace.php b/src/Operation/FindOneAndReplace.php new file mode 100644 index 000000000..1a8c5554a --- /dev/null +++ b/src/Operation/FindOneAndReplace.php @@ -0,0 +1,122 @@ + self::RETURN_DOCUMENT_BEFORE, + 'upsert' => false, + ); + + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { + throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); + } + + if (isset($options['projection']) && ! is_array($options['projection']) && ! is_object($options['projection'])) { + throw new InvalidArgumentTypeException('"projection" option', $options['projection'], 'array or object'); + } + + if ( ! is_integer($options['returnDocument'])) { + throw new InvalidArgumentTypeException('"returnDocument" option', $options['returnDocument'], 'integer'); + } + + if ($options['returnDocument'] !== self::RETURN_DOCUMENT_AFTER && + $options['returnDocument'] !== self::RETURN_DOCUMENT_BEFORE) { + throw new InvalidArgumentException('Invalid value for "returnDocument" option: ' . $options['returnDocument']); + } + + if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) { + throw new InvalidArgumentTypeException('"sort" option', $options['sort'], 'array or object'); + } + + if ( ! is_bool($options['upsert'])) { + throw new InvalidArgumentTypeException('"upsert" option', $options['upsert'], 'boolean'); + } + + $this->findAndModify = new FindAndModify( + $databaseName, + $collectionName, + array( + 'fields' => isset($options['projection']) ? $options['projection'] : null, + 'maxTimeMS' => isset($options['maxTimeMS']) ? $options['maxTimeMS'] : null, + 'new' => $options['returnDocument'] === self::RETURN_DOCUMENT_AFTER, + 'query' => $filter, + 'sort' => isset($options['sort']) ? $options['sort'] : null, + 'update' => $replacement, + 'upsert' => $options['upsert'], + ) + ); + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array|null + */ + public function execute(Server $server) + { + return $this->findAndModify->execute($server); + } +} diff --git a/src/Operation/FindOneAndUpdate.php b/src/Operation/FindOneAndUpdate.php new file mode 100644 index 000000000..06cc2121b --- /dev/null +++ b/src/Operation/FindOneAndUpdate.php @@ -0,0 +1,122 @@ + self::RETURN_DOCUMENT_BEFORE, + 'upsert' => false, + ); + + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { + throw new InvalidArgumentTypeException('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); + } + + if (isset($options['projection']) && ! is_array($options['projection']) && ! is_object($options['projection'])) { + throw new InvalidArgumentTypeException('"projection" option', $options['projection'], 'array or object'); + } + + if ( ! is_integer($options['returnDocument'])) { + throw new InvalidArgumentTypeException('"returnDocument" option', $options['returnDocument'], 'integer'); + } + + if ($options['returnDocument'] !== self::RETURN_DOCUMENT_AFTER && + $options['returnDocument'] !== self::RETURN_DOCUMENT_BEFORE) { + throw new InvalidArgumentException('Invalid value for "returnDocument" option: ' . $options['returnDocument']); + } + + if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) { + throw new InvalidArgumentTypeException('"sort" option', $options['sort'], 'array or object'); + } + + if ( ! is_bool($options['upsert'])) { + throw new InvalidArgumentTypeException('"upsert" option', $options['upsert'], 'boolean'); + } + + $this->findAndModify = new FindAndModify( + $databaseName, + $collectionName, + array( + 'fields' => isset($options['projection']) ? $options['projection'] : null, + 'maxTimeMS' => isset($options['maxTimeMS']) ? $options['maxTimeMS'] : null, + 'new' => $options['returnDocument'] === self::RETURN_DOCUMENT_AFTER, + 'query' => $filter, + 'sort' => isset($options['sort']) ? $options['sort'] : null, + 'update' => $update, + 'upsert' => $options['upsert'], + ) + ); + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array|null + */ + public function execute(Server $server) + { + return $this->findAndModify->execute($server); + } +} diff --git a/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php b/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php index 26c5cf12c..2fce74904 100644 --- a/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php +++ b/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php @@ -3,6 +3,7 @@ namespace MongoDB\Tests\Collection\CrudSpec; use MongoDB\Collection; +use MongoDB\Operation\FindOneAndReplace; /** * CRUD spec functional tests for findOneAndReplace(). @@ -46,7 +47,7 @@ public function testFindOneAndReplaceWhenManyDocumentsMatchReturningDocumentAfte $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndReplace::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); @@ -89,7 +90,7 @@ public function testFindOneAndReplaceWhenOneDocumentMatchesReturningDocumentAfte $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndReplace::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); @@ -156,7 +157,7 @@ public function testFindOneAndReplaceWhenNoDocumentsMatchReturningDocumentAfterM $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndReplace::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); @@ -179,7 +180,7 @@ public function testFindOneAndReplaceWithUpsertWhenNoDocumentsMatchReturningDocu $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndReplace::RETURN_DOCUMENT_AFTER, 'upsert' => true, ); diff --git a/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php b/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php index 10a017da0..af9cf67c0 100644 --- a/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php +++ b/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php @@ -3,6 +3,7 @@ namespace MongoDB\Tests\Collection\CrudSpec; use MongoDB\Collection; +use MongoDB\Operation\FindOneAndUpdate; /** * CRUD spec functional tests for findOneAndUpdate(). @@ -46,7 +47,7 @@ public function testFindOneAndUpdateWhenManyDocumentsMatchReturningDocumentAfter $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); @@ -89,7 +90,7 @@ public function testFindOneAndUpdateWhenOneDocumentMatchesReturningDocumentAfter $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); @@ -155,7 +156,7 @@ public function testFindOneAndUpdateWhenNoDocumentsMatchReturningDocumentAfterMo $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); @@ -177,7 +178,7 @@ public function testFindOneAndUpdateWithUpsertWhenNoDocumentsMatchReturningDocum $options = array( 'projection' => array('x' => 1, '_id' => 0), 'sort' => array('x' => 1), - 'returnDocument' => Collection::FIND_ONE_AND_RETURN_AFTER, + 'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER, 'upsert' => true, ); From 3028dfde3e5ce4039077063dd34be1ba64d4ebc0 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Sun, 14 Jun 2015 22:09:20 -0400 Subject: [PATCH 09/24] Return documents as objects from Collection findAndModify methods --- src/Collection.php | 6 +++--- src/Operation/FindAndModify.php | 4 ++-- src/Operation/FindOneAndDelete.php | 2 +- src/Operation/FindOneAndReplace.php | 2 +- src/Operation/FindOneAndUpdate.php | 2 +- .../CrudSpec/FindOneAndDeleteFunctionalTest.php | 4 ++-- .../CrudSpec/FindOneAndReplaceFunctionalTest.php | 10 +++++----- .../CrudSpec/FindOneAndUpdateFunctionalTest.php | 10 +++++----- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Collection.php b/src/Collection.php index 583b341fa..23b0e23ce 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -460,7 +460,7 @@ public function findOne(array $filter = array(), array $options = array()) * @see FindOneAndDelete::__construct() for supported options * @param array|object $filter Query by which to filter documents * @param array $options Command options - * @return array|null + * @return object|null */ public function findOneAndDelete($filter, array $options = array()) { @@ -482,7 +482,7 @@ public function findOneAndDelete($filter, array $options = array()) * @param array|object $filter Query by which to filter documents * @param array|object $replacement Replacement document * @param array $options Command options - * @return array|null + * @return object|null */ public function findOneAndReplace($filter, $replacement, array $options = array()) { @@ -504,7 +504,7 @@ public function findOneAndReplace($filter, $replacement, array $options = array( * @param array|object $filter Query by which to filter documents * @param array|object $update Update to apply to the matched document * @param array $options Command options - * @return array|null + * @return object|null */ public function findOneAndUpdate($filter, $update, array $options = array()) { diff --git a/src/Operation/FindAndModify.php b/src/Operation/FindAndModify.php index c880bc3ab..0c288b058 100644 --- a/src/Operation/FindAndModify.php +++ b/src/Operation/FindAndModify.php @@ -113,7 +113,7 @@ public function __construct($databaseName, $collectionName, array $options) * * @see Executable::execute() * @param Server $server - * @return array|null + * @return object|null */ public function execute(Server $server) { @@ -143,7 +143,7 @@ public function execute(Server $server) throw new UnexpectedValueException('findAndModify command did not return a "value" document'); } - return (array) $result['value']; + return $result['value']; } /** diff --git a/src/Operation/FindOneAndDelete.php b/src/Operation/FindOneAndDelete.php index 6c841b84e..4ff1cf88a 100644 --- a/src/Operation/FindOneAndDelete.php +++ b/src/Operation/FindOneAndDelete.php @@ -74,7 +74,7 @@ public function __construct($databaseName, $collectionName, $filter, array $opti * * @see Executable::execute() * @param Server $server - * @return array|null + * @return object|null */ public function execute(Server $server) { diff --git a/src/Operation/FindOneAndReplace.php b/src/Operation/FindOneAndReplace.php index 1a8c5554a..dbf834360 100644 --- a/src/Operation/FindOneAndReplace.php +++ b/src/Operation/FindOneAndReplace.php @@ -113,7 +113,7 @@ public function __construct($databaseName, $collectionName, $filter, $replacemen * * @see Executable::execute() * @param Server $server - * @return array|null + * @return object|null */ public function execute(Server $server) { diff --git a/src/Operation/FindOneAndUpdate.php b/src/Operation/FindOneAndUpdate.php index 06cc2121b..7bcc05d7b 100644 --- a/src/Operation/FindOneAndUpdate.php +++ b/src/Operation/FindOneAndUpdate.php @@ -113,7 +113,7 @@ public function __construct($databaseName, $collectionName, $filter, $update, ar * * @see Executable::execute() * @param Server $server - * @return array|null + * @return object|null */ public function execute(Server $server) { diff --git a/tests/Collection/CrudSpec/FindOneAndDeleteFunctionalTest.php b/tests/Collection/CrudSpec/FindOneAndDeleteFunctionalTest.php index 3d5aa9b0a..ecd321220 100644 --- a/tests/Collection/CrudSpec/FindOneAndDeleteFunctionalTest.php +++ b/tests/Collection/CrudSpec/FindOneAndDeleteFunctionalTest.php @@ -25,7 +25,7 @@ public function testFindOneAndDeleteWhenManyDocumentsMatch() ); $document = $this->collection->findOneAndDelete($filter, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -44,7 +44,7 @@ public function testFindOneAndDeleteWhenOneDocumentMatches() ); $document = $this->collection->findOneAndDelete($filter, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), diff --git a/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php b/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php index 2fce74904..9f4480d2b 100644 --- a/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php +++ b/tests/Collection/CrudSpec/FindOneAndReplaceFunctionalTest.php @@ -29,7 +29,7 @@ public function testFindOneAndReplaceWhenManyDocumentsMatchReturningDocumentBefo ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -51,7 +51,7 @@ public function testFindOneAndReplaceWhenManyDocumentsMatchReturningDocumentAfte ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); - $this->assertSame(array('x' => 32), $document); + $this->assertEquals((object) array('x' => 32), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -72,7 +72,7 @@ public function testFindOneAndReplaceWhenOneDocumentMatchesReturningDocumentBefo ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -94,7 +94,7 @@ public function testFindOneAndReplaceWhenOneDocumentMatchesReturningDocumentAfte ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); - $this->assertSame(array('x' => 32), $document); + $this->assertEquals((object) array('x' => 32), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -185,7 +185,7 @@ public function testFindOneAndReplaceWithUpsertWhenNoDocumentsMatchReturningDocu ); $document = $this->collection->findOneAndReplace($filter, $replacement, $options); - $this->assertSame(array('x' => 44), $document); + $this->assertEquals((object) array('x' => 44), $document); $expected = array( array('_id' => 1, 'x' => 11), diff --git a/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php b/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php index af9cf67c0..c07b9a98c 100644 --- a/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php +++ b/tests/Collection/CrudSpec/FindOneAndUpdateFunctionalTest.php @@ -29,7 +29,7 @@ public function testFindOneAndUpdateWhenManyDocumentsMatchReturningDocumentBefor ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -51,7 +51,7 @@ public function testFindOneAndUpdateWhenManyDocumentsMatchReturningDocumentAfter ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); - $this->assertSame(array('x' => 23), $document); + $this->assertEquals((object) array('x' => 23), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -72,7 +72,7 @@ public function testFindOneAndUpdateWhenOneDocumentMatchesReturningDocumentBefor ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); - $this->assertSame(array('x' => 22), $document); + $this->assertEquals((object) array('x' => 22), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -94,7 +94,7 @@ public function testFindOneAndUpdateWhenOneDocumentMatchesReturningDocumentAfter ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); - $this->assertSame(array('x' => 23), $document); + $this->assertEquals((object) array('x' => 23), $document); $expected = array( array('_id' => 1, 'x' => 11), @@ -183,7 +183,7 @@ public function testFindOneAndUpdateWithUpsertWhenNoDocumentsMatchReturningDocum ); $document = $this->collection->findOneAndUpdate($filter, $update, $options); - $this->assertSame(array('x' => 1), $document); + $this->assertEquals((object) array('x' => 1), $document); $expected = array( array('_id' => 1, 'x' => 11), From bc65629c772a53c57b4f5debad6aae5b76c9a01c Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Sun, 14 Jun 2015 22:26:12 -0400 Subject: [PATCH 10/24] Replace private methods with generate_index_name() function --- src/Model/IndexInput.php | 20 +------------------- src/Operation/Count.php | 20 +------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/src/Model/IndexInput.php b/src/Model/IndexInput.php index dc6338f7e..db4288992 100644 --- a/src/Model/IndexInput.php +++ b/src/Model/IndexInput.php @@ -50,7 +50,7 @@ public function __construct(array $index) } if ( ! isset($index['name'])) { - $index['name'] = $this->generateName($index['key']); + $index['name'] = \MongoDB\generate_index_name($index['key']); } if ( ! is_string($index['name'])) { @@ -80,22 +80,4 @@ public function bsonSerialize() { return $this->index; } - - /** - * Generates an index name from its key specification. - * - * @param array|object $key Document containing fields mapped to values, - * which denote order or an index type - * @return string - */ - private function generateName($key) - { - $name = ''; - - foreach ($key as $field => $type) { - $name .= ($name != '' ? '_' : '') . $field . '_' . $type; - } - - return $name; - } } diff --git a/src/Operation/Count.php b/src/Operation/Count.php index a4ec031d9..8eb93b00e 100644 --- a/src/Operation/Count.php +++ b/src/Operation/Count.php @@ -49,7 +49,7 @@ public function __construct($databaseName, $collectionName, array $filter = arra { if (isset($options['hint'])) { if (is_array($options['hint']) || is_object($options['hint'])) { - $options['hint'] = $this->generateIndexName($options['hint']); + $options['hint'] = \MongoDB\generate_index_name($options['hint']); } if ( ! is_string($options['hint'])) { @@ -121,22 +121,4 @@ private function createCommand() return new Command($cmd); } - - /** - * Generates an index name from its key specification. - * - * @param array|object $key Document containing fields mapped to values, - * which denote order or an index type - * @return string - */ - private function generateIndexName($key) - { - $name = ''; - - foreach ($key as $field => $type) { - $name .= ($name != '' ? '_' : '') . $field . '_' . $type; - } - - return $name; - } } From 1269542ca7b95678c9a72560c32be7fdb0c927c7 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:11:36 -0400 Subject: [PATCH 11/24] Extract Client::listDatabases() to an operation class --- src/Client.php | 27 +++--------- src/Operation/ListDatabases.php | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 src/Operation/ListDatabases.php diff --git a/src/Client.php b/src/Client.php index 8386addf3..ed7b6ebe8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,9 +7,8 @@ use MongoDB\Driver\Manager; use MongoDB\Driver\ReadPreference; use MongoDB\Driver\WriteConcern; -use MongoDB\Exception\UnexpectedValueException; use MongoDB\Model\DatabaseInfoIterator; -use MongoDB\Model\DatabaseInfoLegacyIterator; +use MongoDB\Operation\ListDatabases; class Client { @@ -53,29 +52,15 @@ public function dropDatabase($databaseName) /** * List databases. * - * @see http://docs.mongodb.org/manual/reference/command/listDatabases/ + * @see ListDatabases::__construct() for supported options * @return DatabaseInfoIterator - * @throws UnexpectedValueException if the command result is malformed */ - public function listDatabases() + public function listDatabases(array $options = array()) { - $command = new Command(array('listDatabases' => 1)); + $operation = new ListDatabases($options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - $cursor = $this->manager->executeCommand('admin', $command); - $cursor->setTypeMap(array('document' => 'array')); - $result = current($cursor->toArray()); - - if ( ! isset($result['databases']) || ! is_array($result['databases'])) { - throw new UnexpectedValueException('listDatabases command did not return a "databases" array'); - } - - /* Return an Iterator instead of an array in case listDatabases is - * eventually changed to return a command cursor, like the collection - * and index enumeration commands. This makes the "totalSize" command - * field inaccessible, but users can manually invoke the command if they - * need that value. - */ - return new DatabaseInfoLegacyIterator($result['databases']); + return $operation->execute($server); } /** diff --git a/src/Operation/ListDatabases.php b/src/Operation/ListDatabases.php new file mode 100644 index 000000000..1f9f200ed --- /dev/null +++ b/src/Operation/ListDatabases.php @@ -0,0 +1,77 @@ +options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return DatabaseInfoIterator + */ + public function execute(Server $server) + { + $cmd = array('listDatabases' => 1); + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + $cursor = $server->executeCommand('admin', new Command($cmd)); + $cursor->setTypeMap(array('document' => 'array')); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + if ( ! isset($result['databases']) || ! is_array($result['databases'])) { + throw new UnexpectedValueException('listDatabases command did not return a "databases" array'); + } + + /* Return an Iterator instead of an array in case listDatabases is + * eventually changed to return a command cursor, like the collection + * and index enumeration commands. This makes the "totalSize" command + * field inaccessible, but users can manually invoke the command if they + * need that value. + */ + return new DatabaseInfoLegacyIterator($result['databases']); + } +} From 99650bb320b79c9bc5e94dcfc4b2106a814ca33f Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:12:49 -0400 Subject: [PATCH 12/24] Extract Database::listCollections() to an operation class --- src/Database.php | 67 ++-------------- src/Operation/ListCollections.php | 122 ++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 62 deletions(-) create mode 100644 src/Operation/ListCollections.php diff --git a/src/Database.php b/src/Database.php index 8ca8d11c4..d016279e6 100644 --- a/src/Database.php +++ b/src/Database.php @@ -12,8 +12,7 @@ use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Model\CollectionInfoIterator; -use MongoDB\Model\CollectionInfoCommandIterator; -use MongoDB\Model\CollectionInfoLegacyIterator; +use MongoDB\Operation\ListCollections; class Database { @@ -112,18 +111,16 @@ public function getDatabaseName() /** * Returns information for all collections in this database. * - * @see http://docs.mongodb.org/manual/reference/command/listCollections/ + * @see ListCollections::__construct() for supported options * @param array $options * @return CollectionInfoIterator */ public function listCollections(array $options = array()) { - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); - $server = $this->manager->selectServer($readPreference); + $operation = new ListCollections($this->databaseName, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return (FeatureDetection::isSupported($server, FeatureDetection::API_LISTCOLLECTIONS_CMD)) - ? $this->listCollectionsCommand($server, $options) - : $this->listCollectionsLegacy($server, $options); + return $operation->execute($server); } /** @@ -145,58 +142,4 @@ public function selectCollection($collectionName, WriteConcern $writeConcern = n return new Collection($this->manager, $namespace, $writeConcern, $readPreference); } - - /** - * Returns information for all collections in this database using the - * listCollections command. - * - * @param Server $server - * @param array $options - * @return CollectionInfoCommandIterator - */ - private function listCollectionsCommand(Server $server, array $options = array()) - { - $command = new Command(array('listCollections' => 1) + $options); - $cursor = $server->executeCommand($this->databaseName, $command); - $cursor->setTypeMap(array('document' => 'array')); - - return new CollectionInfoCommandIterator($cursor); - } - - /** - * Returns information for all collections in this database by querying the - * "system.namespaces" collection (MongoDB <2.8). - * - * @param Server $server - * @param array $options - * @return CollectionInfoLegacyIterator - * @throws InvalidArgumentException if the filter option is neither an array - * nor object, or if filter.name is not a - * string. - */ - private function listCollectionsLegacy(Server $server, array $options = array()) - { - $filter = array_key_exists('filter', $options) ? $options['filter'] : array(); - - if ( ! is_array($filter) && ! is_object($filter)) { - throw new InvalidArgumentException(sprintf('Expected filter to be array or object, %s given', gettype($filter))); - } - - if (array_key_exists('name', (array) $filter)) { - $filter = (array) $filter; - - if ( ! is_string($filter['name'])) { - throw new InvalidArgumentException(sprintf('Filter "name" must be a string for MongoDB <2.8, %s given', gettype($filter['name']))); - } - - $filter['name'] = $this->databaseName . '.' . $filter['name']; - } - - $namespace = $this->databaseName . '.system.namespaces'; - $query = new Query($filter); - $cursor = $server->executeQuery($namespace, $query); - $cursor->setTypeMap(array('document' => 'array')); - - return new CollectionInfoLegacyIterator($cursor); - } } diff --git a/src/Operation/ListCollections.php b/src/Operation/ListCollections.php new file mode 100644 index 000000000..40a2b3cb6 --- /dev/null +++ b/src/Operation/ListCollections.php @@ -0,0 +1,122 @@ +databaseName = (string) $databaseName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return CollectionInfoIterator + */ + public function execute(Server $server) + { + return \MongoDB\server_supports_feature($server, self::$wireVersionForCommand) + ? $this->executeCommand($server) + : $this->executeLegacy($server); + } + + /** + * Returns information for all collections in this database using the + * listCollections command. + * + * @param Server $server + * @return CollectionInfoCommandIterator + */ + private function executeCommand(Server $server) + { + $cmd = array('listCollections' => 1); + + if ( ! empty($this->options['filter'])) { + $cmd['filter'] = (object) $this->options['filter']; + } + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); + $cursor->setTypeMap(array('document' => 'array')); + + return new CollectionInfoCommandIterator($cursor); + } + + /** + * Returns information for all collections in this database by querying the + * "system.namespaces" collection (MongoDB <3.0). + * + * @param Server $server + * @return CollectionInfoLegacyIterator + * @throws InvalidArgumentException if filter.name is not a string. + */ + private function executeLegacy(Server $server) + { + $filter = empty($this->options['filter']) ? array() : (array) $this->options['filter']; + + if (array_key_exists('name', $filter)) { + if ( ! is_string($filter['name'])) { + throw new InvalidArgumentTypeException('filter name for MongoDB <3.0', $filter['name'], 'string'); + } + + $filter['name'] = $this->databaseName . '.' . $filter['name']; + } + + $options = isset($this->options['maxTimeMS']) + ? array('modifiers' => array('$maxTimeMS' => $this->options['maxTimeMS'])) + : array(); + + $cursor = $server->executeQuery($this->databaseName . '.system.namespaces', new Query($filter, $options)); + $cursor->setTypeMap(array('document' => 'array')); + + return new CollectionInfoLegacyIterator($cursor); + } +} From 6dc7d31645b2571ce4ad14f6c27d75833e9a1273 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:13:49 -0400 Subject: [PATCH 13/24] Extract Collection::listIndexes() to an operation class --- src/Collection.php | 48 ++-------------- src/Operation/ListIndexes.php | 104 ++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 src/Operation/ListIndexes.php diff --git a/src/Collection.php b/src/Collection.php index 23b0e23ce..ee4759bb4 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -13,7 +13,6 @@ use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnexpectedTypeException; use MongoDB\Model\IndexInfoIterator; -use MongoDB\Model\IndexInfoIteratorIterator; use MongoDB\Model\IndexInput; use MongoDB\Operation\Aggregate; use MongoDB\Operation\CreateIndexes; @@ -22,6 +21,7 @@ use MongoDB\Operation\FindOneAndDelete; use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndUpdate; +use MongoDB\Operation\ListIndexes; use Traversable; class Collection @@ -728,18 +728,15 @@ public function insertOne($document) /** * Returns information for all indexes for the collection. * - * @see http://docs.mongodb.org/manual/reference/command/listIndexes/ - * @see http://docs.mongodb.org/manual/reference/method/db.collection.getIndexes/ + * @see ListIndexes::__construct() for supported options * @return IndexInfoIterator */ - public function listIndexes() + public function listIndexes(array $options = array()) { - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); - $server = $this->manager->selectServer($readPreference); + $operation = new ListIndexes($this->dbname, $this->collname, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return (FeatureDetection::isSupported($server, FeatureDetection::API_LISTINDEXES_CMD)) - ? $this->listIndexesCommand($server) - : $this->listIndexesLegacy($server); + return $operation->execute($server); } /** @@ -904,37 +901,4 @@ protected function _update($filter, $update, $options) $bulk->update($filter, $update, $options); return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc); } - - /** - * Returns information for all indexes for this collection using the - * listIndexes command. - * - * @see http://docs.mongodb.org/manual/reference/command/listIndexes/ - * @param Server $server - * @return IndexInfoIteratorIterator - */ - private function listIndexesCommand(Server $server) - { - $command = new Command(array('listIndexes' => $this->collname)); - $cursor = $server->executeCommand($this->dbname, $command); - $cursor->setTypeMap(array('document' => 'array')); - - return new IndexInfoIteratorIterator($cursor); - } - - /** - * Returns information for all indexes for this collection by querying the - * "system.indexes" collection (MongoDB <2.8). - * - * @param Server $server - * @return IndexInfoIteratorIterator - */ - private function listIndexesLegacy(Server $server) - { - $query = new Query(array('ns' => $this->ns)); - $cursor = $server->executeQuery($this->dbname . '.system.indexes', $query); - $cursor->setTypeMap(array('document' => 'array')); - - return new IndexInfoIteratorIterator($cursor); - } } diff --git a/src/Operation/ListIndexes.php b/src/Operation/ListIndexes.php new file mode 100644 index 000000000..1e223a010 --- /dev/null +++ b/src/Operation/ListIndexes.php @@ -0,0 +1,104 @@ +databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return IndexInfoIterator + */ + public function execute(Server $server) + { + return \MongoDB\server_supports_feature($server, self::$wireVersionForCommand) + ? $this->executeCommand($server) + : $this->executeLegacy($server); + } + + /** + * Returns information for all indexes for this collection using the + * listIndexes command. + * + * @param Server $server + * @return IndexInfoIteratorIterator + */ + private function executeCommand(Server $server) + { + $cmd = array('listIndexes' => $this->collectionName); + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); + $cursor->setTypeMap(array('document' => 'array')); + + return new IndexInfoIteratorIterator($cursor); + } + + /** + * Returns information for all indexes for this collection by querying the + * "system.indexes" collection (MongoDB <3.0). + * + * @param Server $server + * @return IndexInfoIteratorIterator + */ + private function executeLegacy(Server $server) + { + $filter = array('ns' => $this->databaseName . '.' . $this->collectionName); + + $options = isset($this->options['maxTimeMS']) + ? array('modifiers' => array('$maxTimeMS' => $this->options['maxTimeMS'])) + : array(); + + $cursor = $server->executeQuery($this->databaseName . '.system.indexes', new Query($filter, $options)); + $cursor->setTypeMap(array('document' => 'array')); + + return new IndexInfoIteratorIterator($cursor); + } +} From c53cb01ac8a9f41dee10af5c5407b187ceb46927 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:15:03 -0400 Subject: [PATCH 14/24] Extract DropDatabase operation class --- src/Client.php | 11 ++++---- src/Database.php | 10 +++---- src/Operation/DropDatabase.php | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 src/Operation/DropDatabase.php diff --git a/src/Client.php b/src/Client.php index ed7b6ebe8..4031ed11e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,6 +8,7 @@ use MongoDB\Driver\ReadPreference; use MongoDB\Driver\WriteConcern; use MongoDB\Model\DatabaseInfoIterator; +use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListDatabases; class Client @@ -36,17 +37,15 @@ public function __construct($uri, array $options = array(), array $driverOptions /** * Drop a database. * - * @see http://docs.mongodb.org/manual/reference/command/dropDatabase/ * @param string $databaseName - * @return Cursor + * @return object Command result document */ public function dropDatabase($databaseName) { - $databaseName = (string) $databaseName; - $command = new Command(array('dropDatabase' => 1)); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropDatabase($databaseName); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($databaseName, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Database.php b/src/Database.php index d016279e6..8cc8a1f1e 100644 --- a/src/Database.php +++ b/src/Database.php @@ -12,6 +12,7 @@ use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Model\CollectionInfoIterator; +use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListCollections; class Database @@ -71,15 +72,14 @@ public function createCollection($collectionName, array $options = array()) /** * Drop this database. * - * @see http://docs.mongodb.org/manual/reference/command/dropDatabase/ - * @return Cursor + * @return object Command result document */ public function drop() { - $command = new Command(array('dropDatabase' => 1)); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropDatabase($this->databaseName); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->databaseName, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Operation/DropDatabase.php b/src/Operation/DropDatabase.php new file mode 100644 index 000000000..4c4313d3e --- /dev/null +++ b/src/Operation/DropDatabase.php @@ -0,0 +1,50 @@ +databaseName = (string) $databaseName; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return object Command result document + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, new Command(array('dropDatabase' => 1))); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + return $result; + } +} From d2fe8b39646041eb47f2422746bae65cfc6adf8c Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:16:12 -0400 Subject: [PATCH 15/24] Extra DropCollection operation class --- src/Collection.php | 10 +++--- src/Database.php | 11 +++---- src/Operation/DropCollection.php | 52 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/Operation/DropCollection.php diff --git a/src/Collection.php b/src/Collection.php index ee4759bb4..496e1d97f 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -18,6 +18,7 @@ use MongoDB\Operation\CreateIndexes; use MongoDB\Operation\Count; use MongoDB\Operation\Distinct; +use MongoDB\Operation\DropCollection; use MongoDB\Operation\FindOneAndDelete; use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndUpdate; @@ -352,15 +353,14 @@ public function distinct($fieldName, array $filter = array(), array $options = a /** * Drop this collection. * - * @see http://docs.mongodb.org/manual/reference/command/drop/ - * @return Cursor + * @return object Command result document */ public function drop() { - $command = new Command(array('drop' => $this->collname)); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropCollection($this->dbname, $this->collname); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->dbname, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Database.php b/src/Database.php index 8cc8a1f1e..a169f4d63 100644 --- a/src/Database.php +++ b/src/Database.php @@ -12,6 +12,7 @@ use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Model\CollectionInfoIterator; +use MongoDB\Operation\DropCollection; use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListCollections; @@ -85,17 +86,15 @@ public function drop() /** * Drop a collection within this database. * - * @see http://docs.mongodb.org/manual/reference/command/drop/ * @param string $collectionName - * @return Cursor + * @return object Command result document */ public function dropCollection($collectionName) { - $collectionName = (string) $collectionName; - $command = new Command(array('drop' => $collectionName)); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropCollection($this->databaseName, $collectionName); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->databaseName, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Operation/DropCollection.php b/src/Operation/DropCollection.php new file mode 100644 index 000000000..437ccfcb1 --- /dev/null +++ b/src/Operation/DropCollection.php @@ -0,0 +1,52 @@ +databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return object Command result document + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, new Command(array('drop' => $this->collectionName))); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + return $result; + } +} From 3c28b7d914a09a064c416ab5b4b4fd32edf464c2 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:17:45 -0400 Subject: [PATCH 16/24] Extract Database::createCollection() to an operation class --- src/Database.php | 13 ++- src/Operation/CreateCollection.php | 139 +++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/Operation/CreateCollection.php diff --git a/src/Database.php b/src/Database.php index a169f4d63..7724d9777 100644 --- a/src/Database.php +++ b/src/Database.php @@ -12,6 +12,7 @@ use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Model\CollectionInfoIterator; +use MongoDB\Operation\CreateCollection; use MongoDB\Operation\DropCollection; use MongoDB\Operation\DropDatabase; use MongoDB\Operation\ListCollections; @@ -55,19 +56,17 @@ public function __toString() /** * Create a new collection explicitly. * - * @see http://docs.mongodb.org/manual/reference/command/create/ - * @see http://docs.mongodb.org/manual/reference/method/db.createCollection/ + * @see CreateCollection::__construct() for supported options * @param string $collectionName * @param array $options - * @return Cursor + * @return object Command result document */ public function createCollection($collectionName, array $options = array()) { - $collectionName = (string) $collectionName; - $command = new Command(array('create' => $collectionName) + $options); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new CreateCollection($this->databaseName, $collectionName, $options); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->databaseName, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Operation/CreateCollection.php b/src/Operation/CreateCollection.php new file mode 100644 index 000000000..8b71d388c --- /dev/null +++ b/src/Operation/CreateCollection.php @@ -0,0 +1,139 @@ +databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * For servers < 2.6, this will actually perform an insert operation on the + * database's "system.indexes" collection. + * + * @see Executable::execute() + * @param Server $server + * @return object Command result document + */ + public function execute(Server $server) + { + $cursor = $server->executeCommand($this->databaseName, $this->createCommand()); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + return $result; + } + + /** + * Create the create command. + * + * @return Command + */ + private function createCommand() + { + $cmd = array('create' => $this->collectionName); + + foreach (array('autoIndexId', 'capped', 'flags', 'max', 'maxTimeMS', 'size') as $option) { + if (isset($this->options[$option])) { + $cmd[$option] = $this->options[$option]; + } + } + + if ( ! empty($this->options['storageEngine'])) { + $cmd['storageEngine'] = (object) $this->options['storageEngine']; + } + + return new Command($cmd); + } +} From d070cf84304d56b0b52df96646138efad0ac3cf5 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:19:14 -0400 Subject: [PATCH 17/24] Extract DropIndexes operation class --- src/Collection.php | 27 ++++++-------- src/Operation/DropIndexes.php | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 src/Operation/DropIndexes.php diff --git a/src/Collection.php b/src/Collection.php index 496e1d97f..1961ac7ca 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -19,6 +19,7 @@ use MongoDB\Operation\Count; use MongoDB\Operation\Distinct; use MongoDB\Operation\DropCollection; +use MongoDB\Operation\DropIndexes; use MongoDB\Operation\FindOneAndDelete; use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndUpdate; @@ -366,43 +367,35 @@ public function drop() /** * Drop a single index in the collection. * - * @see http://docs.mongodb.org/manual/reference/command/dropIndexes/ - * @see http://docs.mongodb.org/manual/reference/method/db.collection.dropIndex/ - * @param string $indexName - * @return Cursor + * @param string $indexName Index name + * @return object Command result document * @throws InvalidArgumentException if $indexName is an empty string or "*" */ public function dropIndex($indexName) { $indexName = (string) $indexName; - if ($indexName === '') { - throw new InvalidArgumentException('Index name cannot be empty'); - } - if ($indexName === '*') { throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes'); } - $command = new Command(array('dropIndexes' => $this->collname, 'index' => $indexName)); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropIndexes($this->dbname, $this->collname, $indexName); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->dbname, $command, $readPreference); + return $operation->execute($server); } /** * Drop all indexes in the collection. * - * @see http://docs.mongodb.org/manual/reference/command/dropIndexes/ - * @see http://docs.mongodb.org/manual/reference/method/db.collection.dropIndexes/ - * @return Cursor + * @return object Command result document */ public function dropIndexes() { - $command = new Command(array('dropIndexes' => $this->collname, 'index' => '*')); - $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $operation = new DropIndexes($this->dbname, $this->collname, '*'); + $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); - return $this->manager->executeCommand($this->dbname, $command, $readPreference); + return $operation->execute($server); } /** diff --git a/src/Operation/DropIndexes.php b/src/Operation/DropIndexes.php new file mode 100644 index 000000000..78ab4de6e --- /dev/null +++ b/src/Operation/DropIndexes.php @@ -0,0 +1,67 @@ +databaseName = (string) $databaseName; + $this->collectionName = (string) $collectionName; + $this->indexName = $indexName; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return object Command result document + */ + public function execute(Server $server) + { + $cmd = array( + 'dropIndexes' => $this->collectionName, + 'index' => $this->indexName, + ); + + $cursor = $server->executeCommand($this->databaseName, new Command($cmd)); + $result = current($cursor->toArray()); + + if (empty($result['ok'])) { + throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); + } + + return $result; + } +} From c257a955ec0602782675becf92202e8916f142f5 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:19:46 -0400 Subject: [PATCH 18/24] assertCommandSucceeded() now accepts a result document --- tests/FunctionalTestCase.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index cdf522e5c..a08f2f529 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -27,9 +27,12 @@ protected function assertCollectionCount($namespace, $count) $this->assertEquals($count, $document['n']); } - protected function assertCommandSucceeded(Cursor $cursor) + protected function assertCommandSucceeded($document) { - $document = current($cursor->toArray()); + if (is_object($document)) { + $document = get_object_vars($document); + } + $this->assertArrayHasKey('ok', $document); $this->assertEquals(1, $document['ok']); } From 9b4d0ef2b27025a08e76d4282cb9eba155f65b16 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 16:25:04 -0400 Subject: [PATCH 19/24] FeatureDetection utility class is obsolete --- src/FeatureDetection.php | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/FeatureDetection.php diff --git a/src/FeatureDetection.php b/src/FeatureDetection.php deleted file mode 100644 index 418bef75d..000000000 --- a/src/FeatureDetection.php +++ /dev/null @@ -1,34 +0,0 @@ -getInfo(); - $maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0; - $minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0; - - return ($minWireVersion <= $feature && $maxWireVersion >= $feature); - } -} From c0f2c1628cf36f7beefe612ef06e8792a965c482 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 16 Jun 2015 17:19:45 -0400 Subject: [PATCH 20/24] Remove unused Collection constants and methods --- src/Collection.php | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/Collection.php b/src/Collection.php index 1961ac7ca..8d0d21871 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -43,9 +43,6 @@ class Collection //self::QUERY_FLAG_TAILABLE_CURSOR | self::QUERY_FLAG_AWAIT_DATA; const CURSOR_TYPE_TAILABLE_AWAIT = 0x22; - const FIND_ONE_AND_RETURN_BEFORE = 0x01; - const FIND_ONE_AND_RETURN_AFTER = 0x02; - protected $manager; protected $ns; protected $wc; @@ -838,19 +835,6 @@ final protected function _delete($filter, $limit = 1) return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc); } - /** - * Internal helper for throwing an exception with error message - * @internal - */ - final protected function _generateCommandException($doc) - { - if ($doc["errmsg"]) { - return new RuntimeException($doc["errmsg"]); - } - var_dump($doc); - return new RuntimeException("FIXME: Unknown error"); - } - /** * Constructs the Query Wire Protocol field 'flags' based on $options * provided to other helpers @@ -871,17 +855,6 @@ final protected function _opQueryFlags($options) return $flags; } - /** - * Internal helper for running a command - * @internal - */ - final protected function _runCommand($dbname, array $cmd, ReadPreference $rp = null) - { - //var_dump(\BSON\toJSON(\BSON\fromArray($cmd))); - $command = new Command($cmd); - return $this->manager->executeCommand($dbname, $command, $rp); - } - /** * Internal helper for replacing/updating one/many documents * @internal From c5a0964fef93f13138e5d54498fb3d1d3fdd4eed Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 17 Jun 2015 13:49:35 -0400 Subject: [PATCH 21/24] Don't assume document PHP type mapping in FunctionalTestCase --- tests/FunctionalTestCase.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index a08f2f529..94c98e2f9 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -21,8 +21,9 @@ protected function assertCollectionCount($namespace, $count) list($databaseName, $collectionName) = explode('.', $namespace, 2); $cursor = $this->manager->executeCommand($databaseName, new Command(array('count' => $collectionName))); - + $cursor->setTypeMap(array('document' => 'array')); $document = current($cursor->toArray()); + $this->assertArrayHasKey('n', $document); $this->assertEquals($count, $document['n']); } @@ -45,6 +46,7 @@ protected function getServerVersion(ReadPreference $readPreference = null) $readPreference ?: new ReadPreference(ReadPreference::RP_PRIMARY) ); + $cursor->setTypeMap(array('document' => 'array')); $document = current($cursor->toArray()); return $document['version']; From fb7cb1b6be96d38efdfb4765a78d0fc39e7f1b59 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 17 Jun 2015 14:03:32 -0400 Subject: [PATCH 22/24] Older servers may return count "n" as a float --- src/Operation/Count.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Operation/Count.php b/src/Operation/Count.php index 8eb93b00e..137fd47d9 100644 --- a/src/Operation/Count.php +++ b/src/Operation/Count.php @@ -91,11 +91,12 @@ public function execute(Server $server) throw new RuntimeException(isset($result['errmsg']) ? $result['errmsg'] : 'Unknown error'); } - if ( ! isset($result['n']) || ! is_integer($result['n'])) { - throw new UnexpectedValueException('count command did not return an "n" integer'); + // Older server versions may return a float + if ( ! isset($result['n']) || ! (is_integer($result['n']) || is_float($result['n']))) { + throw new UnexpectedValueException('count command did not return an "n" value'); } - return $result['n']; + return (integer) $result['n']; } /** From 26078fe2a771bc438c788f394fe3cd24edd2558e Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 17 Jun 2015 16:49:32 -0400 Subject: [PATCH 23/24] Relax assertion in AggregateFunctionalTest --- tests/Collection/CrudSpec/AggregateFunctionalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Collection/CrudSpec/AggregateFunctionalTest.php b/tests/Collection/CrudSpec/AggregateFunctionalTest.php index 744088ec8..183cda284 100644 --- a/tests/Collection/CrudSpec/AggregateFunctionalTest.php +++ b/tests/Collection/CrudSpec/AggregateFunctionalTest.php @@ -37,7 +37,7 @@ public function testAggregateWithMultipleStages() ); // Use iterator_to_array() here since aggregate() may return an ArrayIterator - $this->assertSame($expected, iterator_to_array($cursor)); + $this->assertEquals($expected, iterator_to_array($cursor)); } public function testAggregateWithOut() @@ -64,7 +64,7 @@ public function testAggregateWithOut() array('_id' => 3, 'x' => 33), ); - $this->assertSame($expected, $outputCollection->find()->toArray()); + $this->assertEquals($expected, $outputCollection->find()->toArray()); // Manually clean up our output collection $this->dropCollectionIfItExists($outputCollection); From cab405d64e1ded37d72b447785c34e58741cab80 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 17 Jun 2015 17:24:49 -0400 Subject: [PATCH 24/24] Aggregate should check server support before returning a cursor --- src/Operation/Aggregate.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Operation/Aggregate.php b/src/Operation/Aggregate.php index dbc1472c0..37621d34c 100644 --- a/src/Operation/Aggregate.php +++ b/src/Operation/Aggregate.php @@ -113,10 +113,12 @@ public function __construct($databaseName, $collectionName, array $pipeline, arr */ public function execute(Server $server) { - $command = $this->createCommand($server); + $isCursorSupported = \MongoDB\server_supports_feature($server, self::$wireVersionForCursor); + + $command = $this->createCommand($server, $isCursorSupported); $cursor = $server->executeCommand($this->databaseName, $command); - if ($this->options['useCursor']) { + if ($isCursorSupported && $this->options['useCursor']) { return $cursor; } @@ -139,10 +141,11 @@ function (stdClass $document) { return (array) $document; }, /** * Create the aggregate command. * - * @param Server $server + * @param Server $server + * @param boolean $isCursorSupported * @return Command */ - private function createCommand(Server $server) + private function createCommand(Server $server, $isCursorSupported) { $cmd = array( 'aggregate' => $this->collectionName, @@ -150,7 +153,7 @@ private function createCommand(Server $server) ); // Servers < 2.6 do not support any command options - if ( ! \MongoDB\server_supports_feature($server, self::$wireVersionForCursor)) { + if ( ! $isCursorSupported) { return new Command($cmd); }