diff --git a/README.md b/README.md index 8ab21af..f929ec0 100755 --- a/README.md +++ b/README.md @@ -23,16 +23,28 @@ use com\mongodb\{MongoConnection, ObjectId}; use util\cmd\Console; $c= new MongoConnection('mongodb://localhost'); -$id= new ObjectId(...); +$id= new ObjectId('...'); // Find all documents -$cursor= $c->collection('test.products')->find(); +$cursor= $c->collection('test.products')->query(); // Find document with the specified ID -$cursor= $c->collection('test.products')->find($id); +$cursor= $c->collection('test.products')->query($id); // Find all documents with a name of "Test" -$cursor= $c->collection('test.products')->find(['name' => 'Test']); +$cursor= $c->collection('test.products')->query(['name' => 'Test']); + +// Use aggregation pipelines +$cursor= $c->collection('test.products')->query([ + ['$match' => ['color' => 'green', 'state' => 'ACTIVE']], + ['$lookup' => [ + 'from' => 'users', + 'localField' => 'owner.id', + 'foreignField' => '_id', + 'as' => 'owner', + ]], + ['$addFields' => ['owner' => ['$first' => '$owner']]], +]); foreach ($cursor as $document) { Console::writeLine('>> ', $document); @@ -62,7 +74,7 @@ use com\mongodb\{MongoConnection, ObjectId}; use util\cmd\Console; $c= new MongoConnection('mongodb://localhost'); -$id= new ObjectId(...); +$id= new ObjectId('...'); // Select a single document for updating by its ID $result= $c->collection('test.products')->update($id, ['$inc' => ['qty' => 1]]); @@ -102,7 +114,7 @@ use com\mongodb\{MongoConnection, ObjectId}; use util\cmd\Console; $c= new MongoConnection('mongodb://localhost'); -$id= new ObjectId(...); +$id= new ObjectId('...'); // Select a single document to be removed $result= $c->collection('test.products')->delete($id); diff --git a/src/main/php/com/mongodb/Collection.class.php b/src/main/php/com/mongodb/Collection.class.php index d48d489..d0a846f 100755 --- a/src/main/php/com/mongodb/Collection.class.php +++ b/src/main/php/com/mongodb/Collection.class.php @@ -9,6 +9,7 @@ * A collection inside a database. * * @test com.mongodb.unittest.CollectionTest + * @test com.mongodb.unittest.CollectionQueryTest */ class Collection implements Value { private $proto, $database, $name; @@ -277,6 +278,55 @@ public function aggregate(array $pipeline= [], Options... $options): Cursor { return new Cursor($commands, $options, $result['body']['cursor']); } + /** + * Runs a query and returns a cursor. The query may either be a string + * or an object ID, in which case the `_id` member is matched, a map of + * fields and match values which is passed to `find`, or an aggregation + * pipeline. + * + * Note: The pipeline may not contain `$merge` or `$out` stages, this + * method always uses read context for sending the query! + * + * @param string|com.mongodb.ObjectId|[:var]|[:var][] $query + * @param com.mongodb.Options... $options + * @return com.mongodb.result.Cursor + * @throws com.mongodb.Error + */ + public function query($query= [], Options... $options) { + $array= is_array($query); + if ($array && 0 === key($query)) { + $sections= [ + 'aggregate' => $this->name, + 'pipeline' => $query, + 'cursor' => (object)[], + '$db' => $this->database, + ]; + } else { + $sections= [ + 'find' => $this->name, + 'filter' => $array ? ($query ?: (object)[]) : ['_id' => $query], + '$db' => $this->database, + ]; + } + + $commands= Commands::reading($this->proto); + $result= $commands->send($options, $sections); + return new Cursor($commands, $options, $result['body']['cursor']); + } + + /** + * Returns the first document for a given query, or NULL. Shorthand + * for running `$collection->query($query)->first()`. + * + * @param ?string|com.mongodb.ObjectId|[:var]|[:var][] $query + * @param com.mongodb.Options... $options + * @return ?com.mongodb.Document + * @throws com.mongodb.Error + */ + public function first($query= [], Options... $options) { + return $this->query($query, ...$options)->first(); + } + /** * Watch for changes in this collection * diff --git a/src/test/php/com/mongodb/unittest/CollectionQueryTest.class.php b/src/test/php/com/mongodb/unittest/CollectionQueryTest.class.php new file mode 100755 index 0000000..bb6520f --- /dev/null +++ b/src/test/php/com/mongodb/unittest/CollectionQueryTest.class.php @@ -0,0 +1,108 @@ + 'one', 'name' => 'A']; + const SECOND= ['_id' => 'one', 'name' => 'A']; + const DOCUMENTS= [self::FIRST, self::SECOND]; + const QUERY= ['$db' => 'testing', '$readPreference' => ['mode' => 'primary']]; + + /** + * Returns a new fixture + * + * @param var... $messages + * @return com.mongodb.io.Protocol + */ + private function newFixture(... $messages) { + return $this->protocol([self::$PRIMARY => [$this->hello(self::$PRIMARY), ...$messages]], 'primary')->connect(); + } + + #[Test] + public function query() { + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + [new Document(self::FIRST), new Document(self::SECOND)], + (new Collection($protocol, 'testing', 'tests'))->query()->all() + ); + Assert::equals( + ['find' => 'tests', 'filter' => (object)[]] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first() { + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + new Document(self::FIRST), + (new Collection($protocol, 'testing', 'tests'))->first() + ); + Assert::equals( + ['find' => 'tests', 'filter' => (object)[]] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first_with_empty() { + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + new Document(self::FIRST), + (new Collection($protocol, 'testing', 'tests'))->first([]) + ); + Assert::equals( + ['find' => 'tests', 'filter' => (object)[]] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first_with_id() { + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + new Document(self::FIRST), + (new Collection($protocol, 'testing', 'tests'))->first('one') + ); + Assert::equals( + ['find' => 'tests', 'filter' => ['_id' => 'one']] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first_with_query() { + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + new Document(self::FIRST), + (new Collection($protocol, 'testing', 'tests'))->first(['_id' => 'one']) + ); + Assert::equals( + ['find' => 'tests', 'filter' => ['_id' => 'one']] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first_with_pipeline() { + $pipeline= [['$match' => ['_id' => 'one']]]; + $protocol= $this->newFixture($this->cursor(self::DOCUMENTS)); + Assert::equals( + new Document(self::FIRST), + (new Collection($protocol, 'testing', 'tests'))->first($pipeline) + ); + Assert::equals( + ['aggregate' => 'tests', 'pipeline' => $pipeline, 'cursor' => (object)[]] + self::QUERY, + current($protocol->connections())->command(1) + ); + } + + #[Test] + public function first_without_result() { + $protocol= $this->newFixture($this->cursor([])); + Assert::null((new Collection($protocol, 'testing', 'tests'))->first()); + } +} \ No newline at end of file