Completed
Push — master ( 4352ad...9fdb55 )
by Andreas
20:23
created

MongoCollection::update()   D

Complexity

Conditions 9
Paths 34

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 25
nc 34
nop 3
dl 0
loc 39
rs 4.909
c 0
b 0
f 0
1
<?php
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
     * @return bool|array
375
     * @throws MongoException
376
     * @throws MongoWriteConcernException
377
     */
378
    public function update(array $criteria, array $newobj, array $options = [])
379
    {
380
        $this->checkKeys($newobj);
381
382
        $multiple = isset($options['multiple']) ? $options['multiple'] : false;
383
        $isReplace = ! \MongoDB\is_first_key_operator($newobj);
384
385
        if ($isReplace && $multiple) {
386
            throw new \MongoWriteConcernException('multi update only works with $ operators', 9);
387
        }
388
        unset($options['multiple']);
389
390
        $method = $isReplace ? 'replace' : 'update';
391
        $method .= $multiple ? 'Many' : 'One';
392
393
        try {
394
            /** @var \MongoDB\UpdateResult $result */
395
            $result = $this->collection->$method(
396
                TypeConverter::fromLegacy($criteria),
397
                TypeConverter::fromLegacy($newobj),
398
                $this->convertWriteConcernOptions($options)
399
            );
400
        } catch (\MongoDB\Driver\Exception\Exception $e) {
401
            throw ExceptionConverter::toLegacy($e);
402
        }
403
404
        if (! $result->isAcknowledged()) {
405
            return true;
406
        }
407
408
        return [
409
            'ok' => 1.0,
410
            'nModified' => $result->getModifiedCount(),
411
            'n' => $result->getMatchedCount(),
412
            'err' => null,
413
            'errmsg' => null,
414
            'updatedExisting' => $result->getUpsertedCount() == 0 && $result->getModifiedCount() > 0,
415
        ];
416
    }
417
418
    /**
419
     * Remove records from this collection
420
     *
421
     * @link http://www.php.net/manual/en/mongocollection.remove.php
422
     * @param array $criteria Query criteria for the documents to delete.
423
     * @param array $options An array of options for the remove operation.
424
     * @throws MongoCursorException
425
     * @throws MongoCursorTimeoutException
426
     * @return bool|array Returns an array containing the status of the removal
427
     * if the "w" option is set. Otherwise, returns TRUE.
428
     */
429
    public function remove(array $criteria = [], array $options = [])
430
    {
431
        $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
432
        $method = $multiple ? 'deleteMany' : 'deleteOne';
433
434
        try {
435
            /** @var \MongoDB\DeleteResult $result */
436
            $result = $this->collection->$method(
437
                TypeConverter::fromLegacy($criteria),
438
                $this->convertWriteConcernOptions($options)
439
            );
440
        } catch (\MongoDB\Driver\Exception\Exception $e) {
441
            throw ExceptionConverter::toLegacy($e);
442
        }
443
444
        if (! $result->isAcknowledged()) {
445
            return true;
446
        }
447
448
        return [
449
            'ok' => 1.0,
450
            'n' => $result->getDeletedCount(),
451
            'err' => null,
452
            'errmsg' => null
453
        ];
454
    }
455
456
    /**
457
     * Querys this collection
458
     *
459
     * @link http://www.php.net/manual/en/mongocollection.find.php
460
     * @param array $query The fields for which to search.
461
     * @param array $fields Fields of the results to return.
462
     * @return MongoCursor
463
     */
464 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...
465
    {
466
        $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
467
        $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...
468
469
        return $cursor;
470
    }
471
472
    /**
473
     * Retrieve a list of distinct values for the given key across a collection
474
     *
475
     * @link http://www.php.net/manual/ru/mongocollection.distinct.php
476
     * @param string $key The key to use.
477
     * @param array $query An optional query parameters
478
     * @return array|bool Returns an array of distinct values, or FALSE on failure
479
     */
480
    public function distinct($key, array $query = [])
481
    {
482
        try {
483
            return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, TypeConverter::fromLegacy($query)));
484
        } catch (\MongoDB\Driver\Exception\Exception $e) {
485
            return false;
486
        }
487
    }
488
489
    /**
490
     * Update a document and return it
491
     *
492
     * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
493
     * @param array $query The query criteria to search for.
494
     * @param array $update The update criteria.
495
     * @param array $fields Optionally only return these fields.
496
     * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
497
     * @return array Returns the original document, or the modified document when new is set.
498
     */
499
    public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
500
    {
501
        $query = TypeConverter::fromLegacy($query);
502
        try {
503
            if (isset($options['remove'])) {
504
                unset($options['remove']);
505
                $document = $this->collection->findOneAndDelete($query, $options);
506
            } else {
507
                $update = is_array($update) ? $update : [];
508
                if (isset($options['update']) && is_array($options['update'])) {
509
                    $update = $options['update'];
510
                    unset($options['update']);
511
                }
512
513
                $update = TypeConverter::fromLegacy($update);
514
515
                if (isset($options['new'])) {
516
                    $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
517
                    unset($options['new']);
518
                }
519
520
                $options['projection'] = TypeConverter::convertProjection($fields);
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 499 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...
521
522
                if (! \MongoDB\is_first_key_operator($update)) {
523
                    $document = $this->collection->findOneAndReplace($query, $update, $options);
524
                } else {
525
                    $document = $this->collection->findOneAndUpdate($query, $update, $options);
526
                }
527
            }
528
        } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
529
            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
530
        } catch (\MongoDB\Driver\Exception\Exception $e) {
531
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
532
        }
533
534
        if ($document) {
535
            $document = TypeConverter::toLegacy($document);
536
        }
537
538
        return $document;
539
    }
540
541
    /**
542
     * Querys this collection, returning a single element
543
     *
544
     * @link http://www.php.net/manual/en/mongocollection.findone.php
545
     * @param array $query The fields for which to search.
546
     * @param array $fields Fields of the results to return.
547
     * @param array $options
548
     * @return array|null
549
     */
550
    public function findOne($query = [], array $fields = [], array $options = [])
551
    {
552
        // Can't typehint for array since MongoGridFS extends and accepts strings
553
        if (! is_array($query)) {
554
            trigger_error(sprintf('MongoCollection::findOne(): expects parameter 1 to be an array or object, %s given', gettype($query)), E_USER_WARNING);
555
            return;
556
        }
557
558
        $options = ['projection' => TypeConverter::convertProjection($fields)] + $options;
559
        try {
560
            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
561
        } catch (\MongoDB\Driver\Exception\Exception $e) {
562
            throw ExceptionConverter::toLegacy($e);
563
        }
564
565
        if ($document !== null) {
566
            $document = TypeConverter::toLegacy($document);
567
        }
568
569
        return $document;
570
    }
571
572
    /**
573
     * Creates an index on the given field(s), or does nothing if the index already exists
574
     *
575
     * @link http://www.php.net/manual/en/mongocollection.createindex.php
576
     * @param array $keys Field or fields to use as index.
577
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
578
     * @return array Returns the database response.
579
     */
580
    public function createIndex($keys, array $options = [])
581
    {
582
        if (is_string($keys)) {
583
            if (empty($keys)) {
584
                throw new MongoException('empty string passed as key field');
585
            }
586
            $keys = [$keys => 1];
587
        }
588
589
        if (is_object($keys)) {
590
            $keys = (array) $keys;
591
        }
592
593
        if (! is_array($keys) || ! count($keys)) {
594
            throw new MongoException('index specification has no elements');
595
        }
596
597
        if (! isset($options['name'])) {
598
            $options['name'] = \MongoDB\generate_index_name($keys);
599
        }
600
601
        $indexes = iterator_to_array($this->collection->listIndexes());
602
        $indexCount = count($indexes);
603
        $collectionExists = true;
604
        $indexExists = false;
605
606
        // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
607
        if ($indexCount === 0) {
608
            $collectionExists = false;
609
            $indexCount = 1;
610
        }
611
612
        foreach ($indexes as $index) {
613
            if ($index->getKey() === $keys || $index->getName() === $options['name']) {
614
                $indexExists = true;
615
                break;
616
            }
617
        }
618
619
        try {
620
            foreach (['w', 'wTimeoutMS', 'safe', 'timeout', 'wtimeout'] as $invalidOption) {
621
                if (isset($options[$invalidOption])) {
622
                    unset($options[$invalidOption]);
623
                }
624
            }
625
626
            $this->collection->createIndex($keys, $options);
627
        } catch (\MongoDB\Driver\Exception\Exception $e) {
628
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
629
        }
630
631
        $result = [
632
            'createdCollectionAutomatically' => !$collectionExists,
633
            'numIndexesBefore' => $indexCount,
634
            'numIndexesAfter' => $indexCount,
635
            'note' => 'all indexes already exist',
636
            'ok' => 1.0,
637
        ];
638
639
        if (! $indexExists) {
640
            $result['numIndexesAfter']++;
641
            unset($result['note']);
642
        }
643
644
        return $result;
645
    }
