Commit 2354e108 authored by Jeremy Mikola's avatar Jeremy Mikola

PHPLIB-110: Extract Find and FindOne operation classes

parent 83d7ae2f
...@@ -6,7 +6,6 @@ use MongoDB\Driver\BulkWrite; ...@@ -6,7 +6,6 @@ use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command; use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor; use MongoDB\Driver\Cursor;
use MongoDB\Driver\Manager; use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;
use MongoDB\Driver\ReadPreference; use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server; use MongoDB\Driver\Server;
use MongoDB\Driver\WriteConcern; use MongoDB\Driver\WriteConcern;
...@@ -20,6 +19,8 @@ use MongoDB\Operation\Count; ...@@ -20,6 +19,8 @@ use MongoDB\Operation\Count;
use MongoDB\Operation\Distinct; use MongoDB\Operation\Distinct;
use MongoDB\Operation\DropCollection; use MongoDB\Operation\DropCollection;
use MongoDB\Operation\DropIndexes; use MongoDB\Operation\DropIndexes;
use MongoDB\Operation\Find;
use MongoDB\Operation\FindOne;
use MongoDB\Operation\FindOneAndDelete; use MongoDB\Operation\FindOneAndDelete;
use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndReplace;
use MongoDB\Operation\FindOneAndUpdate; use MongoDB\Operation\FindOneAndUpdate;
...@@ -29,20 +30,6 @@ use Traversable; ...@@ -29,20 +30,6 @@ use Traversable;
class Collection class Collection
{ {
/* {{{ consts & vars */ /* {{{ consts & vars */
const QUERY_FLAG_TAILABLE_CURSOR = 0x02;
const QUERY_FLAG_SLAVE_OKAY = 0x04;
const QUERY_FLAG_OPLOG_REPLY = 0x08;
const QUERY_FLAG_NO_CURSOR_TIMEOUT = 0x10;
const QUERY_FLAG_AWAIT_DATA = 0x20;
const QUERY_FLAG_EXHAUST = 0x40;
const QUERY_FLAG_PARTIAL = 0x80;
const CURSOR_TYPE_NON_TAILABLE = 0x00;
const CURSOR_TYPE_TAILABLE = self::QUERY_FLAG_TAILABLE_CURSOR;
//self::QUERY_FLAG_TAILABLE_CURSOR | self::QUERY_FLAG_AWAIT_DATA;
const CURSOR_TYPE_TAILABLE_AWAIT = 0x22;
protected $manager; protected $manager;
protected $ns; protected $ns;
protected $wc; protected $wc;
...@@ -396,50 +383,37 @@ class Collection ...@@ -396,50 +383,37 @@ class Collection
} }
/** /**
* Performs a find (query) on the collection * Finds documents matching the query.
* *
* @see Find::__construct() for supported options
* @see http://docs.mongodb.org/manual/core/read-operations-introduction/ * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
* @see Collection::getFindOptions() for supported $options * @param array $filter Query by which to filter documents
* * @param array $options Additional options
* @param array $filter The find query to execute
* @param array $options Additional options
* @return Cursor * @return Cursor
*/ */
public function find(array $filter = array(), array $options = array()) public function find(array $filter = array(), array $options = array())
{ {
$options = array_merge($this->getFindOptions(), $options); $operation = new Find($this->dbname, $this->collname, $filter, $options);
$server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
$query = $this->_buildQuery($filter, $options);
$cursor = $this->manager->executeQuery($this->ns, $query, $this->rp);
return $cursor; return $operation->execute($server);
} }
/** /**
* Performs a find (query) on the collection, returning at most one result * Finds a single document matching the query.
* *
* @see FindOne::__construct() for supported options
* @see http://docs.mongodb.org/manual/core/read-operations-introduction/ * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
* @see Collection::getFindOptions() for supported $options
*
* @param array $filter The find query to execute * @param array $filter The find query to execute
* @param array $options Additional options * @param array $options Additional options
* @return array|false The matched document, or false on failure * @return object|null
*/ */
public function findOne(array $filter = array(), array $options = array()) public function findOne(array $filter = array(), array $options = array())
{ {
$options = array_merge($this->getFindOptions(), array("limit" => 1), $options); $operation = new FindOne($this->dbname, $this->collname, $filter, $options);
$server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
$query = $this->_buildQuery($filter, $options);
$cursor = $this->manager->executeQuery($this->ns, $query, $this->rp);
$array = iterator_to_array($cursor);
if ($array) {
return $array[0];
}
return false; return $operation->execute($server);
} }
/** /**
...@@ -536,107 +510,6 @@ class Collection ...@@ -536,107 +510,6 @@ class Collection
return $this->dbname; return $this->dbname;
} }
/**
* Retrieves all find options with their default values.
*
* @return array of Collection::find() options
*/
public function getFindOptions()
{
return array(
/**
* Get partial results from a mongos if some shards are down (instead of throwing an error).
*
* @see http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/#op-query
*/
"allowPartialResults" => false,
/**
* The number of documents to return per batch.
*
* @see http://docs.mongodb.org/manual/reference/method/cursor.batchSize/
*/
"batchSize" => 101,
/**
* Attaches a comment to the query. If $comment also exists
* in the modifiers document, the comment field overwrites $comment.
*
* @see http://docs.mongodb.org/manual/reference/operator/meta/comment/
*/
"comment" => "",
/**
* Indicates the type of cursor to use. This value includes both
* the tailable and awaitData options.
* The default is Collection::CURSOR_TYPE_NON_TAILABLE.
*
* @see http://docs.mongodb.org/manual/reference/operator/meta/comment/
*/
"cursorType" => self::CURSOR_TYPE_NON_TAILABLE,
/**
* The maximum number of documents to return.
*
* @see http://docs.mongodb.org/manual/reference/method/cursor.limit/
*/
"limit" => 0,
/**
* The maximum amount of time to allow the query to run. If $maxTimeMS also exists
* in the modifiers document, the maxTimeMS field overwrites $maxTimeMS.
*
* @see http://docs.mongodb.org/manual/reference/operator/meta/maxTimeMS/
*/
"maxTimeMS" => 0,
/**
* Meta-operators modifying the output or behavior of a query.
*
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
*/
"modifiers" => array(),
/**
* The server normally times out idle cursors after an inactivity period (10 minutes)
* to prevent excess memory use. Set this option to prevent that.
*
* @see http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/#op-query
*/
"noCursorTimeout" => false,
/**
* Internal replication use only - driver should not set
*
* @see http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/#op-query
* @internal
*/
"oplogReplay" => false,
/**
* Limits the fields to return for all matching documents.
*
* @see http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results/
*/
"projection" => array(),
/**
* The number of documents to skip before returning.
*
* @see http://docs.mongodb.org/manual/reference/method/cursor.skip/
*/
"skip" => 0,
/**
* The order in which to return matching documents. If $orderby also exists
* in the modifiers document, the sort field overwrites $orderby.
*
* @see http://docs.mongodb.org/manual/reference/method/cursor.sort/
*/
"sort" => array(),
);
}
/** /**
* Return the collection namespace. * Return the collection namespace.
* *
...@@ -793,35 +666,6 @@ class Collection ...@@ -793,35 +666,6 @@ class Collection
return new UpdateResult($wr); return new UpdateResult($wr);
} }
/**
* Helper to build a Query object
*
* @param array $filter the query document
* @param array $options query/protocol options
* @return Query
* @internal
*/
final protected function _buildQuery($filter, $options)
{
if ($options["comment"]) {
$options["modifiers"]['$comment'] = $options["comment"];
}
if ($options["maxTimeMS"]) {
$options["modifiers"]['$maxTimeMS'] = $options["maxTimeMS"];
}
if ($options["sort"]) {
$options['$orderby'] = $options["sort"];
}
$flags = $this->_opQueryFlags($options);
$options["cursorFlags"] = $flags;
$query = new Query($filter, $options);
return $query;
}
/** /**
* Internal helper for delete one/many documents * Internal helper for delete one/many documents
* @internal * @internal
...@@ -835,26 +679,6 @@ class Collection ...@@ -835,26 +679,6 @@ class Collection
return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc); return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc);
} }
/**
* Constructs the Query Wire Protocol field 'flags' based on $options
* provided to other helpers
*
* @param array $options
* @return integer OP_QUERY Wire Protocol flags
* @internal
*/
final protected function _opQueryFlags($options)
{
$flags = 0;
$flags |= $options["allowPartialResults"] ? self::QUERY_FLAG_PARTIAL : 0;
$flags |= $options["cursorType"] ? $options["cursorType"] : 0;
$flags |= $options["oplogReplay"] ? self::QUERY_FLAG_OPLOG_REPLY: 0;
$flags |= $options["noCursorTimeout"] ? self::QUERY_FLAG_NO_CURSOR_TIMEOUT : 0;
return $flags;
}
/** /**
* Internal helper for replacing/updating one/many documents * Internal helper for replacing/updating one/many documents
* @internal * @internal
......
<?php
namespace MongoDB\Operation;
use MongoDB\Driver\Query;
use MongoDB\Driver\Server;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\InvalidArgumentTypeException;
use MongoDB\Exception\RuntimeException;
use MongoDB\Exception\UnexpectedValueException;
/**
* Operation for the find command.
*
* @api
* @see MongoDB\Collection::find()
* @see http://docs.mongodb.org/manual/tutorial/query-documents/
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
*/
class Find implements Executable
{
const NON_TAILABLE = 1;
const TAILABLE = 2;
const TAILABLE_AWAIT = 3;
private $databaseName;
private $collectionName;
private $filter;
private $options;
/**
* Constructs a find command.
*
* Supported options:
*
* * allowPartialResults (boolean): Get partial results from a mongos if
* some shards are inaccessible (instead of throwing an error).
*
* * batchSize (integer): The number of documents to return per batch.
*
* * comment (string): Attaches a comment to the query. If "$comment" also
* exists in the modifiers document, this option will take precedence.
*
* * cursorType (enum): Indicates the type of cursor to use. Must be either
* NON_TAILABLE, TAILABLE, or TAILABLE_AWAIT. The default is
* NON_TAILABLE.
*
* * limit (integer): The maximum number of documents to return.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run. If "$maxTimeMS" also exists in the modifiers document, this
* option will take precedence.
*
* * modifiers (document): Meta-operators modifying the output or behavior
* of a query.
*
* * noCursorTimeout (boolean): The server normally times out idle cursors
* after an inactivity period (10 minutes) to prevent excess memory use.
* Set this option to prevent that.
*
* * oplogReplay (boolean): Internal replication use only. The driver
* should not set this.
*
* * projection (document): Limits the fields to return for the matching
* document.
*
* * skip (integer): The number of documents to skip before returning.
*
* * sort (document): The order in which to return matching documents. If
* "$orderby" also exists in the modifiers document, this option will
* take precedence.
*
* @param string $databaseName Database name
* @param string $collectionName Collection name
* @param array|object $filter Query by which to filter documents
* @param array $options Command options
* @throws InvalidArgumentException
*/
public function __construct($databaseName, $collectionName, $filter, array $options = array())
{
if ( ! is_array($filter) && ! is_object($filter)) {
throw new InvalidArgumentTypeException('$filter', $filter, 'array or object');
}
if (isset($options['allowPartialResults']) && ! is_bool($options['allowPartialResults'])) {
throw new InvalidArgumentTypeException('"allowPartialResults" option', $options['allowPartialResults'], 'boolean');
}
if (isset($options['batchSize']) && ! is_integer($options['batchSize'])) {
throw new InvalidArgumentTypeException('"batchSize" option', $options['batchSize'], 'integer');
}
if (isset($options['comment']) && ! is_string($options['comment'])) {
throw new InvalidArgumentTypeException('"comment" option', $options['comment'], 'comment');
}
if (isset($options['cursorType'])) {
if ( ! is_integer($options['cursorType'])) {
throw new InvalidArgumentTypeException('"cursorType" option', $options['cursorType'], 'integer');
}
if ($options['cursorType'] !== self::NON_TAILABLE &&
$options['cursorType'] !== self::TAILABLE &&
$options['cursorType'] !== self::TAILABLE_AWAIT) {
throw new InvalidArgumentException('Invalid value for "cursorType" option: ' . $options['cursorType']);
}
}
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['modifiers']) && ! is_array($options['modifiers']) && ! is_object($options['modifiers'])) {
throw new InvalidArgumentTypeException('"modifiers" option', $options['modifiers'], 'array or object');
}
if (isset($options['noCursorTimeout']) && ! is_bool($options['noCursorTimeout'])) {
throw new InvalidArgumentTypeException('"noCursorTimeout" option', $options['noCursorTimeout'], 'boolean');
}
if (isset($options['oplogReplay']) && ! is_bool($options['oplogReplay'])) {
throw new InvalidArgumentTypeException('"oplogReplay" option', $options['oplogReplay'], 'boolean');
}
if (isset($options['projection']) && ! is_array($options['projection']) && ! is_object($options['projection'])) {
throw new InvalidArgumentTypeException('"projection" option', $options['projection'], 'array or object');
}
if (isset($options['skip']) && ! is_integer($options['skip'])) {
throw new InvalidArgumentTypeException('"skip" option', $options['skip'], 'integer');
}
if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) {
throw new InvalidArgumentTypeException('"sort" option', $options['sort'], 'array or object');
}
$this->databaseName = (string) $databaseName;
$this->collectionName = (string) $collectionName;
$this->filter = $filter;
$this->options = $options;
}
/**
* Execute the operation.
*
* @see Executable::execute()
* @param Server $server
* @return Cursor
*/
public function execute(Server $server)
{
return $server->executeQuery($this->databaseName . '.' . $this->collectionName, $this->createQuery());
}
/**
* Create the find query.
*
* @return Query
*/
private function createQuery()
{
$options = array();
if ( ! empty($this->options['allowPartialResults'])) {
$options['partial'] = true;
}
if (isset($options['cursorType'])) {
if ($options['cursorType'] === self::TAILABLE) {
$options['tailable'] = true;
}
if ($options['cursorType'] === self::TAILABLE_AWAIT) {
$options['tailable'] = true;
$options['awaitData'] = true;
}
}
foreach (array('batchSize', 'limit', 'skip', 'sort', 'noCursorTimeout', 'oplogReplay', 'projection') as $option) {
if (isset($this->options[$option])) {
$options[$option] = $this->options[$option];
}
}
$modifiers = empty($this->options['modifiers']) ? array() : (array) $this->options['modifiers'];
if (isset($options['comment'])) {
$modifiers['$comment'] = $options['comment'];
}
if (isset($options['maxTimeMS'])) {
$modifiers['$maxTimeMS'] = $options['maxTimeMS'];
}
if ( ! empty($modifiers)) {
$options['modifiers'] = $modifiers;
}
return new Query($this->filter, $options);
}
}
<?php
namespace MongoDB\Operation;
use MongoDB\Driver\Server;
/**
* Operation for finding a single document with the find command.
*
* @api
* @see MongoDB\Collection::findOne()
* @see http://docs.mongodb.org/manual/tutorial/query-documents/
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
*/
class FindOne implements Executable
{
private $find;
/**
* Constructs a find command for finding a single document.
*
* Supported options:
*
* * comment (string): Attaches a comment to the query. If "$comment" also
* exists in the modifiers document, this option will take precedence.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run. If "$maxTimeMS" also exists in the modifiers document, this
* option will take precedence.
*
* * modifiers (document): Meta-operators modifying the output or behavior
* of a query.
*
* * projection (document): Limits the fields to return for the matching
* document.
*
* * skip (integer): The number of documents to skip before returning.
*
* * sort (document): The order in which to return matching documents. If
* "$orderby" also exists in the modifiers document, this option will
* take precedence.
*
* @param string $databaseName Database name
* @param string $collectionName Collection name
* @param array|object $filter Query by which to filter documents
* @param array $options Command options
* @throws InvalidArgumentException
*/
public function __construct($databaseName, $collectionName, $filter, array $options = array())
{
$this->find = new Find(
$databaseName,
$collectionName,
$filter,
array('limit' => -1) + $options
);
}
/**
* Execute the operation.
*
* @see Executable::execute()
* @param Server $server
* @return object|null
*/
public function execute(Server $server)
{
$cursor = $this->find->execute($server);
$document = current($cursor->toArray());
return ($document === false) ? null : $document;
}
}
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
namespace MongoDB\Tests\Collection; namespace MongoDB\Tests\Collection;
use MongoDB\Driver\BulkWrite;
/** /**
* Functional tests for the Collection class. * Functional tests for the Collection class.
*/ */
...@@ -16,4 +18,40 @@ class CollectionFunctionalTest extends FunctionalTestCase ...@@ -16,4 +18,40 @@ class CollectionFunctionalTest extends FunctionalTestCase
$this->assertCommandSucceeded($commandResult); $this->assertCommandSucceeded($commandResult);
$this->assertCollectionCount($this->getNamespace(), 0); $this->assertCollectionCount($this->getNamespace(), 0);
} }
public function testFindOne()
{
$this->createFixtures(5);
$filter = array('_id' => array('$lt' => 5));
$options = array(
'skip' => 1,
'sort' => array('x' => -1),
);
$expected = array('_id' => 3, 'x' => 33);
$this->assertSame($expected, $this->collection->findOne($filter, $options));
}
/**
* Create data fixtures.
*
* @param integer $n
*/
private function createFixtures($n)
{
$bulkWrite = new BulkWrite(true);
for ($i = 1; $i <= $n; $i++) {
$bulkWrite->insert(array(
'_id' => $i,
'x' => (integer) ($i . $i),
));
}
$result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
$this->assertEquals($n, $result->getInsertedCount());
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment