1<?php
2/*
3 * Copyright 2015-2017 MongoDB, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *   http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace MongoDB;
19
20use MongoDB\BSON\JavascriptInterface;
21use MongoDB\Driver\Cursor;
22use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
23use MongoDB\Driver\Manager;
24use MongoDB\Driver\ReadConcern;
25use MongoDB\Driver\ReadPreference;
26use MongoDB\Driver\WriteConcern;
27use MongoDB\Exception\InvalidArgumentException;
28use MongoDB\Exception\UnexpectedValueException;
29use MongoDB\Exception\UnsupportedException;
30use MongoDB\Model\BSONArray;
31use MongoDB\Model\BSONDocument;
32use MongoDB\Model\IndexInfo;
33use MongoDB\Model\IndexInfoIterator;
34use MongoDB\Operation\Aggregate;
35use MongoDB\Operation\BulkWrite;
36use MongoDB\Operation\Count;
37use MongoDB\Operation\CountDocuments;
38use MongoDB\Operation\CreateIndexes;
39use MongoDB\Operation\DeleteMany;
40use MongoDB\Operation\DeleteOne;
41use MongoDB\Operation\Distinct;
42use MongoDB\Operation\DropCollection;
43use MongoDB\Operation\DropIndexes;
44use MongoDB\Operation\EstimatedDocumentCount;
45use MongoDB\Operation\Explain;
46use MongoDB\Operation\Explainable;
47use MongoDB\Operation\Find;
48use MongoDB\Operation\FindOne;
49use MongoDB\Operation\FindOneAndDelete;
50use MongoDB\Operation\FindOneAndReplace;
51use MongoDB\Operation\FindOneAndUpdate;
52use MongoDB\Operation\InsertMany;
53use MongoDB\Operation\InsertOne;
54use MongoDB\Operation\ListIndexes;
55use MongoDB\Operation\MapReduce;
56use MongoDB\Operation\ReplaceOne;
57use MongoDB\Operation\UpdateMany;
58use MongoDB\Operation\UpdateOne;
59use MongoDB\Operation\Watch;
60use Traversable;
61use function array_diff_key;
62use function array_intersect_key;
63use function current;
64use function is_array;
65use function strlen;
66
67class Collection
68{
69    /** @var array */
70    private static $defaultTypeMap = [
71        'array' => BSONArray::class,
72        'document' => BSONDocument::class,
73        'root' => BSONDocument::class,
74    ];
75
76    /** @var integer */
77    private static $wireVersionForFindAndModifyWriteConcern = 4;
78
79    /** @var integer */
80    private static $wireVersionForReadConcern = 4;
81
82    /** @var integer */
83    private static $wireVersionForWritableCommandWriteConcern = 5;
84
85    /** @var integer */
86    private static $wireVersionForReadConcernWithWriteStage = 8;
87
88    /** @var string */
89    private $collectionName;
90
91    /** @var string */
92    private $databaseName;
93
94    /** @var Manager */
95    private $manager;
96
97    /** @var ReadConcern */
98    private $readConcern;
99
100    /** @var ReadPreference */
101    private $readPreference;
102
103    /** @var array */
104    private $typeMap;
105
106    /** @var WriteConcern */
107    private $writeConcern;
108
109    /**
110     * Constructs new Collection instance.
111     *
112     * This class provides methods for collection-specific operations, such as
113     * CRUD (i.e. create, read, update, and delete) and index management.
114     *
115     * Supported options:
116     *
117     *  * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
118     *    use for collection operations. Defaults to the Manager's read concern.
119     *
120     *  * readPreference (MongoDB\Driver\ReadPreference): The default read
121     *    preference to use for collection operations. Defaults to the Manager's
122     *    read preference.
123     *
124     *  * typeMap (array): Default type map for cursors and BSON documents.
125     *
126     *  * writeConcern (MongoDB\Driver\WriteConcern): The default write concern
127     *    to use for collection operations. Defaults to the Manager's write
128     *    concern.
129     *
130     * @param Manager $manager        Manager instance from the driver
131     * @param string  $databaseName   Database name
132     * @param string  $collectionName Collection name
133     * @param array   $options        Collection options
134     * @throws InvalidArgumentException for parameter/option parsing errors
135     */
136    public function __construct(Manager $manager, $databaseName, $collectionName, array $options = [])
137    {
138        if (strlen($databaseName) < 1) {
139            throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
140        }
141
142        if (strlen($collectionName) < 1) {
143            throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName);
144        }
145
146        if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
147            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
148        }
149
150        if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
151            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class);
152        }
153
154        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
155            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
156        }
157
158        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
159            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
160        }
161
162        $this->manager = $manager;
163        $this->databaseName = (string) $databaseName;
164        $this->collectionName = (string) $collectionName;
165        $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern();
166        $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference();
167        $this->typeMap = $options['typeMap'] ?? self::$defaultTypeMap;
168        $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern();
169    }
170
171    /**
172     * Return internal properties for debugging purposes.
173     *
174     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
175     * @return array
176     */
177    public function __debugInfo()
178    {
179        return [
180            'collectionName' => $this->collectionName,
181            'databaseName' => $this->databaseName,
182            'manager' => $this->manager,
183            'readConcern' => $this->readConcern,
184            'readPreference' => $this->readPreference,
185            'typeMap' => $this->typeMap,
186            'writeConcern' => $this->writeConcern,
187        ];
188    }
189
190    /**
191     * Return the collection namespace (e.g. "db.collection").
192     *
193     * @see https://docs.mongodb.org/manual/faq/developers/#faq-dev-namespace
194     * @return string
195     */
196    public function __toString()
197    {
198        return $this->databaseName . '.' . $this->collectionName;
199    }
200
201    /**
202     * Executes an aggregation framework pipeline on the collection.
203     *
204     * Note: this method's return value depends on the MongoDB server version
205     * and the "useCursor" option. If "useCursor" is true, a Cursor will be
206     * returned; otherwise, an ArrayIterator is returned, which wraps the
207     * "result" array from the command response document.
208     *
209     * @see Aggregate::__construct() for supported options
210     * @param array $pipeline List of pipeline operations
211     * @param array $options  Command options
212     * @return Traversable
213     * @throws UnexpectedValueException if the command response was malformed
214     * @throws UnsupportedException if options are not supported by the selected server
215     * @throws InvalidArgumentException for parameter/option parsing errors
216     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
217     */
218    public function aggregate(array $pipeline, array $options = [])
219    {
220        $hasWriteStage = is_last_pipeline_operator_write($pipeline);
221
222        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
223            $options['readPreference'] = $this->readPreference;
224        }
225
226        if ($hasWriteStage) {
227            $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
228        }
229
230        $server = select_server($this->manager, $options);
231
232        /* MongoDB 4.2 and later supports a read concern when an $out stage is
233         * being used, but earlier versions do not.
234         *
235         * A read concern is also not compatible with transactions.
236         */
237        if (! isset($options['readConcern']) &&
238            server_supports_feature($server, self::$wireVersionForReadConcern) &&
239            ! is_in_transaction($options) &&
240            ( ! $hasWriteStage || server_supports_feature($server, self::$wireVersionForReadConcernWithWriteStage))
241        ) {
242            $options['readConcern'] = $this->readConcern;
243        }
244
245        if (! isset($options['typeMap'])) {
246            $options['typeMap'] = $this->typeMap;
247        }
248
249        if ($hasWriteStage &&
250            ! isset($options['writeConcern']) &&
251            server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) &&
252            ! is_in_transaction($options)) {
253            $options['writeConcern'] = $this->writeConcern;
254        }
255
256        $operation = new Aggregate($this->databaseName, $this->collectionName, $pipeline, $options);
257
258        return $operation->execute($server);
259    }
260
261    /**
262     * Executes multiple write operations.
263     *
264     * @see BulkWrite::__construct() for supported options
265     * @param array[] $operations List of write operations
266     * @param array   $options    Command options
267     * @return BulkWriteResult
268     * @throws UnsupportedException if options are not supported by the selected server
269     * @throws InvalidArgumentException for parameter/option parsing errors
270     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
271     */
272    public function bulkWrite(array $operations, array $options = [])
273    {
274        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
275            $options['writeConcern'] = $this->writeConcern;
276        }
277
278        $operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
279        $server = select_server($this->manager, $options);
280
281        return $operation->execute($server);
282    }
283
284    /**
285     * Gets the number of documents matching the filter.
286     *
287     * @see Count::__construct() for supported options
288     * @param array|object $filter  Query by which to filter documents
289     * @param array        $options Command options
290     * @return integer
291     * @throws UnexpectedValueException if the command response was malformed
292     * @throws UnsupportedException if options are not supported by the selected server
293     * @throws InvalidArgumentException for parameter/option parsing errors
294     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
295     *
296     * @deprecated 1.4
297     */
298    public function count($filter = [], array $options = [])
299    {
300        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
301            $options['readPreference'] = $this->readPreference;
302        }
303
304        $server = select_server($this->manager, $options);
305
306        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
307            $options['readConcern'] = $this->readConcern;
308        }
309
310        $operation = new Count($this->databaseName, $this->collectionName, $filter, $options);
311
312        return $operation->execute($server);
313    }
314
315    /**
316     * Gets the number of documents matching the filter.
317     *
318     * @see CountDocuments::__construct() for supported options
319     * @param array|object $filter  Query by which to filter documents
320     * @param array        $options Command options
321     * @return integer
322     * @throws UnexpectedValueException if the command response was malformed
323     * @throws UnsupportedException if options are not supported by the selected server
324     * @throws InvalidArgumentException for parameter/option parsing errors
325     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
326     */
327    public function countDocuments($filter = [], array $options = [])
328    {
329        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
330            $options['readPreference'] = $this->readPreference;
331        }
332
333        $server = select_server($this->manager, $options);
334
335        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
336            $options['readConcern'] = $this->readConcern;
337        }
338
339        $operation = new CountDocuments($this->databaseName, $this->collectionName, $filter, $options);
340
341        return $operation->execute($server);
342    }
343
344    /**
345     * Create a single index for the collection.
346     *
347     * @see Collection::createIndexes()
348     * @see CreateIndexes::__construct() for supported command options
349     * @param array|object $key     Document containing fields mapped to values,
350     *                              which denote order or an index type
351     * @param array        $options Index and command options
352     * @return string The name of the created index
353     * @throws UnsupportedException if options are not supported by the selected server
354     * @throws InvalidArgumentException for parameter/option parsing errors
355     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
356     */
357    public function createIndex($key, array $options = [])
358    {
359        $commandOptionKeys = ['commitQuorum' => 1, 'maxTimeMS' => 1, 'session' => 1, 'writeConcern' => 1];
360        $indexOptions = array_diff_key($options, $commandOptionKeys);
361        $commandOptions = array_intersect_key($options, $commandOptionKeys);
362
363        return current($this->createIndexes([['key' => $key] + $indexOptions], $commandOptions));
364    }
365
366    /**
367     * Create one or more indexes for the collection.
368     *
369     * Each element in the $indexes array must have a "key" document, which
370     * contains fields mapped to an order or type. Other options may follow.
371     * For example:
372     *
373     *     $indexes = [
374     *         // Create a unique index on the "username" field
375     *         [ 'key' => [ 'username' => 1 ], 'unique' => true ],
376     *         // Create a 2dsphere index on the "loc" field with a custom name
377     *         [ 'key' => [ 'loc' => '2dsphere' ], 'name' => 'geo' ],
378     *     ];
379     *
380     * If the "name" option is unspecified, a name will be generated from the
381     * "key" document.
382     *
383     * @see http://docs.mongodb.org/manual/reference/command/createIndexes/
384     * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
385     * @see CreateIndexes::__construct() for supported command options
386     * @param array[] $indexes List of index specifications
387     * @param array   $options Command options
388     * @return string[] The names of the created indexes
389     * @throws UnsupportedException if options are not supported by the selected server
390     * @throws InvalidArgumentException for parameter/option parsing errors
391     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
392     */
393    public function createIndexes(array $indexes, array $options = [])
394    {
395        $server = select_server($this->manager, $options);
396
397        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
398            $options['writeConcern'] = $this->writeConcern;
399        }
400
401        $operation = new CreateIndexes($this->databaseName, $this->collectionName, $indexes, $options);
402
403        return $operation->execute($server);
404    }
405
406    /**
407     * Deletes all documents matching the filter.
408     *
409     * @see DeleteMany::__construct() for supported options
410     * @see http://docs.mongodb.org/manual/reference/command/delete/
411     * @param array|object $filter  Query by which to delete documents
412     * @param array        $options Command options
413     * @return DeleteResult
414     * @throws UnsupportedException if options are not supported by the selected server
415     * @throws InvalidArgumentException for parameter/option parsing errors
416     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
417     */
418    public function deleteMany($filter, array $options = [])
419    {
420        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
421            $options['writeConcern'] = $this->writeConcern;
422        }
423
424        $operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
425        $server = select_server($this->manager, $options);
426
427        return $operation->execute($server);
428    }
429
430    /**
431     * Deletes at most one document matching the filter.
432     *
433     * @see DeleteOne::__construct() for supported options
434     * @see http://docs.mongodb.org/manual/reference/command/delete/
435     * @param array|object $filter  Query by which to delete documents
436     * @param array        $options Command options
437     * @return DeleteResult
438     * @throws UnsupportedException if options are not supported by the selected server
439     * @throws InvalidArgumentException for parameter/option parsing errors
440     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
441     */
442    public function deleteOne($filter, array $options = [])
443    {
444        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
445            $options['writeConcern'] = $this->writeConcern;
446        }
447
448        $operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
449        $server = select_server($this->manager, $options);
450
451        return $operation->execute($server);
452    }
453
454    /**
455     * Finds the distinct values for a specified field across the collection.
456     *
457     * @see Distinct::__construct() for supported options
458     * @param string       $fieldName Field for which to return distinct values
459     * @param array|object $filter    Query by which to filter documents
460     * @param array        $options   Command options
461     * @return mixed[]
462     * @throws UnexpectedValueException if the command response was malformed
463     * @throws UnsupportedException if options are not supported by the selected server
464     * @throws InvalidArgumentException for parameter/option parsing errors
465     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
466     */
467    public function distinct($fieldName, $filter = [], array $options = [])
468    {
469        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
470            $options['readPreference'] = $this->readPreference;
471        }
472
473        if (! isset($options['typeMap'])) {
474            $options['typeMap'] = $this->typeMap;
475        }
476
477        $server = select_server($this->manager, $options);
478
479        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
480            $options['readConcern'] = $this->readConcern;
481        }
482
483        $operation = new Distinct($this->databaseName, $this->collectionName, $fieldName, $filter, $options);
484
485        return $operation->execute($server);
486    }
487
488    /**
489     * Drop this collection.
490     *
491     * @see DropCollection::__construct() for supported options
492     * @param array $options Additional options
493     * @return array|object Command result document
494     * @throws UnsupportedException if options are not supported by the selected server
495     * @throws InvalidArgumentException for parameter/option parsing errors
496     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
497     */
498    public function drop(array $options = [])
499    {
500        if (! isset($options['typeMap'])) {
501            $options['typeMap'] = $this->typeMap;
502        }
503
504        $server = select_server($this->manager, $options);
505
506        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
507            $options['writeConcern'] = $this->writeConcern;
508        }
509
510        $operation = new DropCollection($this->databaseName, $this->collectionName, $options);
511
512        return $operation->execute($server);
513    }
514
515    /**
516     * Drop a single index in the collection.
517     *
518     * @see DropIndexes::__construct() for supported options
519     * @param string|IndexInfo $indexName Index name or model object
520     * @param array            $options   Additional options
521     * @return array|object Command result document
522     * @throws UnsupportedException if options are not supported by the selected server
523     * @throws InvalidArgumentException for parameter/option parsing errors
524     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
525     */
526    public function dropIndex($indexName, array $options = [])
527    {
528        $indexName = (string) $indexName;
529
530        if ($indexName === '*') {
531            throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes');
532        }
533
534        if (! isset($options['typeMap'])) {
535            $options['typeMap'] = $this->typeMap;
536        }
537
538        $server = select_server($this->manager, $options);
539
540        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
541            $options['writeConcern'] = $this->writeConcern;
542        }
543
544        $operation = new DropIndexes($this->databaseName, $this->collectionName, $indexName, $options);
545
546        return $operation->execute($server);
547    }
548
549    /**
550     * Drop all indexes in the collection.
551     *
552     * @see DropIndexes::__construct() for supported options
553     * @param array $options Additional options
554     * @return array|object Command result document
555     * @throws UnsupportedException if options are not supported by the selected server
556     * @throws InvalidArgumentException for parameter/option parsing errors
557     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
558     */
559    public function dropIndexes(array $options = [])
560    {
561        if (! isset($options['typeMap'])) {
562            $options['typeMap'] = $this->typeMap;
563        }
564
565        $server = select_server($this->manager, $options);
566
567        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
568            $options['writeConcern'] = $this->writeConcern;
569        }
570
571        $operation = new DropIndexes($this->databaseName, $this->collectionName, '*', $options);
572
573        return $operation->execute($server);
574    }
575
576    /**
577     * Gets an estimated number of documents in the collection using the collection metadata.
578     *
579     * @see EstimatedDocumentCount::__construct() for supported options
580     * @param array $options Command options
581     * @return integer
582     * @throws UnexpectedValueException if the command response was malformed
583     * @throws UnsupportedException if options are not supported by the selected server
584     * @throws InvalidArgumentException for parameter/option parsing errors
585     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
586     */
587    public function estimatedDocumentCount(array $options = [])
588    {
589        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
590            $options['readPreference'] = $this->readPreference;
591        }
592
593        $server = select_server($this->manager, $options);
594
595        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
596            $options['readConcern'] = $this->readConcern;
597        }
598
599        $operation = new EstimatedDocumentCount($this->databaseName, $this->collectionName, $options);
600
601        return $operation->execute($server);
602    }
603
604    /**
605     * Explains explainable commands.
606     *
607     * @see Explain::__construct() for supported options
608     * @see http://docs.mongodb.org/manual/reference/command/explain/
609     * @param Explainable $explainable Command on which to run explain
610     * @param array       $options     Additional options
611     * @return array|object
612     * @throws UnsupportedException if explainable or options are not supported by the selected server
613     * @throws InvalidArgumentException for parameter/option parsing errors
614     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
615     */
616    public function explain(Explainable $explainable, array $options = [])
617    {
618        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
619            $options['readPreference'] = $this->readPreference;
620        }
621
622        if (! isset($options['typeMap'])) {
623            $options['typeMap'] = $this->typeMap;
624        }
625
626        $server = select_server($this->manager, $options);
627
628        $operation = new Explain($this->databaseName, $explainable, $options);
629
630        return $operation->execute($server);
631    }
632
633    /**
634     * Finds documents matching the query.
635     *
636     * @see Find::__construct() for supported options
637     * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
638     * @param array|object $filter  Query by which to filter documents
639     * @param array        $options Additional options
640     * @return Cursor
641     * @throws UnsupportedException if options are not supported by the selected server
642     * @throws InvalidArgumentException for parameter/option parsing errors
643     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
644     */
645    public function find($filter = [], array $options = [])
646    {
647        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
648            $options['readPreference'] = $this->readPreference;
649        }
650
651        $server = select_server($this->manager, $options);
652
653        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
654            $options['readConcern'] = $this->readConcern;
655        }
656
657        if (! isset($options['typeMap'])) {
658            $options['typeMap'] = $this->typeMap;
659        }
660
661        $operation = new Find($this->databaseName, $this->collectionName, $filter, $options);
662
663        return $operation->execute($server);
664    }
665
666    /**
667     * Finds a single document matching the query.
668     *
669     * @see FindOne::__construct() for supported options
670     * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
671     * @param array|object $filter  Query by which to filter documents
672     * @param array        $options Additional options
673     * @return array|object|null
674     * @throws UnsupportedException if options are not supported by the selected server
675     * @throws InvalidArgumentException for parameter/option parsing errors
676     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
677     */
678    public function findOne($filter = [], array $options = [])
679    {
680        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
681            $options['readPreference'] = $this->readPreference;
682        }
683
684        $server = select_server($this->manager, $options);
685
686        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
687            $options['readConcern'] = $this->readConcern;
688        }
689
690        if (! isset($options['typeMap'])) {
691            $options['typeMap'] = $this->typeMap;
692        }
693
694        $operation = new FindOne($this->databaseName, $this->collectionName, $filter, $options);
695
696        return $operation->execute($server);
697    }
698
699    /**
700     * Finds a single document and deletes it, returning the original.
701     *
702     * The document to return may be null if no document matched the filter.
703     *
704     * @see FindOneAndDelete::__construct() for supported options
705     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
706     * @param array|object $filter  Query by which to filter documents
707     * @param array        $options Command options
708     * @return array|object|null
709     * @throws UnexpectedValueException if the command response was malformed
710     * @throws UnsupportedException if options are not supported by the selected server
711     * @throws InvalidArgumentException for parameter/option parsing errors
712     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
713     */
714    public function findOneAndDelete($filter, array $options = [])
715    {
716        $server = select_server($this->manager, $options);
717
718        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
719            $options['writeConcern'] = $this->writeConcern;
720        }
721
722        if (! isset($options['typeMap'])) {
723            $options['typeMap'] = $this->typeMap;
724        }
725
726        $operation = new FindOneAndDelete($this->databaseName, $this->collectionName, $filter, $options);
727
728        return $operation->execute($server);
729    }
730
731    /**
732     * Finds a single document and replaces it, returning either the original or
733     * the replaced document.
734     *
735     * The document to return may be null if no document matched the filter. By
736     * default, the original document is returned. Specify
737     * FindOneAndReplace::RETURN_DOCUMENT_AFTER for the "returnDocument" option
738     * to return the updated document.
739     *
740     * @see FindOneAndReplace::__construct() for supported options
741     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
742     * @param array|object $filter      Query by which to filter documents
743     * @param array|object $replacement Replacement document
744     * @param array        $options     Command options
745     * @return array|object|null
746     * @throws UnexpectedValueException if the command response was malformed
747     * @throws UnsupportedException if options are not supported by the selected server
748     * @throws InvalidArgumentException for parameter/option parsing errors
749     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
750     */
751    public function findOneAndReplace($filter, $replacement, array $options = [])
752    {
753        $server = select_server($this->manager, $options);
754
755        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
756            $options['writeConcern'] = $this->writeConcern;
757        }
758
759        if (! isset($options['typeMap'])) {
760            $options['typeMap'] = $this->typeMap;
761        }
762
763        $operation = new FindOneAndReplace($this->databaseName, $this->collectionName, $filter, $replacement, $options);
764
765        return $operation->execute($server);
766    }
767
768    /**
769     * Finds a single document and updates it, returning either the original or
770     * the updated document.
771     *
772     * The document to return may be null if no document matched the filter. By
773     * default, the original document is returned. Specify
774     * FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the "returnDocument" option
775     * to return the updated document.
776     *
777     * @see FindOneAndReplace::__construct() for supported options
778     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
779     * @param array|object $filter  Query by which to filter documents
780     * @param array|object $update  Update to apply to the matched document
781     * @param array        $options Command options
782     * @return array|object|null
783     * @throws UnexpectedValueException if the command response was malformed
784     * @throws UnsupportedException if options are not supported by the selected server
785     * @throws InvalidArgumentException for parameter/option parsing errors
786     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
787     */
788    public function findOneAndUpdate($filter, $update, array $options = [])
789    {
790        $server = select_server($this->manager, $options);
791
792        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
793            $options['writeConcern'] = $this->writeConcern;
794        }
795
796        if (! isset($options['typeMap'])) {
797            $options['typeMap'] = $this->typeMap;
798        }
799
800        $operation = new FindOneAndUpdate($this->databaseName, $this->collectionName, $filter, $update, $options);
801
802        return $operation->execute($server);
803    }
804
805    /**
806     * Return the collection name.
807     *
808     * @return string
809     */
810    public function getCollectionName()
811    {
812        return $this->collectionName;
813    }
814
815    /**
816     * Return the database name.
817     *
818     * @return string
819     */
820    public function getDatabaseName()
821    {
822        return $this->databaseName;
823    }
824
825    /**
826     * Return the Manager.
827     *
828     * @return Manager
829     */
830    public function getManager()
831    {
832        return $this->manager;
833    }
834
835    /**
836     * Return the collection namespace.
837     *
838     * @see https://docs.mongodb.org/manual/reference/glossary/#term-namespace
839     * @return string
840     */
841    public function getNamespace()
842    {
843        return $this->databaseName . '.' . $this->collectionName;
844    }
845
846    /**
847     * Return the read concern for this collection.
848     *
849     * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
850     * @return ReadConcern
851     */
852    public function getReadConcern()
853    {
854        return $this->readConcern;
855    }
856
857    /**
858     * Return the read preference for this collection.
859     *
860     * @return ReadPreference
861     */
862    public function getReadPreference()
863    {
864        return $this->readPreference;
865    }
866
867    /**
868     * Return the type map for this collection.
869     *
870     * @return array
871     */
872    public function getTypeMap()
873    {
874        return $this->typeMap;
875    }
876
877    /**
878     * Return the write concern for this collection.
879     *
880     * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
881     * @return WriteConcern
882     */
883    public function getWriteConcern()
884    {
885        return $this->writeConcern;
886    }
887
888    /**
889     * Inserts multiple documents.
890     *
891     * @see InsertMany::__construct() for supported options
892     * @see http://docs.mongodb.org/manual/reference/command/insert/
893     * @param array[]|object[] $documents The documents to insert
894     * @param array            $options   Command options
895     * @return InsertManyResult
896     * @throws InvalidArgumentException for parameter/option parsing errors
897     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
898     */
899    public function insertMany(array $documents, array $options = [])
900    {
901        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
902            $options['writeConcern'] = $this->writeConcern;
903        }
904
905        $operation = new InsertMany($this->databaseName, $this->collectionName, $documents, $options);
906        $server = select_server($this->manager, $options);
907
908        return $operation->execute($server);
909    }
910
911    /**
912     * Inserts one document.
913     *
914     * @see InsertOne::__construct() for supported options
915     * @see http://docs.mongodb.org/manual/reference/command/insert/
916     * @param array|object $document The document to insert
917     * @param array        $options  Command options
918     * @return InsertOneResult
919     * @throws InvalidArgumentException for parameter/option parsing errors
920     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
921     */
922    public function insertOne($document, array $options = [])
923    {
924        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
925            $options['writeConcern'] = $this->writeConcern;
926        }
927
928        $operation = new InsertOne($this->databaseName, $this->collectionName, $document, $options);
929        $server = select_server($this->manager, $options);
930
931        return $operation->execute($server);
932    }
933
934    /**
935     * Returns information for all indexes for the collection.
936     *
937     * @see ListIndexes::__construct() for supported options
938     * @param array $options
939     * @return IndexInfoIterator
940     * @throws InvalidArgumentException for parameter/option parsing errors
941     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
942     */
943    public function listIndexes(array $options = [])
944    {
945        $operation = new ListIndexes($this->databaseName, $this->collectionName, $options);
946        $server = select_server($this->manager, $options);
947
948        return $operation->execute($server);
949    }
950
951    /**
952     * Executes a map-reduce aggregation on the collection.
953     *
954     * @see MapReduce::__construct() for supported options
955     * @see http://docs.mongodb.org/manual/reference/command/mapReduce/
956     * @param JavascriptInterface $map     Map function
957     * @param JavascriptInterface $reduce  Reduce function
958     * @param string|array|object $out     Output specification
959     * @param array               $options Command options
960     * @return MapReduceResult
961     * @throws UnsupportedException if options are not supported by the selected server
962     * @throws InvalidArgumentException for parameter/option parsing errors
963     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
964     * @throws UnexpectedValueException if the command response was malformed
965     */
966    public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce, $out, array $options = [])
967    {
968        $hasOutputCollection = ! is_mapreduce_output_inline($out);
969
970        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
971            $options['readPreference'] = $this->readPreference;
972        }
973
974        // Check if the out option is inline because we will want to coerce a primary read preference if not
975        if ($hasOutputCollection) {
976            $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
977        }
978
979        $server = select_server($this->manager, $options);
980
981        /* A "majority" read concern is not compatible with inline output, so
982         * avoid providing the Collection's read concern if it would conflict.
983         *
984         * A read concern is also not compatible with transactions.
985         */
986        if (! isset($options['readConcern']) && ! ($hasOutputCollection && $this->readConcern->getLevel() === ReadConcern::MAJORITY) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
987            $options['readConcern'] = $this->readConcern;
988        }
989
990        if (! isset($options['typeMap'])) {
991            $options['typeMap'] = $this->typeMap;
992        }
993
994        if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
995            $options['writeConcern'] = $this->writeConcern;
996        }
997
998        $operation = new MapReduce($this->databaseName, $this->collectionName, $map, $reduce, $out, $options);
999
1000        return $operation->execute($server);
1001    }
1002
1003    /**
1004     * Replaces at most one document matching the filter.
1005     *
1006     * @see ReplaceOne::__construct() for supported options
1007     * @see http://docs.mongodb.org/manual/reference/command/update/
1008     * @param array|object $filter      Query by which to filter documents
1009     * @param array|object $replacement Replacement document
1010     * @param array        $options     Command options
1011     * @return UpdateResult
1012     * @throws UnsupportedException if options are not supported by the selected server
1013     * @throws InvalidArgumentException for parameter/option parsing errors
1014     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
1015     */
1016    public function replaceOne($filter, $replacement, array $options = [])
1017    {
1018        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
1019            $options['writeConcern'] = $this->writeConcern;
1020        }
1021
1022        $operation = new ReplaceOne($this->databaseName, $this->collectionName, $filter, $replacement, $options);
1023        $server = select_server($this->manager, $options);
1024
1025        return $operation->execute($server);
1026    }
1027
1028    /**
1029     * Updates all documents matching the filter.
1030     *
1031     * @see UpdateMany::__construct() for supported options
1032     * @see http://docs.mongodb.org/manual/reference/command/update/
1033     * @param array|object $filter  Query by which to filter documents
1034     * @param array|object $update  Update to apply to the matched documents
1035     * @param array        $options Command options
1036     * @return UpdateResult
1037     * @throws UnsupportedException if options are not supported by the selected server
1038     * @throws InvalidArgumentException for parameter/option parsing errors
1039     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
1040     */
1041    public function updateMany($filter, $update, array $options = [])
1042    {
1043        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
1044            $options['writeConcern'] = $this->writeConcern;
1045        }
1046
1047        $operation = new UpdateMany($this->databaseName, $this->collectionName, $filter, $update, $options);
1048        $server = select_server($this->manager, $options);
1049
1050        return $operation->execute($server);
1051    }
1052
1053    /**
1054     * Updates at most one document matching the filter.
1055     *
1056     * @see UpdateOne::__construct() for supported options
1057     * @see http://docs.mongodb.org/manual/reference/command/update/
1058     * @param array|object $filter  Query by which to filter documents
1059     * @param array|object $update  Update to apply to the matched document
1060     * @param array        $options Command options
1061     * @return UpdateResult
1062     * @throws UnsupportedException if options are not supported by the selected server
1063     * @throws InvalidArgumentException for parameter/option parsing errors
1064     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
1065     */
1066    public function updateOne($filter, $update, array $options = [])
1067    {
1068        if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
1069            $options['writeConcern'] = $this->writeConcern;
1070        }
1071
1072        $operation = new UpdateOne($this->databaseName, $this->collectionName, $filter, $update, $options);
1073        $server = select_server($this->manager, $options);
1074
1075        return $operation->execute($server);
1076    }
1077
1078    /**
1079     * Create a change stream for watching changes to the collection.
1080     *
1081     * @see Watch::__construct() for supported options
1082     * @param array $pipeline List of pipeline operations
1083     * @param array $options  Command options
1084     * @return ChangeStream
1085     * @throws InvalidArgumentException for parameter/option parsing errors
1086     */
1087    public function watch(array $pipeline = [], array $options = [])
1088    {
1089        if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
1090            $options['readPreference'] = $this->readPreference;
1091        }
1092
1093        $server = select_server($this->manager, $options);
1094
1095        /* Although change streams require a newer version of the server than
1096         * read concerns, perform the usual wire version check before inheriting
1097         * the collection's read concern. In the event that the server is too
1098         * old, this makes it more likely that users will encounter an error
1099         * related to change streams being unsupported instead of an
1100         * UnsupportedException regarding use of the "readConcern" option from
1101         * the Aggregate operation class. */
1102        if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
1103            $options['readConcern'] = $this->readConcern;
1104        }
1105
1106        if (! isset($options['typeMap'])) {
1107            $options['typeMap'] = $this->typeMap;
1108        }
1109
1110        $operation = new Watch($this->manager, $this->databaseName, $this->collectionName, $pipeline, $options);
1111
1112        return $operation->execute($server);
1113    }
1114
1115    /**
1116     * Get a clone of this collection with different options.
1117     *
1118     * @see Collection::__construct() for supported options
1119     * @param array $options Collection constructor options
1120     * @return Collection
1121     * @throws InvalidArgumentException for parameter/option parsing errors
1122     */
1123    public function withOptions(array $options = [])
1124    {
1125        $options += [
1126            'readConcern' => $this->readConcern,
1127            'readPreference' => $this->readPreference,
1128            'typeMap' => $this->typeMap,
1129            'writeConcern' => $this->writeConcern,
1130        ];
1131
1132        return new Collection($this->manager, $this->databaseName, $this->collectionName, $options);
1133    }
1134}
1135