Completed
Pull Request — master (#193)
by
unknown
04:51 queued 56s
created

MongoCollection::update()   C

Complexity

Conditions 8
Paths 26

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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