646
647
    /**
648
     * Creates an index on the given field(s), or does nothing if the index already exists
649
     *
650
     * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
651
     * @param array $keys Field or fields to use as index.
652
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
653
     * @return array Returns the database response.
654
     * @deprecated Use MongoCollection::createIndex() instead.
655
     */
656
    public function ensureIndex(array $keys, array $options = [])
657
    {
658
        return $this->createIndex($keys, $options);
659
    }
660
661
    /**
662
     * Deletes an index from this collection
663
     *
664
     * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
665
     * @param string|array $keys Field or fields from which to delete the index.
666
     * @return array Returns the database response.
667
     */
668
    public function deleteIndex($keys)
669
    {
670
        if (is_string($keys)) {
671
            $indexName = $keys;
672
            if (! preg_match('#_-?1$#', $indexName)) {
673
                $indexName .= '_1';
674
            }
675
        } elseif (is_array($keys)) {
676
            $indexName = \MongoDB\generate_index_name($keys);
677
        } else {
678
            throw new \InvalidArgumentException();
679
        }
680
681
        try {
682
            return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
683
        } catch (\MongoDB\Driver\Exception\Exception $e) {
684
            return ExceptionConverter::toResultArray($e) + ['nIndexesWas' => count($this->getIndexInfo())];
685
        }
686
    }
687
688
    /**
689
     * Delete all indexes for this collection
690
     *
691
     * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
692
     * @return array Returns the database response.
693
     */
694
    public function deleteIndexes()
695
    {
696
        try {
697
            return TypeConverter::toLegacy($this->collection->dropIndexes());
698
        } catch (\MongoDB\Driver\Exception\Exception $e) {
699
            return ExceptionConverter::toResultArray($e);
700
        }
701
    }
702
703
    /**
704
     * Returns an array of index names for this collection
705
     *
706
     * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
707
     * @return array Returns a list of index names.
708
     */
709
    public function getIndexInfo()
710
    {
711
        $convertIndex = function (\MongoDB\Model\IndexInfo $indexInfo) {
712
            $infos = [
713
                'v' => $indexInfo->getVersion(),
714
                'key' => $indexInfo->getKey(),
715
                'name' => $indexInfo->getName(),
716
                'ns' => $indexInfo->getNamespace(),
717
            ];
718
719
            $additionalKeys = [
720
                'unique',
721
                'sparse',
722
                'partialFilterExpression',
723
                'expireAfterSeconds',
724
                'storageEngine',
725
                'weights',
726
                'default_language',
727
                'language_override',
728
                'textIndexVersion',
729
                'collation',
730
                '2dsphereIndexVersion',
731
                'bucketSize'
732
            ];
733
734
            foreach ($additionalKeys as $key) {
735
                if (! isset($indexInfo[$key])) {
736
                    continue;
737
                }
738
739
                $infos[$key] = $indexInfo[$key];
740
            }
741
742
            return $infos;
743
        };
744
745
        return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
746
    }
747
748
    /**
749
     * Counts the number of documents in this collection
750
     *
751
     * @link http://www.php.net/manual/en/mongocollection.count.php
752
     * @param array|stdClass $query
753
     * @param array $options
754
     * @return int Returns the number of documents matching the query.
755
     */
756
    public function count($query = [], $options = [])
