Completed
Pull Request — master (#64)
by Andreas
03:07
created

MongoCollection   F

Complexity

Total Complexity 136

Size/Duplication

Total Lines 952
Duplicated Lines 3.89 %

Coupling/Cohesion

Components 1
Dependencies 20
Metric Value
wmc 136
lcom 1
cbo 20
dl 37
loc 952
rs 1.263

37 Methods

Rating   Name   Duplication   Size   Complexity  
A drop() 0 4 1
A getCollection() 0 4 1
A __toString() 0 4 1
A __construct() 0 11 1
A __get() 0 9 3
A __set() 0 7 3
C aggregate() 0 38 7
A aggregateCursor() 0 20 2
A getName() 0 4 1
A setReadPreference() 0 7 1
A setWriteConcern() 0 7 1
A validate() 0 9 1
B insert() 0 31 5
C batchInsert() 0 47 10
C update() 0 37 8
B remove() 0 26 5
A find() 7 7 1
A distinct() 0 8 2
D findAndModify() 0 35 9
A findOne() 0 15 3
A ensureIndex() 0 4 1
B deleteIndex() 0 19 5
A deleteIndexes() 0 8 2
A getIndexInfo() 0 13 1
A count() 0 8 2
B save() 0 37 4
B createDBRef() 17 22 6
A getDBRef() 0 4 1
B group() 0 32 6
A parallelCollectionScan() 0 4 1
A notImplemented() 0 4 1
A createCollectionObject() 13 13 2
D convertWriteConcernOptions() 0 27 9
D ensureDocumentHasMongoId() 0 37 10
A checkCollectionName() 0 8 3
D createIndex() 0 66 15
A __sleep() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MongoCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MongoCollection, and based on these observations, apply Extract Interface, too.

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
use Alcaeus\MongoDbAdapter\Helper;
17
use Alcaeus\MongoDbAdapter\TypeConverter;
18
use Alcaeus\MongoDbAdapter\ExceptionConverter;
19
20
/**
21
 * Represents a database collection.
22
 * @link http://www.php.net/manual/en/class.mongocollection.php
23
 */
24
class MongoCollection
25
{
26
    use Helper\ReadPreference;
27
    use Helper\SlaveOkay;
28
    use Helper\WriteConcern;
29
30
    const ASCENDING = 1;
31
    const DESCENDING = -1;
32
33
    /**
34
     * @var MongoDB
35
     */
36
    public $db = NULL;
37
38
    /**
39
     * @var string
40
     */
41
    protected $name;
42
43
    /**
44
     * @var \MongoDB\Collection
45
     */
46
    protected $collection;
47
48
    /**
49
     * Creates a new collection
50
     *
51
     * @link http://www.php.net/manual/en/mongocollection.construct.php
52
     * @param MongoDB $db Parent database.
53
     * @param string $name Name for this collection.
54
     * @throws Exception
55
     */
56
    public function __construct(MongoDB $db, $name)
57
    {
58
        $this->checkCollectionName($name);
59
        $this->db = $db;
60
        $this->name = $name;
61
62
        $this->setReadPreferenceFromArray($db->getReadPreference());
63
        $this->setWriteConcernFromArray($db->getWriteConcern());
64
65
        $this->createCollectionObject();
66
    }
67
68
    /**
69
     * Gets the underlying collection for this object
70
     *
71
     * @internal This part is not of the ext-mongo API and should not be used
72
     * @return \MongoDB\Collection
73
     */
74
    public function getCollection()
75
    {
76
        return $this->collection;
77
    }
78
79
    /**
80
     * String representation of this collection
81
     *
82
     * @link http://www.php.net/manual/en/mongocollection.--tostring.php
83
     * @return string Returns the full name of this collection.
84
     */
85
    public function __toString()
86
    {
87
        return (string) $this->db . '.' . $this->name;
88
    }
89
90
    /**
91
     * Gets a collection
92
     *
93
     * @link http://www.php.net/manual/en/mongocollection.get.php
94
     * @param string $name The next string in the collection name.
95
     * @return MongoCollection
96
     */
97
    public function __get($name)
98
    {
99
        // Handle w and wtimeout properties that replicate data stored in $readPreference
100
        if ($name === 'w' || $name === 'wtimeout') {
101
            return $this->getWriteConcern()[$name];
102
        }
103
104
        return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
105
    }
106
107
    /**
108
     * @param string $name
109
     * @param mixed $value
110
     */
111
    public function __set($name, $value)
112
    {
113
        if ($name === 'w' || $name === 'wtimeout') {
114
            $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
115
            $this->createCollectionObject();
116
        }
117
    }
118
119
    /**
120
     * Perform an aggregation using the aggregation framework
121
     *
122
     * @link http://www.php.net/manual/en/mongocollection.aggregate.php
123
     * @param array $pipeline
124
     * @param array $op
125
     * @return array
126
     */
127
    public function aggregate(array $pipeline, array $op = [])
128
    {
129
        if (! TypeConverter::isNumericArray($pipeline)) {
130
            $pipeline = [];
131
            $options = [];
132
133
            $i = 0;
134
            foreach (func_get_args() as $operator) {
135
                $i++;
136
                if (! is_array($operator)) {
137
                    trigger_error("Argument $i is not an array", E_WARNING);
138
                    return;
139
                }
140
141
                $pipeline[] = $operator;
142
            }
143
        } else {
144
            $options = $op;
145
        }
146
147
        if (isset($options['cursor'])) {
148
            $options['useCursor'] = true;
149
150
            if (isset($options['cursor']['batchSize'])) {
151
                $options['batchSize'] = $options['cursor']['batchSize'];
152
            }
153
154
            unset($options['cursor']);
155
        } else {
156
            $options['useCursor'] = false;
157
        }
158
159
        try {
160
            return $this->collection->aggregate(TypeConverter::fromLegacy($pipeline), $options);
161
        } catch (\MongoDB\Driver\Exception\Exception $e) {
162
            throw ExceptionConverter::toLegacy($e);
163
        }
164
    }
165
166
    /**
167
     * Execute an aggregation pipeline command and retrieve results through a cursor
168
     *
169
     * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
170
     * @param array $pipeline
171
     * @param array $options
172
     * @return MongoCommandCursor
173
     */
174
    public function aggregateCursor(array $pipeline, array $options = [])
175
    {
176
        // Build command manually, can't use mongo-php-library here
177
        $command = [
178
            'aggregate' => $this->name,
179
            'pipeline' => $pipeline
180
        ];
181
182
        // Convert cursor option
183
        if (! isset($options['cursor'])) {
184
            $options['cursor'] = new \stdClass();
185
        }
186
187
        $command += $options;
188
189
        $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
190
        $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...
191
192
        return $cursor;
193
    }
194
195
    /**
196
     * Returns this collection's name
197
     *
198
     * @link http://www.php.net/manual/en/mongocollection.getname.php
199
     * @return string
200
     */
201
    public function getName()
202
    {
203
        return $this->name;
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209
    public function setReadPreference($readPreference, $tags = null)
210
    {
211
        $result = $this->setReadPreferenceFromParameters($readPreference, $tags);
212
        $this->createCollectionObject();
213
214
        return $result;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function setWriteConcern($wstring, $wtimeout = 0)
221
    {
222
        $result = $this->setWriteConcernFromParameters($wstring, $wtimeout);
223
        $this->createCollectionObject();
224
225
        return $result;
226
    }
227
228
    /**
229
     * Drops this collection
230
     *
231
     * @link http://www.php.net/manual/en/mongocollection.drop.php
232
     * @return array Returns the database response.
233
     */
234
    public function drop()
235
    {
236
        return TypeConverter::toLegacy($this->collection->drop());
237
    }
238
239
    /**
240
     * Validates this collection
241
     *
242
     * @link http://www.php.net/manual/en/mongocollection.validate.php
243
     * @param bool $scan_data Only validate indices, not the base collection.
244
     * @return array Returns the database's evaluation of this object.
245
     */
246
    public function validate($scan_data = FALSE)
247
    {
248
        $command = [
249
            'validate' => $this->name,
250
            'full'     => $scan_data,
251
        ];
252
253
        return $this->db->command($command);
254
    }
255
256
    /**
257
     * Inserts an array into the collection
258
     *
259
     * @link http://www.php.net/manual/en/mongocollection.insert.php
260
     * @param array|object $a
261
     * @param array $options
262
     * @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.
263
     * @throws MongoCursorException if the "w" option is set and the write fails.
264
     * @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.
265
     * @return bool|array Returns an array containing the status of the insertion if the "w" option is set.
266
     */
267
    public function insert(&$a, array $options = [])
268
    {
269
        if (! $this->ensureDocumentHasMongoId($a)) {
270
            trigger_error(sprintf('%s(): expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
271
            return;
272
        }
273
274
        if (! count((array)$a)) {
275
            throw new \MongoException('document must be an array or object');
276
        }
277
278
        try {
279
            $result = $this->collection->insertOne(
280
                TypeConverter::fromLegacy($a),
281
                $this->convertWriteConcernOptions($options)
282
            );
283
        } catch (\MongoDB\Driver\Exception\Exception $e) {
284
            throw ExceptionConverter::toLegacy($e);
285
        }
286
287
        if (! $result->isAcknowledged()) {
288
            return true;
289
        }
290
291
        return [
292
            'ok' => 1.0,
293
            'n' => 0,
294
            'err' => null,
295
            'errmsg' => null,
296
        ];
297
    }
298
299
    /**
300
     * Inserts multiple documents into this collection
301
     *
302
     * @link http://www.php.net/manual/en/mongocollection.batchinsert.php
303
     * @param array $a An array of arrays.
304
     * @param array $options Options for the inserts.
305
     * @throws MongoCursorException
306
     * @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.
307
     */
308
    public function batchInsert(array &$a, array $options = [])
309
    {
310
        if (empty($a)) {
311
            throw new \MongoException('No write ops were included in the batch');
312
        }
313
314
        $continueOnError = isset($options['continueOnError']) && $options['continueOnError'];
315
316
        foreach ($a as $key => $item) {
317
            try {
318
                if (! $this->ensureDocumentHasMongoId($a[$key])) {
319
                    if ($continueOnError) {
320
                        unset($a[$key]);
321
                    } else {
322
                        trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
323
                        return;
324
                    }
325
                }
326
            } catch (MongoException $e) {
327
                if ( ! $continueOnError) {
328
                    throw $e;
329
                }
330
            }
331
        }
332
333
        try {
334
            $result = $this->collection->insertMany(
335
                TypeConverter::fromLegacy(array_values($a)),
336
                $this->convertWriteConcernOptions($options)
337
            );
338
        } catch (\MongoDB\Driver\Exception\Exception $e) {
339
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
340
        }
341
342
        if (! $result->isAcknowledged()) {
343
            return true;
344
        }
345
346
        return [
347
            'ok' => 1.0,
348
            'connectionId' => 0,
349
            'n' => 0,
350
            'syncMillis' => 0,
351
            'writtenTo' => null,
352
            'err' => null,
353
        ];
354
    }
355
356
    /**
357
     * Update records based on a given criteria
358
     *
359
     * @link http://www.php.net/manual/en/mongocollection.update.php
360
     * @param array $criteria Description of the objects to update.
361
     * @param array $newobj The object with which to update the matching records.
362
     * @param array $options
363
     * @throws MongoCursorException
364
     * @return boolean
365
     */
366
    public function update(array $criteria , array $newobj, array $options = [])
367
    {
368
        $multiple = isset($options['multiple']) ? $options['multiple'] : false;
369
        $isReplace = ! \MongoDB\is_first_key_operator($newobj);
370
371
        if ($isReplace && $multiple) {
372
            throw new \MongoWriteConcernException('multi update only works with $ operators', 9);
373
        }
374
        unset($options['multiple']);
375
376
        $method = $isReplace ? 'replace' : 'update';
377
        $method .= $multiple ? 'Many' : 'One';
378
379
        try {
380
            /** @var \MongoDB\UpdateResult $result */
381
            $result = $this->collection->$method(
382
                TypeConverter::fromLegacy($criteria),
383
                TypeConverter::fromLegacy($newobj),
384
                $this->convertWriteConcernOptions($options)
385
            );
386
        } catch (\MongoDB\Driver\Exception\Exception $e) {
387
            throw ExceptionConverter::toLegacy($e);
388
        }
389
390
        if (! $result->isAcknowledged()) {
391
            return true;
392
        }
393
394
        return [
395
            'ok' => 1.0,
396
            'nModified' => $result->getModifiedCount(),
397
            'n' => $result->getMatchedCount(),
398
            'err' => null,
399
            'errmsg' => null,
400
            'updatedExisting' => $result->getUpsertedCount() == 0,
401
        ];
402
    }
403
404
    /**
405
     * Remove records from this collection
406
     *
407
     * @link http://www.php.net/manual/en/mongocollection.remove.php
408
     * @param array $criteria Query criteria for the documents to delete.
409
     * @param array $options An array of options for the remove operation.
410
     * @throws MongoCursorException
411
     * @throws MongoCursorTimeoutException
412
     * @return bool|array Returns an array containing the status of the removal
413
     * if the "w" option is set. Otherwise, returns TRUE.
414
     */
415
    public function remove(array $criteria = [], array $options = [])
416
    {
417
        $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
418
        $method = $multiple ? 'deleteMany' : 'deleteOne';
419
420
        try {
421
            /** @var \MongoDB\DeleteResult $result */
422
            $result = $this->collection->$method(
423
                TypeConverter::fromLegacy($criteria),
424
                $this->convertWriteConcernOptions($options)
425
            );
426
        } catch (\MongoDB\Driver\Exception\Exception $e) {
427
            throw ExceptionConverter::toLegacy($e);
428
        }
429
430
        if (! $result->isAcknowledged()) {
431
            return true;
432
        }
433
434
        return [
435
            'ok' => 1.0,
436
            'n' => $result->getDeletedCount(),
437
            'err' => null,
438
            'errmsg' => null
439
        ];
440
    }
441
442
    /**
443
     * Querys this collection
444
     *
445
     * @link http://www.php.net/manual/en/mongocollection.find.php
446
     * @param array $query The fields for which to search.
447
     * @param array $fields Fields of the results to return.
448
     * @return MongoCursor
449
     */
450 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...
451
    {
452
        $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
453
        $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...
454
455
        return $cursor;
456
    }
457
458
    /**
459
     * Retrieve a list of distinct values for the given key across a collection
460
     *
461
     * @link http://www.php.net/manual/ru/mongocollection.distinct.php
462
     * @param string $key The key to use.
463
     * @param array $query An optional query parameters
464
     * @return array|bool Returns an array of distinct values, or FALSE on failure
465
     */
466
    public function distinct($key, array $query = [])
467
    {
468
        try {
469
            return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, $query));
470
        } catch (\MongoDB\Driver\Exception\Exception $e) {
471
            return false;
472
        }
473
    }
474
475
    /**
476
     * Update a document and return it
477
     *
478
     * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
479
     * @param array $query The query criteria to search for.
480
     * @param array $update The update criteria.
481
     * @param array $fields Optionally only return these fields.
482
     * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
483
     * @return array Returns the original document, or the modified document when new is set.
484
     */
485
    public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
486
    {
487
        $query = TypeConverter::fromLegacy($query);
488
        try {
489
            if (isset($options['remove'])) {
490
                unset($options['remove']);
491
                $document = $this->collection->findOneAndDelete($query, $options);
492
            } else {
493
                $update = is_array($update) ? TypeConverter::fromLegacy($update) : [];
494
495
                if (isset($options['new'])) {
496
                    $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
497
                    unset($options['new']);
498
                }
499
500
                $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
501
502
                if (! \MongoDB\is_first_key_operator($update)) {
503
                    $document = $this->collection->findOneAndReplace($query, $update, $options);
504
                } else {
505
                    $document = $this->collection->findOneAndUpdate($query, $update, $options);
506
                }
507
            }
508
        } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
509
            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
510
        } catch (\MongoDB\Driver\Exception\Exception $e) {
511
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
512
        }
513
514
        if ($document) {
515
            $document = TypeConverter::toLegacy($document);
516
        }
517
518
        return $document;
519
    }
520
521
    /**
522
     * Querys this collection, returning a single element
523
     *
524
     * @link http://www.php.net/manual/en/mongocollection.findone.php
525
     * @param array $query The fields for which to search.
526
     * @param array $fields Fields of the results to return.
527
     * @param array $options
528
     * @return array|null
529
     */
530
    public function findOne(array $query = [], array $fields = [], array $options = [])
531
    {
532
        $options = ['projection' => $fields] + $options;
533
        try {
534
            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
535
        } catch (\MongoDB\Driver\Exception\Exception $e) {
536
            throw ExceptionConverter::toLegacy($e);
537
        }
538
539
        if ($document !== null) {
540
            $document = TypeConverter::toLegacy($document);
541
        }
542
543
        return $document;
544
    }
545
546
    /**
547
     * Creates an index on the given field(s), or does nothing if the index already exists
548
     *
549
     * @link http://www.php.net/manual/en/mongocollection.createindex.php
550
     * @param array $keys Field or fields to use as index.
551
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
552
     * @return array Returns the database response.
553
     */
554
    public function createIndex($keys, array $options = [])
555
    {
556
        if (is_string($keys)) {
557
            if (empty($keys)) {
558
                throw new MongoException('empty string passed as key field');
559
            }
560
            $keys = [$keys => 1];
561
        }
562
563
        if (is_object($keys)) {
564
            $keys = (array) $keys;
565
        }
566
567
        if (! is_array($keys) || ! count($keys)) {
568
            throw new MongoException('index specification has no elements');
569
        }
570
571
        if (! isset($options['name'])) {
572
            $options['name'] = \MongoDB\generate_index_name($keys);
573
        }
574
575
        $indexes = iterator_to_array($this->collection->listIndexes());
576
        $indexCount = count($indexes);
577
        $collectionExists = true;
578
        $indexExists = false;
579
580
        // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
581
        if ($indexCount === 0) {
582
            $collectionExists = false;
583
            $indexCount = 1;
584
        }
585
586
        foreach ($indexes as $index) {
587
            if ($index->getKey() === $keys || $index->getName() === $options['name']) {
588
                $indexExists = true;
589
                break;
590
            }
591
        }
592
593
        try {
594
            foreach (['w', 'wTimeoutMS', 'safe', 'timeout', 'wtimeout'] as $invalidOption) {
595
                if (isset($options[$invalidOption])) {
596
                    unset($options[$invalidOption]);
597
                }
598
            }
599
600
            $this->collection->createIndex($keys, $options);
601
        } catch (\MongoDB\Driver\Exception\Exception $e) {
602
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
603
        }
604
605
        $result = [
606
            'createdCollectionAutomatically' => !$collectionExists,
607
            'numIndexesBefore' => $indexCount,
608
            'numIndexesAfter' => $indexCount,
609
            'note' => 'all indexes already exist',
610
            'ok' => 1.0,
611
        ];
612
613
        if (! $indexExists) {
614
            $result['numIndexesAfter']++;
615
            unset($result['note']);
616
        }
617
618
        return $result;
619
    }
620
621
    /**
622
     * Creates an index on the given field(s), or does nothing if the index already exists
623
     *
624
     * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
625
     * @param array $keys Field or fields to use as index.
626
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
627
     * @return array Returns the database response.
628
     * @deprecated Use MongoCollection::createIndex() instead.
629
     */
630
    public function ensureIndex(array $keys, array $options = [])
631
    {
632
        return $this->createIndex($keys, $options);
633
    }
634
635
    /**
636
     * Deletes an index from this collection
637
     *
638
     * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
639
     * @param string|array $keys Field or fields from which to delete the index.
640
     * @return array Returns the database response.
641
     */
642
    public function deleteIndex($keys)
643
    {
644
        if (is_string($keys)) {
645
            $indexName = $keys;
646
            if (! preg_match('#_-?1$#', $indexName)) {
647
                $indexName .= '_1';
648
            }
649
        } elseif (is_array($keys)) {
650
            $indexName = \MongoDB\generate_index_name($keys);
651
        } else {
652
            throw new \InvalidArgumentException();
653
        }
654
655
        try {
656
            return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
657
        } catch (\MongoDB\Driver\Exception\Exception $e) {
658
            return ExceptionConverter::toResultArray($e) + ['nIndexesWas' => count($this->getIndexInfo())];
659
        }
660
    }
661
662
    /**
663
     * Delete all indexes for this collection
664
     *
665
     * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
666
     * @return array Returns the database response.
667
     */
668
    public function deleteIndexes()
669
    {
670
        try {
671
            return TypeConverter::toLegacy($this->collection->dropIndexes());
672
        } catch (\MongoDB\Driver\Exception\Exception $e) {
673
            return ExceptionConverter::toResultArray($e);
674
        }
675
    }
676
677
    /**
678
     * Returns an array of index names for this collection
679
     *
680
     * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
681
     * @return array Returns a list of index names.
682
     */
683
    public function getIndexInfo()
684
    {
685
        $convertIndex = function(\MongoDB\Model\IndexInfo $indexInfo) {
686
            return [
687
                'v' => $indexInfo->getVersion(),
688
                'key' => $indexInfo->getKey(),
689
                'name' => $indexInfo->getName(),
690
                'ns' => $indexInfo->getNamespace(),
691
            ];
692
        };
693
694
        return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
695
    }
696
697
    /**
698
     * Counts the number of documents in this collection
699
     *
700
     * @link http://www.php.net/manual/en/mongocollection.count.php
701
     * @param array|stdClass $query
702
     * @param array $options
703
     * @return int Returns the number of documents matching the query.
704
     */
705
    public function count($query = [], array $options = [])
706
    {
707
        try {
708
            return $this->collection->count(TypeConverter::fromLegacy($query), $options);
709
        } catch (\MongoDB\Driver\Exception\Exception $e) {
710
            throw ExceptionConverter::toLegacy($e);
711
        }
712
    }
713
714
    /**
715
     * Saves an object to this collection
716
     *
717
     * @link http://www.php.net/manual/en/mongocollection.save.php
718
     * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
719
     * @param array $options Options for the save.
720
     * @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.
721
     * @throws MongoCursorException if the "w" option is set and the write fails.
722
     * @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.
723
     * @return array|boolean If w was set, returns an array containing the status of the save.
724
     * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
725
     */
726
    public function save(&$a, array $options = [])
727
    {
728
        $id = $this->ensureDocumentHasMongoId($a);
729
730
        $document = (array) $a;
731
732
        $options['upsert'] = true;
733
734
        try {
735
            /** @var \MongoDB\UpdateResult $result */
736
            $result = $this->collection->replaceOne(
737
                TypeConverter::fromLegacy(['_id' => $id]),
738
                TypeConverter::fromLegacy($document),
739
                $this->convertWriteConcernOptions($options)
740
            );
741
742
            if (! $result->isAcknowledged()) {
743
                return true;
744
            }
745
746
            $resultArray = [
747
                'ok' => 1.0,
748
                'nModified' => $result->getModifiedCount(),
749
                'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
750
                'err' => null,
751
                'errmsg' => null,
752
                'updatedExisting' => $result->getUpsertedCount() == 0,
753
            ];
754
            if ($result->getUpsertedId() !== null) {
755
                $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
756
            }
757
758
            return $resultArray;
759
        } catch (\MongoDB\Driver\Exception\Exception $e) {
760
            throw ExceptionConverter::toLegacy($e);
761
        }
762
    }
763
764
    /**
765
     * Creates a database reference
766
     *
767
     * @link http://www.php.net/manual/en/mongocollection.createdbref.php
768
     * @param array|object $document_or_id Object to which to create a reference.
769
     * @return array Returns a database reference array.
770
     */
771
    public function createDBRef($document_or_id)
772
    {
773 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...
774
            $id = $document_or_id;
775
        } elseif (is_object($document_or_id)) {
776
            if (! isset($document_or_id->_id)) {
777
                return null;
778
            }
779
780
            $id = $document_or_id->_id;
781
        } elseif (is_array($document_or_id)) {
782
            if (! isset($document_or_id['_id'])) {
783
                return null;
784
            }
785
786
            $id = $document_or_id['_id'];
787
        } else {
788
            $id = $document_or_id;
789
        }
790
791
        return MongoDBRef::create($this->name, $id);
792
    }
793
794
    /**
795
     * Fetches the document pointed to by a database reference
796
     *
797
     * @link http://www.php.net/manual/en/mongocollection.getdbref.php
798
     * @param array $ref A database reference.
799
     * @return array Returns the database document pointed to by the reference.
800
     */
801
    public function getDBRef(array $ref)
802
    {
803
        return $this->db->getDBRef($ref);
804
    }
805
806
    /**
807
     * Performs an operation similar to SQL's GROUP BY command
808
     *
809
     * @link http://www.php.net/manual/en/mongocollection.group.php
810
     * @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.
811
     * @param array $initial Initial value of the aggregation counter object.
812
     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
813
     * @param array $condition An condition that must be true for a row to be considered.
814
     * @return array
815
     */
816
    public function group($keys, array $initial, $reduce, array $condition = [])
817
    {
818
        if (is_string($reduce)) {
819
            $reduce = new MongoCode($reduce);
820
        }
821
822
        $command = [
823
            'group' => [
824
                'ns' => $this->name,
825
                '$reduce' => (string)$reduce,
826
                'initial' => $initial,
827
                'cond' => $condition,
828
            ],
829
        ];
830
831
        if ($keys instanceof MongoCode) {
832
            $command['group']['$keyf'] = (string)$keys;
833
        } else {
834
            $command['group']['key'] = $keys;
835
        }
836
        if (array_key_exists('condition', $condition)) {
837
            $command['group']['cond'] = $condition['condition'];
838
        }
839
        if (array_key_exists('finalize', $condition)) {
840
            if ($condition['finalize'] instanceof MongoCode) {
841
                $condition['finalize'] = (string)$condition['finalize'];
842
            }
843
            $command['group']['finalize'] = $condition['finalize'];
844
        }
845
846
        return $this->db->command($command);
847
    }
848
849
    /**
850
     * Returns an array of cursors to iterator over a full collection in parallel
851
     *
852
     * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
853
     * @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.
854
     * @return MongoCommandCursor[]
855
     */
856
    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...
857
    {
858
        $this->notImplemented();
859
    }
860
861
    protected function notImplemented()
862
    {
863
        throw new \Exception('Not implemented');
864
    }
865
866
    /**
867
     * @return \MongoDB\Collection
868
     */
869 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...
870
    {
871
        $options = [
872
            'readPreference' => $this->readPreference,
873
            'writeConcern' => $this->writeConcern,
874
        ];
875
876
        if ($this->collection === null) {
877
            $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
878
        } else {
879
            $this->collection = $this->collection->withOptions($options);
880
        }
881
    }
882
883
    /**
884
     * Converts legacy write concern options to a WriteConcern object
885
     *
886
     * @param array $options
887
     * @return array
888
     */
889
    private function convertWriteConcernOptions(array $options)
890
    {
891
        if (isset($options['safe'])) {
892
            $options['w'] = ($options['safe']) ? 1 : 0;
893
        }
894
895
        if (isset($options['wtimeout']) && !isset($options['wTimeoutMS'])) {
896
            $options['wTimeoutMS'] = $options['wtimeout'];
897
        }
898
899
        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
900
            $collectionWriteConcern = $this->getWriteConcern();
901
            $writeConcern = $this->createWriteConcernFromParameters(
902
                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
903
                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
904
            );
905
906
            $options['writeConcern'] = $writeConcern;
907
        }
908
909
        unset($options['safe']);
910
        unset($options['w']);
911
        unset($options['wTimeout']);
912
        unset($options['wTimeoutMS']);
913
914
        return $options;
915
    }
916
917
    /**
918
     * @param array|object $document
919
     * @return MongoId
920
     */
921
    private function ensureDocumentHasMongoId(&$document)
922
    {
923
        $checkKeys = function($array) {
924
            foreach (array_keys($array) as $key) {
925
                if (empty($key) && $key !== 0) {
926
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
927
                }
928
            }
929
        };
930
931
        if (is_array($document)) {
932
            if (! isset($document['_id'])) {
933
                $document['_id'] = new \MongoId();
934
            }
935
936
            $checkKeys($document);
937
938
            return $document['_id'];
939
        } elseif (is_object($document)) {
940
            $reflectionObject = new \ReflectionObject($document);
941
            foreach ($reflectionObject->getProperties() as $property) {
942
                if (! $property->isPublic()) {
943
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
944
                }
945
            }
946
947
            if (! isset($document->_id)) {
948
                $document->_id = new \MongoId();
949
            }
950
951
            $checkKeys((array) $document);
952
953
            return $document->_id;
954
        }
955
956
        return null;
957
    }
958
959
    private function checkCollectionName($name)
960
    {
961
        if (empty($name)) {
962
            throw new Exception('Collection name cannot be empty');
963
        } elseif (strpos($name, chr(0)) !== false) {
964
            throw new Exception('Collection name cannot contain null bytes');
965
        }
966
    }
967
968
    /**
969
     * @return array
970
     */
971
    public function __sleep()
972
    {
973
        return ['db', 'name'];
974
    }
975
}
976
977