Completed
Push — master ( 9d713d...92dc6f )
by Andreas
03:54
created

MongoCollection::find()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 7
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 7
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 28 and the first side effect is on line 17.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 */
15
16
if (class_exists('MongoCollection', false)) {
17
    return;
18
}
19
20
use Alcaeus\MongoDbAdapter\Helper;
21
use Alcaeus\MongoDbAdapter\TypeConverter;
22
use Alcaeus\MongoDbAdapter\ExceptionConverter;
23
24
/**
25
 * Represents a database collection.
26
 * @link http://www.php.net/manual/en/class.mongocollection.php
27
 */
28
class MongoCollection
29
{
30
    use Helper\ReadPreference;
31
    use Helper\SlaveOkay;
32
    use Helper\WriteConcern;
33
34
    const ASCENDING = 1;
35
    const DESCENDING = -1;
36
37
    /**
38
     * @var MongoDB
39
     */
40
    public $db = NULL;
41
42
    /**
43
     * @var string
44
     */
45
    protected $name;
46
47
    /**
48
     * @var \MongoDB\Collection
49
     */
50
    protected $collection;
51
52
    /**
53
     * Creates a new collection
54
     *
55
     * @link http://www.php.net/manual/en/mongocollection.construct.php
56
     * @param MongoDB $db Parent database.
57
     * @param string $name Name for this collection.
58
     * @throws Exception
59
     */
60
    public function __construct(MongoDB $db, $name)
61
    {
62
        $this->checkCollectionName($name);
63
        $this->db = $db;
64
        $this->name = (string) $name;
65
66
        $this->setReadPreferenceFromArray($db->getReadPreference());
67
        $this->setWriteConcernFromArray($db->getWriteConcern());
68
69
        $this->createCollectionObject();
70
    }
71
72
    /**
73
     * Gets the underlying collection for this object
74
     *
75
     * @internal This part is not of the ext-mongo API and should not be used
76
     * @return \MongoDB\Collection
77
     */
78
    public function getCollection()
79
    {
80
        return $this->collection;
81
    }
82
83
    /**
84
     * String representation of this collection
85
     *
86
     * @link http://www.php.net/manual/en/mongocollection.--tostring.php
87
     * @return string Returns the full name of this collection.
88
     */
89
    public function __toString()
90
    {
91
        return (string) $this->db . '.' . $this->name;
92
    }
93
94
    /**
95
     * Gets a collection
96
     *
97
     * @link http://www.php.net/manual/en/mongocollection.get.php
98
     * @param string $name The next string in the collection name.
99
     * @return MongoCollection
100
     */
101
    public function __get($name)
102
    {
103
        // Handle w and wtimeout properties that replicate data stored in $readPreference
104
        if ($name === 'w' || $name === 'wtimeout') {
105
            return $this->getWriteConcern()[$name];
106
        }
107
108
        return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
109
    }
110
111
    /**
112
     * @param string $name
113
     * @param mixed $value
114
     */
115
    public function __set($name, $value)
116
    {
117
        if ($name === 'w' || $name === 'wtimeout') {
118
            $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
119
            $this->createCollectionObject();
120
        }
121
    }
122
123
    /**
124
     * Perform an aggregation using the aggregation framework
125
     *
126
     * @link http://www.php.net/manual/en/mongocollection.aggregate.php
127
     * @param array $pipeline
128
     * @param array $op
129
     * @return array
130
     */
131
    public function aggregate(array $pipeline, array $op = [])
132
    {
133
        if (! TypeConverter::isNumericArray($pipeline)) {
134
            $operators = func_get_args();
135
            $pipeline = [];
136
            $options = [];
137
138
            $i = 0;
139
            foreach ($operators as $operator) {
140
                $i++;
141
                if (! is_array($operator)) {
142
                    trigger_error("Argument $i is not an array", E_USER_WARNING);
143
                    return;
144
                }
145
146
                $pipeline[] = $operator;
147
            }
148
        } else {
149
            $options = $op;
150
        }
151
152
        if (isset($options['cursor'])) {
153
            $options['useCursor'] = true;
154
155
            if (isset($options['cursor']['batchSize'])) {
156
                $options['batchSize'] = $options['cursor']['batchSize'];
157
            }
158
159
            unset($options['cursor']);
160
        } else {
161
            $options['useCursor'] = false;
162
        }
163
164
        try {
165
            $cursor = $this->collection->aggregate(TypeConverter::fromLegacy($pipeline), $options);
166
167
            return [
168
                'ok' => 1.0,
169
                'result' => TypeConverter::toLegacy($cursor),
170
                'waitedMS' => 0,
171
            ];
172
        } catch (\MongoDB\Driver\Exception\Exception $e) {
173
            throw ExceptionConverter::toLegacy($e,'MongoResultException');
174
        }
175
    }
176
177
    /**
178
     * Execute an aggregation pipeline command and retrieve results through a cursor
179
     *
180
     * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
181
     * @param array $pipeline
182
     * @param array $options
183
     * @return MongoCommandCursor
184
     */
185
    public function aggregateCursor(array $pipeline, array $options = [])
186
    {
187
        // Build command manually, can't use mongo-php-library here
188
        $command = [
189
            'aggregate' => $this->name,
190
            'pipeline' => $pipeline
191
        ];
192
193
        // Convert cursor option
194
        if (! isset($options['cursor'])) {
195
            $options['cursor'] = new \stdClass();
196
        }
197
198
        $command += $options;
199
200
        $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
201
        $cursor->setReadPreference($this->getReadPreference());
0 ignored issues
show
Documentation introduced by
$this->getReadPreference() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
202
203
        return $cursor;
204
    }
205
206
    /**
207
     * Returns this collection's name
208
     *
209
     * @link http://www.php.net/manual/en/mongocollection.getname.php
210
     * @return string
211
     */
212
    public function getName()
213
    {
214
        return $this->name;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function setReadPreference($readPreference, $tags = null)
221
    {
222
        $result = $this->setReadPreferenceFromParameters($readPreference, $tags);
223
        $this->createCollectionObject();
224
225
        return $result;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function setWriteConcern($wstring, $wtimeout = 0)
232
    {
233
        $result = $this->setWriteConcernFromParameters($wstring, $wtimeout);
234
        $this->createCollectionObject();
235
236
        return $result;
237
    }
238
239
    /**
240
     * Drops this collection
241
     *
242
     * @link http://www.php.net/manual/en/mongocollection.drop.php
243
     * @return array Returns the database response.
244
     */
245
    public function drop()
246
    {
247
        return TypeConverter::toLegacy($this->collection->drop());
248
    }
249
250
    /**
251
     * Validates this collection
252
     *
253
     * @link http://www.php.net/manual/en/mongocollection.validate.php
254
     * @param bool $scan_data Only validate indices, not the base collection.
255
     * @return array Returns the database's evaluation of this object.
256
     */
257
    public function validate($scan_data = FALSE)
258
    {
259
        $command = [
260
            'validate' => $this->name,
261
            'full'     => $scan_data,
262
        ];
263
264
        return $this->db->command($command);
265
    }
266
267
    /**
268
     * Inserts an array into the collection
269
     *
270
     * @link http://www.php.net/manual/en/mongocollection.insert.php
271
     * @param array|object $a
272
     * @param array $options
273
     * @throws MongoException if the inserted document is empty or if it contains zero-length keys. Attempting to insert an object with protected and private properties will cause a zero-length key error.
274
     * @throws MongoCursorException if the "w" option is set and the write fails.
275
     * @throws MongoCursorTimeoutException if the "w" option is set to a value greater than one and the operation takes longer than MongoCursor::$timeout milliseconds to complete. This does not kill the operation on the server, it is a client-side timeout. The operation in MongoCollection::$wtimeout is milliseconds.
276
     * @return bool|array Returns an array containing the status of the insertion if the "w" option is set.
277
     */
278
    public function insert(&$a, array $options = [])
279
    {
280
        if (! $this->ensureDocumentHasMongoId($a)) {
281
            trigger_error(sprintf('%s(): expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
282
            return;
283
        }
284
285
        if (! count((array)$a)) {
286
            throw new \MongoException('document must be an array or object');
287
        }
288
289
        try {
290
            $result = $this->collection->insertOne(
291
                TypeConverter::fromLegacy($a),
292
                $this->convertWriteConcernOptions($options)
293
            );
294
        } catch (\MongoDB\Driver\Exception\Exception $e) {
295
            throw ExceptionConverter::toLegacy($e);
296
        }
297
298
        if (! $result->isAcknowledged()) {
299
            return true;
300
        }
301
302
        return [
303
            'ok' => 1.0,
304
            'n' => 0,
305
            'err' => null,
306
            'errmsg' => null,
307
        ];
308
    }
309
310
    /**
311
     * Inserts multiple documents into this collection
312
     *
313
     * @link http://www.php.net/manual/en/mongocollection.batchinsert.php
314
     * @param array $a An array of arrays.
315
     * @param array $options Options for the inserts.
316
     * @throws MongoCursorException
317
     * @return mixed If "safe" is set, returns an associative array with the status of the inserts ("ok") and any error that may have occured ("err"). Otherwise, returns TRUE if the batch insert was successfully sent, FALSE otherwise.
318
     */
319
    public function batchInsert(array &$a, array $options = [])
320
    {
321
        if (empty($a)) {
322
            throw new \MongoException('No write ops were included in the batch');
323
        }
324
325
        $continueOnError = isset($options['continueOnError']) && $options['continueOnError'];
326
327
        foreach ($a as $key => $item) {
328
            try {
329
                if (! $this->ensureDocumentHasMongoId($a[$key])) {
330
                    if ($continueOnError) {
331
                        unset($a[$key]);
332
                    } else {
333
                        trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
334
                        return;
335
                    }
336
                }
337
            } catch (MongoException $e) {
338
                if ( ! $continueOnError) {
339
                    throw $e;
340
                }
341
            }
342
        }
343
344
        try {
345
            $result = $this->collection->insertMany(
346
                TypeConverter::fromLegacy(array_values($a)),
347
                $this->convertWriteConcernOptions($options)
348
            );
349
        } catch (\MongoDB\Driver\Exception\Exception $e) {
350
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
351
        }
352
353
        if (! $result->isAcknowledged()) {
354
            return true;
355
        }
356
357
        return [
358
            'ok' => 1.0,
359
            'connectionId' => 0,
360
            'n' => 0,
361
            'syncMillis' => 0,
362
            'writtenTo' => null,
363
            'err' => null,
364
        ];
365
    }
366
367
    /**
368
     * Update records based on a given criteria
369
     *
370
     * @link http://www.php.net/manual/en/mongocollection.update.php
371
     * @param array $criteria Description of the objects to update.
372
     * @param array $newobj The object with which to update the matching records.
373
     * @param array $options
374
     * @throws MongoCursorException
375
     * @return boolean
376
     */
377
    public function update(array $criteria , array $newobj, array $options = [])
378
    {
379
        $multiple = isset($options['multiple']) ? $options['multiple'] : false;
380
        $isReplace = ! \MongoDB\is_first_key_operator($newobj);
381
382
        if ($isReplace && $multiple) {
383
            throw new \MongoWriteConcernException('multi update only works with $ operators', 9);
384
        }
385
        unset($options['multiple']);
386
387
        $method = $isReplace ? 'replace' : 'update';
388
        $method .= $multiple ? 'Many' : 'One';
389
390
        try {
391
            /** @var \MongoDB\UpdateResult $result */
392
            $result = $this->collection->$method(
393
                TypeConverter::fromLegacy($criteria),
394
                TypeConverter::fromLegacy($newobj),
395
                $this->convertWriteConcernOptions($options)
396
            );
397
        } catch (\MongoDB\Driver\Exception\Exception $e) {
398
            throw ExceptionConverter::toLegacy($e);
399
        }
400
401
        if (! $result->isAcknowledged()) {
402
            return true;
403
        }
404
405
        return [
406
            'ok' => 1.0,
407
            'nModified' => $result->getModifiedCount(),
408
            'n' => $result->getMatchedCount(),
409
            'err' => null,
410
            'errmsg' => null,
411
            'updatedExisting' => $result->getUpsertedCount() == 0,
412
        ];
413
    }
414
415
    /**
416
     * Remove records from this collection
417
     *
418
     * @link http://www.php.net/manual/en/mongocollection.remove.php
419
     * @param array $criteria Query criteria for the documents to delete.
420
     * @param array $options An array of options for the remove operation.
421
     * @throws MongoCursorException
422
     * @throws MongoCursorTimeoutException
423
     * @return bool|array Returns an array containing the status of the removal
424
     * if the "w" option is set. Otherwise, returns TRUE.
425
     */
426
    public function remove(array $criteria = [], array $options = [])
427
    {
428
        $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
429
        $method = $multiple ? 'deleteMany' : 'deleteOne';
430
431
        try {
432
            /** @var \MongoDB\DeleteResult $result */
433
            $result = $this->collection->$method(
434
                TypeConverter::fromLegacy($criteria),
435
                $this->convertWriteConcernOptions($options)
436
            );
437
        } catch (\MongoDB\Driver\Exception\Exception $e) {
438
            throw ExceptionConverter::toLegacy($e);
439
        }
440
441
        if (! $result->isAcknowledged()) {
442
            return true;
443
        }
444
445
        return [
446
            'ok' => 1.0,
447
            'n' => $result->getDeletedCount(),
448
            'err' => null,
449
            'errmsg' => null
450
        ];
451
    }
452
453
    /**
454
     * Querys this collection
455
     *
456
     * @link http://www.php.net/manual/en/mongocollection.find.php
457
     * @param array $query The fields for which to search.
458
     * @param array $fields Fields of the results to return.
459
     * @return MongoCursor
460
     */
461 View Code Duplication
    public function find(array $query = [], array $fields = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
462
    {
463
        $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
464
        $cursor->setReadPreference($this->getReadPreference());
0 ignored issues
show
Documentation introduced by
$this->getReadPreference() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
465
466
        return $cursor;
467
    }
468
469
    /**
470
     * Retrieve a list of distinct values for the given key across a collection
471
     *
472
     * @link http://www.php.net/manual/ru/mongocollection.distinct.php
473
     * @param string $key The key to use.
474
     * @param array $query An optional query parameters
475
     * @return array|bool Returns an array of distinct values, or FALSE on failure
476
     */
477
    public function distinct($key, array $query = [])
478
    {
479
        try {
480
            return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, TypeConverter::fromLegacy($query)));
481
        } catch (\MongoDB\Driver\Exception\Exception $e) {
482
            return false;
483
        }
484
    }
485
486
    /**
487
     * Update a document and return it
488
     *
489
     * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
490
     * @param array $query The query criteria to search for.
491
     * @param array $update The update criteria.
492
     * @param array $fields Optionally only return these fields.
493
     * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
494
     * @return array Returns the original document, or the modified document when new is set.
495
     */
496
    public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
497
    {
498
        $query = TypeConverter::fromLegacy($query);
499
        try {
500
            if (isset($options['remove'])) {
501
                unset($options['remove']);
502
                $document = $this->collection->findOneAndDelete($query, $options);
503
            } else {
504
                $update = is_array($update) ? $update : [];
505
                if (isset($options['update']) && is_array($options['update'])) {
506
                    $update = $options['update'];
507
                    unset($options['update']);
508
                }
509
510
                $update = TypeConverter::fromLegacy($update);
511
512
                if (isset($options['new'])) {
513
                    $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
514
                    unset($options['new']);
515
                }
516
517
                $options['projection'] = TypeConverter::convertProjection($fields);
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 496 can also be of type null; however, Alcaeus\MongoDbAdapter\T...er::convertProjection() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
518
519
                if (! \MongoDB\is_first_key_operator($update)) {
520
                    $document = $this->collection->findOneAndReplace($query, $update, $options);
521
                } else {
522
                    $document = $this->collection->findOneAndUpdate($query, $update, $options);
523
                }
524
            }
525
        } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
526
            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
527
        } catch (\MongoDB\Driver\Exception\Exception $e) {
528
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
529
        }
530
531
        if ($document) {
532
            $document = TypeConverter::toLegacy($document);
533
        }
534
535
        return $document;
536
    }
537
538
    /**
539
     * Querys this collection, returning a single element
540
     *
541
     * @link http://www.php.net/manual/en/mongocollection.findone.php
542
     * @param array $query The fields for which to search.
543
     * @param array $fields Fields of the results to return.
544
     * @param array $options
545
     * @return array|null
546
     */
547
    public function findOne($query = [], array $fields = [], array $options = [])
548
    {
549
        // Can't typehint for array since MongoGridFS extends and accepts strings
550
        if (! is_array($query)) {
551
            trigger_error(sprintf('MongoCollection::findOne(): expects parameter 1 to be an array or object, %s given', gettype($query)), E_USER_WARNING);
552
            return;
553
        }
554
555
        $options = ['projection' => TypeConverter::convertProjection($fields)] + $options;
556
        try {
557
            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
558
        } catch (\MongoDB\Driver\Exception\Exception $e) {
559
            throw ExceptionConverter::toLegacy($e);
560
        }
561
562
        if ($document !== null) {
563
            $document = TypeConverter::toLegacy($document);
564
        }
565
566
        return $document;
567
    }
568
569
    /**
570
     * Creates an index on the given field(s), or does nothing if the index already exists
571
     *
572
     * @link http://www.php.net/manual/en/mongocollection.createindex.php
573
     * @param array $keys Field or fields to use as index.
574
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
575
     * @return array Returns the database response.
576
     */
577
    public function createIndex($keys, array $options = [])
578
    {
579
        if (is_string($keys)) {
580
            if (empty($keys)) {
581
                throw new MongoException('empty string passed as key field');
582
            }
583
            $keys = [$keys => 1];
584
        }
585
586
        if (is_object($keys)) {
587
            $keys = (array) $keys;
588
        }
589
590
        if (! is_array($keys) || ! count($keys)) {
591
            throw new MongoException('index specification has no elements');
592
        }
593
594
        if (! isset($options['name'])) {
595
            $options['name'] = \MongoDB\generate_index_name($keys);
596
        }
597
598
        $indexes = iterator_to_array($this->collection->listIndexes());
599
        $indexCount = count($indexes);
600
        $collectionExists = true;
601
        $indexExists = false;
602
603
        // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
604
        if ($indexCount === 0) {
605
            $collectionExists = false;
606
            $indexCount = 1;
607
        }
608
609
        foreach ($indexes as $index) {
610
            if ($index->getKey() === $keys || $index->getName() === $options['name']) {
611
                $indexExists = true;
612
                break;
613
            }
614
        }
615
616
        try {
617
            foreach (['w', 'wTimeoutMS', 'safe', 'timeout', 'wtimeout'] as $invalidOption) {
618
                if (isset($options[$invalidOption])) {
619
                    unset($options[$invalidOption]);
620
                }
621
            }
622
623
            $this->collection->createIndex($keys, $options);
624
        } catch (\MongoDB\Driver\Exception\Exception $e) {
625
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
626
        }
627
628
        $result = [
629
            'createdCollectionAutomatically' => !$collectionExists,
630
            'numIndexesBefore' => $indexCount,
631
            'numIndexesAfter' => $indexCount,
632
            'note' => 'all indexes already exist',
633
            'ok' => 1.0,
634
        ];
635
636
        if (! $indexExists) {
637
            $result['numIndexesAfter']++;
638
            unset($result['note']);
639
        }
640
641
        return $result;
642
    }
643
644
    /**
645
     * Creates an index on the given field(s), or does nothing if the index already exists
646
     *
647
     * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
648
     * @param array $keys Field or fields to use as index.
649
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
650
     * @return array Returns the database response.
651
     * @deprecated Use MongoCollection::createIndex() instead.
652
     */
653
    public function ensureIndex(array $keys, array $options = [])
654
    {
655
        return $this->createIndex($keys, $options);
656
    }
657
658
    /**
659
     * Deletes an index from this collection
660
     *
661
     * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
662
     * @param string|array $keys Field or fields from which to delete the index.
663
     * @return array Returns the database response.
664
     */
665
    public function deleteIndex($keys)
666
    {
667
        if (is_string($keys)) {
668
            $indexName = $keys;
669
            if (! preg_match('#_-?1$#', $indexName)) {
670
                $indexName .= '_1';
671
            }
672
        } elseif (is_array($keys)) {
673
            $indexName = \MongoDB\generate_index_name($keys);
674
        } else {
675
            throw new \InvalidArgumentException();
676
        }
677
678
        try {
679
            return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
680
        } catch (\MongoDB\Driver\Exception\Exception $e) {
681
            return ExceptionConverter::toResultArray($e) + ['nIndexesWas' => count($this->getIndexInfo())];
682
        }
683
    }
684
685
    /**
686
     * Delete all indexes for this collection
687
     *
688
     * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
689
     * @return array Returns the database response.
690
     */
691
    public function deleteIndexes()
692
    {
693
        try {
694
            return TypeConverter::toLegacy($this->collection->dropIndexes());
695
        } catch (\MongoDB\Driver\Exception\Exception $e) {
696
            return ExceptionConverter::toResultArray($e);
697
        }
698
    }
699
700
    /**
701
     * Returns an array of index names for this collection
702
     *
703
     * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
704
     * @return array Returns a list of index names.
705
     */
706
    public function getIndexInfo()
707
    {
708
        $convertIndex = function(\MongoDB\Model\IndexInfo $indexInfo) {
709
            $infos = [
710
                'v' => $indexInfo->getVersion(),
711
                'key' => $indexInfo->getKey(),
712
                'name' => $indexInfo->getName(),
713
                'ns' => $indexInfo->getNamespace(),
714
            ];
715
716
            $additionalKeys = [
717
                'unique',
718
                'sparse',
719
                'partialFilterExpression',
720
                'expireAfterSeconds',
721
                'storageEngine',
722
                'weights',
723
                'default_language',
724
                'language_override',
725
                'textIndexVersion',
726
                'collation',
727
                '2dsphereIndexVersion',
728
                'bucketSize'
729
            ];
730
731
            foreach ($additionalKeys as $key) {
732
                if (! isset($indexInfo[$key])) {
733
                    continue;
734
                }
735
736
                $infos[$key] = $indexInfo[$key];
737
            }
738
739
            return $infos;
740
        };
741
742
        return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
743
    }
744
745
    /**
746
     * Counts the number of documents in this collection
747
     *
748
     * @link http://www.php.net/manual/en/mongocollection.count.php
749
     * @param array|stdClass $query
750
     * @param array $options
751
     * @return int Returns the number of documents matching the query.
752
     */
753
    public function count($query = [], $options = [])
754
    {
755
        try {
756
            // Handle legacy mode - limit and skip as second and third parameters, respectively
757
            if (! is_array($options)) {
758
                $limit = $options;
759
                $options = [];
760
761
                if ($limit !== null) {
762
                    $options['limit'] = (int) $limit;
763
                }
764
765
                if (func_num_args() > 2) {
766
                    $options['skip'] = (int) func_get_args()[2];
767
                }
768
            }
769
770
            return $this->collection->count(TypeConverter::fromLegacy($query), $options);
771
        } catch (\MongoDB\Driver\Exception\Exception $e) {
772
            throw ExceptionConverter::toLegacy($e);
773
        }
774
    }
775
776
    /**
777
     * Saves an object to this collection
778
     *
779
     * @link http://www.php.net/manual/en/mongocollection.save.php
780
     * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
781
     * @param array $options Options for the save.
782
     * @throws MongoException if the inserted document is empty or if it contains zero-length keys. Attempting to insert an object with protected and private properties will cause a zero-length key error.
783
     * @throws MongoCursorException if the "w" option is set and the write fails.
784
     * @throws MongoCursorTimeoutException if the "w" option is set to a value greater than one and the operation takes longer than MongoCursor::$timeout milliseconds to complete. This does not kill the operation on the server, it is a client-side timeout. The operation in MongoCollection::$wtimeout is milliseconds.
785
     * @return array|boolean If w was set, returns an array containing the status of the save.
786
     * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
787
     */
788
    public function save(&$a, array $options = [])
789
    {
790
        $id = $this->ensureDocumentHasMongoId($a);
791
792
        $document = (array) $a;
793
794
        $options['upsert'] = true;
795
796
        try {
797
            /** @var \MongoDB\UpdateResult $result */
798
            $result = $this->collection->replaceOne(
799
                TypeConverter::fromLegacy(['_id' => $id]),
800
                TypeConverter::fromLegacy($document),
801
                $this->convertWriteConcernOptions($options)
802
            );
803
804
            if (! $result->isAcknowledged()) {
805
                return true;
806
            }
807
808
            $resultArray = [
809
                'ok' => 1.0,
810
                'nModified' => $result->getModifiedCount(),
811
                'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
812
                'err' => null,
813
                'errmsg' => null,
814
                'updatedExisting' => $result->getUpsertedCount() == 0,
815
            ];
816
            if ($result->getUpsertedId() !== null) {
817
                $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
818
            }
819
820
            return $resultArray;
821
        } catch (\MongoDB\Driver\Exception\Exception $e) {
822
            throw ExceptionConverter::toLegacy($e);
823
        }
824
    }
825
826
    /**
827
     * Creates a database reference
828
     *
829
     * @link http://www.php.net/manual/en/mongocollection.createdbref.php
830
     * @param array|object $document_or_id Object to which to create a reference.
831
     * @return array Returns a database reference array.
832
     */
833
    public function createDBRef($document_or_id)
834
    {
835 View Code Duplication
        if ($document_or_id instanceof \MongoId) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
836
            $id = $document_or_id;
837
        } elseif (is_object($document_or_id)) {
838
            if (! isset($document_or_id->_id)) {
839
                return null;
840
            }
841
842
            $id = $document_or_id->_id;
843
        } elseif (is_array($document_or_id)) {
844
            if (! isset($document_or_id['_id'])) {
845
                return null;
846
            }
847
848
            $id = $document_or_id['_id'];
849
        } else {
850
            $id = $document_or_id;
851
        }
852
853
        return MongoDBRef::create($this->name, $id);
854
    }
855
856
    /**
857
     * Fetches the document pointed to by a database reference
858
     *
859
     * @link http://www.php.net/manual/en/mongocollection.getdbref.php
860
     * @param array $ref A database reference.
861
     * @return array Returns the database document pointed to by the reference.
862
     */
863
    public function getDBRef(array $ref)
864
    {
865
        return $this->db->getDBRef($ref);
866
    }
867
868
    /**
869
     * Performs an operation similar to SQL's GROUP BY command
870
     *
871
     * @link http://www.php.net/manual/en/mongocollection.group.php
872
     * @param mixed $keys Fields to group by. If an array or non-code object is passed, it will be the key used to group results.
873
     * @param array $initial Initial value of the aggregation counter object.
874
     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
875
     * @param array $condition An condition that must be true for a row to be considered.
876
     * @return array
877
     */
878
    public function group($keys, array $initial, $reduce, array $condition = [])
879
    {
880
        if (is_string($reduce)) {
881
            $reduce = new MongoCode($reduce);
882
        }
883
884
        $command = [
885
            'group' => [
886
                'ns' => $this->name,
887
                '$reduce' => (string)$reduce,
888
                'initial' => $initial,
889
                'cond' => $condition,
890
            ],
891
        ];
892
893
        if ($keys instanceof MongoCode) {
894
            $command['group']['$keyf'] = (string)$keys;
895
        } else {
896
            $command['group']['key'] = $keys;
897
        }
898
        if (array_key_exists('condition', $condition)) {
899
            $command['group']['cond'] = $condition['condition'];
900
        }
901
        if (array_key_exists('finalize', $condition)) {
902
            if ($condition['finalize'] instanceof MongoCode) {
903
                $condition['finalize'] = (string)$condition['finalize'];
904
            }
905
            $command['group']['finalize'] = $condition['finalize'];
906
        }
907
908
        return $this->db->command($command);
909
    }
910
911
    /**
912
     * Returns an array of cursors to iterator over a full collection in parallel
913
     *
914
     * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
915
     * @param int $num_cursors The number of cursors to request from the server. Please note, that the server can return less cursors than you requested.
916
     * @return MongoCommandCursor[]
917
     */
918
    public function parallelCollectionScan($num_cursors)
0 ignored issues
show
Unused Code introduced by
The parameter $num_cursors is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
919
    {
920
        $this->notImplemented();
921
    }
922
923
    protected function notImplemented()
924
    {
925
        throw new \Exception('Not implemented');
926
    }
927
928
    /**
929
     * @return \MongoDB\Collection
930
     */
931 View Code Duplication
    private function createCollectionObject()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
932
    {
933
        $options = [
934
            'readPreference' => $this->readPreference,
935
            'writeConcern' => $this->writeConcern,
936
        ];
937
938
        if ($this->collection === null) {
939
            $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
940
        } else {
941
            $this->collection = $this->collection->withOptions($options);
942
        }
943
    }
944
945
    /**
946
     * Converts legacy write concern options to a WriteConcern object
947
     *
948
     * @param array $options
949
     * @return array
950
     */
951
    private function convertWriteConcernOptions(array $options)
952
    {
953
        if (isset($options['safe'])) {
954
            $options['w'] = ($options['safe']) ? 1 : 0;
955
        }
956
957 View Code Duplication
        if (isset($options['wtimeout']) && !isset($options['wTimeoutMS'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
958
            $options['wTimeoutMS'] = $options['wtimeout'];
959
        }
960
961
        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
962
            $collectionWriteConcern = $this->getWriteConcern();
963
            $writeConcern = $this->createWriteConcernFromParameters(
964
                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
965
                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
966
            );
967
968
            $options['writeConcern'] = $writeConcern;
969
        }
970
971
        unset($options['safe']);
972
        unset($options['w']);
973
        unset($options['wTimeout']);
974
        unset($options['wTimeoutMS']);
975
976
        return $options;
977
    }
978
979
    /**
980
     * @param array|object $document
981
     * @return MongoId
982
     */
983
    private function ensureDocumentHasMongoId(&$document)
984
    {
985
        $checkKeys = function($array) {
986
            foreach (array_keys($array) as $key) {
987
                if (empty($key) && $key !== 0) {
988
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
989
                }
990
            }
991
        };
992
993
        if (is_array($document)) {
994
            if (! isset($document['_id'])) {
995
                $document['_id'] = new \MongoId();
996
            }
997
998
            $checkKeys($document);
999
1000
            return $document['_id'];
1001
        } elseif (is_object($document)) {
1002
            $reflectionObject = new \ReflectionObject($document);
1003
            foreach ($reflectionObject->getProperties() as $property) {
1004
                if (! $property->isPublic()) {
1005
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
1006
                }
1007
            }
1008
1009
            if (! isset($document->_id)) {
1010
                $document->_id = new \MongoId();
1011
            }
1012
1013
            $checkKeys((array) $document);
1014
1015
            return $document->_id;
1016
        }
1017
1018
        return null;
1019
    }
1020
1021
    private function checkCollectionName($name)
1022
    {
1023
        if (empty($name)) {
1024
            throw new Exception('Collection name cannot be empty');
1025
        } elseif (strpos($name, chr(0)) !== false) {
1026
            throw new Exception('Collection name cannot contain null bytes');
1027
        }
1028
    }
1029
1030
    /**
1031
     * @return array
1032
     */
1033
    public function __sleep()
1034
    {
1035
        return ['db', 'name'];
1036
    }
1037
}
1038