757
    {
758
        try {
759
            // Handle legacy mode - limit and skip as second and third parameters, respectively
760
            if (! is_array($options)) {
761
                $limit = $options;
762
                $options = [];
763
764
                if ($limit !== null) {
765
                    $options['limit'] = (int) $limit;
766
                }
767
768
                if (func_num_args() > 2) {
769
                    $options['skip'] = (int) func_get_args()[2];
770
                }
771
            }
772
773
            return $this->collection->count(TypeConverter::fromLegacy($query), $options);
774
        } catch (\MongoDB\Driver\Exception\Exception $e) {
775
            throw ExceptionConverter::toLegacy($e);
776
        }
777
    }
778
779
    /**
780
     * Saves an object to this collection
781
     *
782
     * @link http://www.php.net/manual/en/mongocollection.save.php
783
     * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
784
     * @param array $options Options for the save.
785
     * @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.
786
     * @throws MongoCursorException if the "w" option is set and the write fails.
787
     * @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.
788
     * @return array|boolean If w was set, returns an array containing the status of the save.
789
     * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
790
     */
791
    public function save(&$a, array $options = [])
792
    {
793
        $id = $this->ensureDocumentHasMongoId($a);
794
795
        $document = (array) $a;
796
797
        $options['upsert'] = true;
798
799
        try {
800
            /** @var \MongoDB\UpdateResult $result */
801
            $result = $this->collection->replaceOne(
802
                TypeConverter::fromLegacy(['_id' => $id]),
803
                TypeConverter::fromLegacy($document),
804
                $this->convertWriteConcernOptions($options)
805
            );
806
807
            if (! $result->isAcknowledged()) {
808
                return true;
809
            }
810
811
            $resultArray = [
812
                'ok' => 1.0,
813
                'nModified' => $result->getModifiedCount(),
814
                'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
815
                'err' => null,
816
                'errmsg' => null,
817
                'updatedExisting' => $result->getUpsertedCount() == 0 && $result->getModifiedCount() > 0,
818
            ];
819
            if ($result->getUpsertedId() !== null) {
820
                $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
821
            }
822
823
            return $resultArray;
824
        } catch (\MongoDB\Driver\Exception\Exception $e) {
825
            throw ExceptionConverter::toLegacy($e);
826
        }
827
    }
828
829
    /**
830
     * Creates a database reference
831
     *
832
     * @link http://www.php.net/manual/en/mongocollection.createdbref.php
833
     * @param array|object $document_or_id Object to which to create a reference.
834
     * @return array Returns a database reference array.
835
     */
836
    public function createDBRef($document_or_id)
837
    {
838 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...
839
            $id = $document_or_id;
840
        } elseif (is_object($document_or_id)) {
841
            if (! isset($document_or_id->_id)) {
842
                return null;
843
            }
844
845
            $id = $document_or_id->_id;
846
        } elseif (is_array($document_or_id)) {
847
            if (! isset($document_or_id['_id'])) {
848
                return null;
849
            }
850
851
            $id = $document_or_id['_id'];
852
        } else {
853
            $id = $document_or_id;
854
        }
855
856
        return MongoDBRef::create($this->name, $id);
857
    }
858
859
    /**
860
     * Fetches the document pointed to by a database reference
861
     *
862
     * @link http://www.php.net/manual/en/mongocollection.getdbref.php
863
     * @param array $ref A database reference.
864
     * @return array Returns the database document pointed to by the reference.
865
     */
866
    public function getDBRef(array $ref)
867
    {
868
        return $this->db->getDBRef($ref);
869
    }
870
871
    /**
872
     * Performs an operation similar to SQL's GROUP BY command
873
     *
874
     * @link http://www.php.net/manual/en/mongocollection.group.php
875
     * @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.
876
     * @param array $initial Initial value of the aggregation counter object.
877
     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
878
     * @param array $condition An condition that must be true for a row to be considered.
879
     * @return array
880
     */
881
    public function group($keys, array $initial, $reduce, array $condition = [])
882
    {
883
        if (is_string($reduce)) {
884
            $reduce = new MongoCode($reduce);
885
        }
886
887
        $command = [
888
            'group' => [
889
                'ns' => $this->name,
890
                '$reduce' => (string)$reduce,
891
                'initial' => $initial,
892
                'cond' => $condition,
893
            ],
894
        ];
895
896
        if ($keys instanceof MongoCode) {
897
            $command['group']['$keyf'] = (string)$keys;
898
        } else {
899
            $command['group']['key'] = $keys;
900
        }
901
        if (array_key_exists('condition', $condition)) {
902
            $command['group']['cond'] = $condition['condition'];
903
        }
904
        if (array_key_exists('finalize', $condition)) {
905
            if ($condition['finalize'] instanceof MongoCode) {
906
                $condition['finalize'] = (string)$condition['finalize'];
907
            }
908
            $command['group']['finalize'] = $condition['finalize'];
909
        }
910
911
        return $this->db->command($command);
912
    }
913
914
    /**
915
     * Returns an array of cursors to iterator over a full collection in parallel
916
     *
917
     * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
918
     * @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.
919
     * @return MongoCommandCursor[]
920
     */
921
    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...
922
    {
923
        $this->notImplemented();
924
    }
925
926
    protected function notImplemented()
927
    {
928
        throw new \Exception('Not implemented');
929
    }
930
931
    /**
932
     * @return \MongoDB\Collection
933
     */
934 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...
935
    {
936
        $options = [
937
            'readPreference' => $this->readPreference,
938
            'writeConcern' => $this->writeConcern,
939
        ];
940
941
        if ($this->collection === null) {
942
            $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
943
        } else {
944
            $this->collection = $this->collection->withOptions($options);
945
        }
946
    }
947
948
    /**
949
     * Converts legacy write concern options to a WriteConcern object
950
     *
951
     * @param array $options
952
     * @return array
953
     */
954
    private function convertWriteConcernOptions(array $options)
955
    {
956
        if (isset($options['safe'])) {
957
            $options['w'] = ($options['safe']) ? 1 : 0;
958
        }
959
960 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...
961
            $options['wTimeoutMS'] = $options['wtimeout'];
962
        }
963
964
        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
965
            $collectionWriteConcern = $this->getWriteConcern();
966
            $writeConcern = $this->createWriteConcernFromParameters(
967
                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
968
                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
969
            );
970
971
            $options['writeConcern'] = $writeConcern;
972
        }
973
974
        unset($options['safe']);
975
        unset($options['w']);
976
        unset($options['wTimeout']);
977
        unset($options['wTimeoutMS']);
978
979
        return $options;
980
    }
981
982
    private function checkKeys($array)
983
    {
984
        foreach (array_keys($array) as $key) {
985
            if (empty($key) && $key !== 0) {
986
                throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
987
            }
988
        }
989
    }
990
991
    /**
992
     * @param array|object $document
993
     * @return MongoId
994
     */
995
    private function ensureDocumentHasMongoId(&$document)
996
    {
997
        if (is_array($document)) {
998
            if (! isset($document['_id'])) {
999
                $document['_id'] = new \MongoId();
1000
            }
1001
1002
            $this->checkKeys($document);
1003
1004
            return $document['_id'];
1005
        } elseif (is_object($document)) {
1006
            $reflectionObject = new \ReflectionObject($document);
1007
            foreach ($reflectionObject->getProperties() as $property) {
1008
                if (! $property->isPublic()) {
1009
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
1010
                }
1011
            }
1012
1013
            if (! isset($document->_id)) {
1014
                $document->_id = new \MongoId();
1015
            }
1016
1017
            $this->checkKeys((array) $document);
1018
1019
            return $document->_id;
1020
        }
1021
1022
        return null;
1023
    }
1024
1025
    private function checkCollectionName($name)
1026
    {
1027
        if (empty($name)) {
1028
            throw new Exception('Collection name cannot be empty');
1029
        } elseif (strpos($name, chr(0)) !== false) {
1030
            throw new Exception('Collection name cannot contain null bytes');
1031
        }
1032
    }
1033
1034
    /**
1035
     * @return array
1036
     */
1037
    public function __sleep()
1038
    {
1039
        return ['db', 'name'];
1040
    }
1041
}
1042