Completed
Pull Request — master (#48)
by
unknown
03:20
created

MongoCollection::findAndModify()   D

Complexity

Conditions 9
Paths 157

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 35
rs 4.6666
cc 9
eloc 22
nc 157
nop 4
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 . '.' . $name);
105
    }
106
107
    /**
108
     * @param string $name
109
     * @param mixed $value
110
     */
111 View Code Duplication
    public function __set($name, $value)
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...
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
        $command = [
148
            'aggregate' => $this->name,
149
            'pipeline' => $pipeline
150
        ];
151
152
        $command += $options;
153
154
        try {
155
            return $this->db->command($command);
156
        } catch (MongoCursorTimeoutException $e) {
157
            throw new MongoExecutionTimeoutException($e->getMessage(), $e->getCode(), $e);
158
        }
159
160
    }
161
162
    /**
163
     * Execute an aggregation pipeline command and retrieve results through a cursor
164
     *
165
     * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
166
     * @param array $pipeline
167
     * @param array $options
168
     * @return MongoCommandCursor
169
     */
170
    public function aggregateCursor(array $pipeline, array $options = [])
171
    {
172
        // Build command manually, can't use mongo-php-library here
173
        $command = [
174
            'aggregate' => $this->name,
175
            'pipeline' => $pipeline
176
        ];
177
178
        // Convert cursor option
179
        if (! isset($options['cursor'])) {
180
            $options['cursor'] = true;
181
        }
182
183
        $command += $options;
184
185
        $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
186
        $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...
187
188
        return $cursor;
189
    }
190
191
    /**
192
     * Returns this collection's name
193
     *
194
     * @link http://www.php.net/manual/en/mongocollection.getname.php
195
     * @return string
196
     */
197
    public function getName()
198
    {
199
        return $this->name;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function setReadPreference($readPreference, $tags = null)
206
    {
207
        $result = $this->setReadPreferenceFromParameters($readPreference, $tags);
208
        $this->createCollectionObject();
209
210
        return $result;
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216
    public function setWriteConcern($wstring, $wtimeout = 0)
217
    {
218
        $result = $this->setWriteConcernFromParameters($wstring, $wtimeout);
219
        $this->createCollectionObject();
220
221
        return $result;
222
    }
223
224
    /**
225
     * Drops this collection
226
     *
227
     * @link http://www.php.net/manual/en/mongocollection.drop.php
228
     * @return array Returns the database response.
229
     */
230
    public function drop()
231
    {
232
        return TypeConverter::toLegacy($this->collection->drop());
233
    }
234
235
    /**
236
     * Validates this collection
237
     *
238
     * @link http://www.php.net/manual/en/mongocollection.validate.php
239
     * @param bool $scan_data Only validate indices, not the base collection.
240
     * @return array Returns the database's evaluation of this object.
241
     */
242
    public function validate($scan_data = FALSE)
243
    {
244
        $command = [
245
            'validate' => $this->name,
246
            'full'     => $scan_data,
247
        ];
248
249
        return $this->db->command($command);
250
    }
251
252
    /**
253
     * Inserts an array into the collection
254
     *
255
     * @link http://www.php.net/manual/en/mongocollection.insert.php
256
     * @param array|object $a
257
     * @param array $options
258
     * @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.
259
     * @throws MongoCursorException if the "w" option is set and the write fails.
260
     * @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.
261
     * @return bool|array Returns an array containing the status of the insertion if the "w" option is set.
262
     */
263
    public function insert(&$a, array $options = [])
264
    {
265
        if (! $this->ensureDocumentHasMongoId($a)) {
266
            trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
267
            return;
268
        }
269
270
        if (! count((array)$a)) {
271
            throw new \MongoException('document must be an array or object');
272
        }
273
274
        try {
275
            $result = $this->collection->insertOne(
276
                TypeConverter::fromLegacy($a),
277
                $this->convertWriteConcernOptions($options)
278
            );
279
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
280
            $writeResult = $e->getWriteResult();
281
            $writeError = $writeResult->getWriteErrors()[0];
282
            return [
283
                'ok' => 0.0,
284
                'n' => 0,
285
                'err' => $writeError->getCode(),
286
                'errmsg' => $writeError->getMessage(),
287
            ];
288
        } catch (\MongoDB\Driver\Exception\Exception $e) {
289
            ExceptionConverter::toLegacy($e);
290
        }
291
292
        if (! $result->isAcknowledged()) {
293
            return true;
294
        }
295
296
        return [
297
            'ok' => 1.0,
298
            'n' => 0,
299
            'err' => null,
300
            'errmsg' => null,
301
        ];
302
    }
303
304
    /**
305
     * Inserts multiple documents into this collection
306
     *
307
     * @link http://www.php.net/manual/en/mongocollection.batchinsert.php
308
     * @param array $a An array of arrays.
309
     * @param array $options Options for the inserts.
310
     * @throws MongoCursorException
311
     * @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.
312
     */
313
    public function batchInsert(array &$a, array $options = [])
314
    {
315
        if (empty($a)) {
316
            throw new \MongoException('No write ops were included in the batch');
317
        }
318
319
        $continueOnError = isset($options['continueOnError']) && $options['continueOnError'];
320
321
        foreach ($a as $key => $item) {
322
            try {
323
                if (! $this->ensureDocumentHasMongoId($a[$key])) {
324
                    if ($continueOnError) {
325
                        unset($a[$key]);
326
                    } else {
327
                        trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
328
                        return;
329
                    }
330
                }
331
            } catch (MongoException $e) {
332
                if ( ! $continueOnError) {
333
                    throw $e;
334
                }
335
            }
336
        }
337
338
        try {
339
            $result = $this->collection->insertMany(
340
                TypeConverter::fromLegacy(array_values($a)),
341
                $this->convertWriteConcernOptions($options)
342
            );
343
        } catch (\MongoDB\Driver\Exception\Exception $e) {
344
            ExceptionConverter::toLegacy($e);
345
        }
346
347
        if (! $result->isAcknowledged()) {
348
            return true;
349
        }
350
351
        return [
352
            'connectionId' => 0,
353
            'n' => 0,
354
            'syncMillis' => 0,
355
            'writtenTo' => null,
356
            'err' => null,
357
            'errmsg' => null,
358
        ];
359
    }
360
361
    /**
362
     * Update records based on a given criteria
363
     *
364
     * @link http://www.php.net/manual/en/mongocollection.update.php
365
     * @param array $criteria Description of the objects to update.
366
     * @param array $newobj The object with which to update the matching records.
367
     * @param array $options
368
     * @throws MongoCursorException
369
     * @return boolean
370
     */
371
    public function update(array $criteria , array $newobj, array $options = [])
372
    {
373
        $multiple = isset($options['multiple']) ? $options['multiple'] : false;
374
        $method = $multiple ? 'updateMany' : 'updateOne';
375
        unset($options['multiple']);
376
377
        try {
378
            /** @var \MongoDB\UpdateResult $result */
379
            $result = $this->collection->$method(
380
                TypeConverter::fromLegacy($criteria),
381
                TypeConverter::fromLegacy($newobj),
382
                $this->convertWriteConcernOptions($options)
383
            );
384
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
385
            $writeResult = $e->getWriteResult();
386
            $writeError = $writeResult->getWriteErrors()[0];
387
            return [
388
                'ok' => 0.0,
389
                'nModified' => $writeResult->getModifiedCount(),
390
                'n' => $writeResult->getMatchedCount(),
391
                'err' => $writeError->getCode(),
392
                'errmsg' => $writeError->getMessage(),
393
                'updatedExisting' => $writeResult->getUpsertedCount() == 0,
394
            ];
395
        } catch (\MongoDB\Driver\Exception\Exception $e) {
396
            ExceptionConverter::toLegacy($e);
397
        }
398
399
        if (! $result->isAcknowledged()) {
400
            return true;
401
        }
402
403
        return [
404
            'ok' => 1.0,
405
            'nModified' => $result->getModifiedCount(),
406
            'n' => $result->getMatchedCount(),
407
            'err' => null,
408
            'errmsg' => null,
409
            'updatedExisting' => $result->getUpsertedCount() == 0,
410
        ];
411
    }
412
413
    /**
414
     * Remove records from this collection
415
     *
416
     * @link http://www.php.net/manual/en/mongocollection.remove.php
417
     * @param array $criteria Query criteria for the documents to delete.
418
     * @param array $options An array of options for the remove operation.
419
     * @throws MongoCursorException
420
     * @throws MongoCursorTimeoutException
421
     * @return bool|array Returns an array containing the status of the removal
422
     * if the "w" option is set. Otherwise, returns TRUE.
423
     */
424
    public function remove(array $criteria = [], array $options = [])
425
    {
426
        $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
427
        $method = $multiple ? 'deleteMany' : 'deleteOne';
428
429
        try {
430
            /** @var \MongoDB\DeleteResult $result */
431
            $result = $this->collection->$method(
432
                TypeConverter::fromLegacy($criteria),
433
                $this->convertWriteConcernOptions($options)
434
            );
435
        } catch (\MongoDB\Driver\Exception\Exception $e) {
436
            ExceptionConverter::toLegacy($e);
437
        }
438
439
        if (! $result->isAcknowledged()) {
440
            return true;
441
        }
442
443
        return [
444
            'ok' => 1.0,
445
            'n' => $result->getDeletedCount(),
446
            'err' => null,
447
            'errmsg' => null
448
        ];
449
    }
450
451
    /**
452
     * Querys this collection
453
     *
454
     * @link http://www.php.net/manual/en/mongocollection.find.php
455
     * @param array $query The fields for which to search.
456
     * @param array $fields Fields of the results to return.
457
     * @return MongoCursor
458
     */
459 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...
460
    {
461
        $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
462
        $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...
463
464
        return $cursor;
465
    }
466
467
    /**
468
     * Retrieve a list of distinct values for the given key across a collection
469
     *
470
     * @link http://www.php.net/manual/ru/mongocollection.distinct.php
471
     * @param string $key The key to use.
472
     * @param array $query An optional query parameters
473
     * @return array|bool Returns an array of distinct values, or FALSE on failure
474
     */
475
    public function distinct($key, array $query = [])
476
    {
477
        try {
478
            return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, $query));
479
        } catch (\MongoDB\Driver\Exception\Exception $e) {
480
            return false;
481
        }
482
    }
483
484
    /**
485
     * Update a document and return it
486
     *
487
     * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
488
     * @param array $query The query criteria to search for.
489
     * @param array $update The update criteria.
490
     * @param array $fields Optionally only return these fields.
491
     * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
492
     * @return array Returns the original document, or the modified document when new is set.
493
     */
494
    public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
495
    {
496
        $query = TypeConverter::fromLegacy($query);
497
        try {
498
            if (isset($options['remove'])) {
499
                unset($options['remove']);
500
                $document = $this->collection->findOneAndDelete($query, $options);
501
            } else {
502
                $update = is_array($update) ? TypeConverter::fromLegacy($update) : [];
503
504
                if (!\MongoDB\is_first_key_operator($update)) {
505
                    $update = ['$set' => $update];
506
                }
507
508
                if (isset($options['new'])) {
509
                    $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
510
                    unset($options['new']);
511
                }
512
513
                $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
514
515
                $document = $this->collection->findOneAndUpdate($query, $update, $options);
516
            }
517
        } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
518
            throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
519
        } catch (\MongoDB\Driver\Exception\Exception $e) {
520
            ExceptionConverter::toLegacy($e, 'MongoResultException');
521
        }
522
523
        if ($document) {
524
            $document = TypeConverter::toLegacy($document);
0 ignored issues
show
Bug introduced by
The variable $document does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
525
        }
526
527
        return $document;
528
    }
529
530
    /**
531
     * Querys this collection, returning a single element
532
     *
533
     * @link http://www.php.net/manual/en/mongocollection.findone.php
534
     * @param array $query The fields for which to search.
535
     * @param array $fields Fields of the results to return.
536
     * @param array $options
537
     * @return array|null
538
     */
539
    public function findOne(array $query = [], array $fields = [], array $options = [])
540
    {
541
        $options = ['projection' => $fields] + $options;
542
        try {
543
            $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
544
        } catch (\MongoDB\Driver\Exception\Exception $e) {
545
            ExceptionConverter::toLegacy($e);
546
        }
547
548
        if ($document !== null) {
549
            $document = TypeConverter::toLegacy($document);
550
        }
551
552
        return $document;
553
    }
554
555
    /**
556
     * Creates an index on the given field(s), or does nothing if the index already exists
557
     *
558
     * @link http://www.php.net/manual/en/mongocollection.createindex.php
559
     * @param array $keys Field or fields to use as index.
560
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
561
     * @return array Returns the database response.
562
     *
563
     * @todo This method does not yet return the correct result
564
     */
565
    public function createIndex($keys, array $options = [])
566
    {
567
        if (is_string($keys)) {
568
            if (empty($keys)) {
569
                throw new MongoException('empty string passed as key field');
570
            }
571
            $keys = [$keys => 1];
572
        }
573
574
        if (is_object($keys)) {
575
            $keys = (array) $keys;
576
        }
577
578
        if (! is_array($keys) || ! count($keys)) {
579
            throw new MongoException('keys cannot be empty');
580
        }
581
582
        // duplicate
583
        $neededOptions = ['unique' => 1, 'sparse' => 1, 'expireAfterSeconds' => 1, 'background' => 1, 'dropDups' => 1];
584
        $indexOptions = array_intersect_key($options, $neededOptions);
585
        $indexes = $this->collection->listIndexes();
586
        foreach ($indexes as $index) {
587
588
            if (! empty($options['name']) && $index->getName() === $options['name']) {
589
                throw new \MongoResultException(sprintf('index with name: %s already exists', $index->getName()));
590
            }
591
592
            if ($index->getKey() == $keys) {
593
                $currentIndexOptions = array_intersect_key($index->__debugInfo(), $neededOptions);
594
595
                unset($currentIndexOptions['name']);
596
                if ($currentIndexOptions != $indexOptions) {
597
                    throw new \MongoResultException('Index with same keys but different options already exists');
598
                }
599
600
                return [
601
                    'createdCollectionAutomatically' => false,
602
                    'numIndexesBefore' => count($indexes),
603
                    'numIndexesAfter' => count($indexes),
604
                    'note' => 'all indexes already exist',
605
                    'ok' => 1.0
606
                ];
607
            }
608
        }
609
610
        try {
611
            $this->collection->createIndex($keys, $this->convertWriteConcernOptions($options));
612
        } catch (\MongoDB\Driver\Exception\Exception $e) {
613
            ExceptionConverter::toLegacy($e);
614
        }
615
616
        return [
617
            'createdCollectionAutomatically' => true,
618
            'numIndexesBefore' => count($indexes),
619
            'numIndexesAfter' => count($indexes) + 1,
620
            'ok' => 1.0
621
        ];
622
    }
623
624
    /**
625
     * Creates an index on the given field(s), or does nothing if the index already exists
626
     *
627
     * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
628
     * @param array $keys Field or fields to use as index.
629
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
630
     * @return boolean always true
631
     * @deprecated Use MongoCollection::createIndex() instead.
632
     */
633
    public function ensureIndex(array $keys, array $options = [])
634
    {
635
        $this->createIndex($keys, $options);
636
637
        return true;
638
    }
639
640
    /**
641
     * Deletes an index from this collection
642
     *
643
     * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
644
     * @param string|array $keys Field or fields from which to delete the index.
645
     * @return array Returns the database response.
646
     */
647
    public function deleteIndex($keys)
648
    {
649
        if (is_string($keys)) {
650
            $indexName = $keys;
651
        } elseif (is_array($keys)) {
652
            $indexName = \MongoDB\generate_index_name($keys);
653
        } else {
654
            throw new \InvalidArgumentException();
655
        }
656
657
        return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
658
    }
659
660
    /**
661
     * Delete all indexes for this collection
662
     *
663
     * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
664
     * @return array Returns the database response.
665
     */
666
    public function deleteIndexes()
667
    {
668
        return TypeConverter::toLegacy($this->collection->dropIndexes());
669
    }
670
671
    /**
672
     * Returns an array of index names for this collection
673
     *
674
     * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
675
     * @return array Returns a list of index names.
676
     */
677
    public function getIndexInfo()
678
    {
679
        $convertIndex = function(\MongoDB\Model\IndexInfo $indexInfo) {
680
            return [
681
                'v' => $indexInfo->getVersion(),
682
                'key' => $indexInfo->getKey(),
683
                'name' => $indexInfo->getName(),
684
                'ns' => $indexInfo->getNamespace(),
685
            ];
686
        };
687
688
        return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
689
    }
690
691
    /**
692
     * Counts the number of documents in this collection
693
     *
694
     * @link http://www.php.net/manual/en/mongocollection.count.php
695
     * @param array|stdClass $query
696
     * @param array $options
697
     * @return int Returns the number of documents matching the query.
698
     */
699
    public function count($query = [], array $options = [])
700
    {
701
        try {
702
            return $this->collection->count(TypeConverter::fromLegacy($query), $options);
703
        } catch (\MongoDB\Driver\Exception\Exception $e) {
704
            ExceptionConverter::toLegacy($e);
705
        }
706
    }
707
708
    /**
709
     * Saves an object to this collection
710
     *
711
     * @link http://www.php.net/manual/en/mongocollection.save.php
712
     * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
713
     * @param array $options Options for the save.
714
     * @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.
715
     * @throws MongoCursorException if the "w" option is set and the write fails.
716
     * @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.
717
     * @return array|boolean If w was set, returns an array containing the status of the save.
718
     * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
719
     */
720
    public function save(&$a, array $options = [])
721
    {
722
        $id = $this->ensureDocumentHasMongoId($a);
723
724
        $document = (array) $a;
725
726
        $options['upsert'] = true;
727
728
        try {
729
            /** @var \MongoDB\UpdateResult $result */
730
            $result = $this->collection->replaceOne(
731
                TypeConverter::fromLegacy(['_id' => $id]),
732
                TypeConverter::fromLegacy($document),
733
                $this->convertWriteConcernOptions($options)
734
            );
735
        } catch (\MongoDB\Driver\Exception\Exception $e) {
736
            ExceptionConverter::toLegacy($e);
737
        }
738
739
        if (!$result->isAcknowledged()) {
740
            return true;
741
        }
742
743
        return [
744
            'ok' => 1.0,
745
            'nModified' => $result->getModifiedCount(),
746
            'n' => $result->getMatchedCount(),
747
            'err' => null,
748
            'errmsg' => null,
749
            'updatedExisting' => $result->getUpsertedCount() == 0,
750
        ];
751
    }
752
753
    /**
754
     * Creates a database reference
755
     *
756
     * @link http://www.php.net/manual/en/mongocollection.createdbref.php
757
     * @param array|object $document_or_id Object to which to create a reference.
758
     * @return array Returns a database reference array.
759
     */
760 View Code Duplication
    public function createDBRef($document_or_id)
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...
761
    {
762
        if ($document_or_id instanceof \MongoId) {
763
            $id = $document_or_id;
764
        } elseif (is_object($document_or_id)) {
765
            if (! isset($document_or_id->_id)) {
766
                return null;
767
            }
768
769
            $id = $document_or_id->_id;
770
        } elseif (is_array($document_or_id)) {
771
            if (! isset($document_or_id['_id'])) {
772
                return null;
773
            }
774
775
            $id = $document_or_id['_id'];
776
        } else {
777
            $id = $document_or_id;
778
        }
779
780
        return MongoDBRef::create($this->name, $id);
781
    }
782
783
    /**
784
     * Fetches the document pointed to by a database reference
785
     *
786
     * @link http://www.php.net/manual/en/mongocollection.getdbref.php
787
     * @param array $ref A database reference.
788
     * @return array Returns the database document pointed to by the reference.
789
     */
790
    public function getDBRef(array $ref)
791
    {
792
        return $this->db->getDBRef($ref);
793
    }
794
795
    /**
796
     * Performs an operation similar to SQL's GROUP BY command
797
     *
798
     * @link http://www.php.net/manual/en/mongocollection.group.php
799
     * @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.
800
     * @param array $initial Initial value of the aggregation counter object.
801
     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
802
     * @param array $condition An condition that must be true for a row to be considered.
803
     * @return array
804
     */
805
    public function group($keys, array $initial, $reduce, array $condition = [])
806
    {
807
        if (is_string($reduce)) {
808
            $reduce = new MongoCode($reduce);
809
        }
810
811
        $command = [
812
            'group' => [
813
                'ns' => $this->name,
814
                '$reduce' => (string)$reduce,
815
                'initial' => $initial,
816
                'cond' => $condition,
817
            ],
818
        ];
819
820
        if ($keys instanceof MongoCode) {
821
            $command['group']['$keyf'] = (string)$keys;
822
        } else {
823
            $command['group']['key'] = $keys;
824
        }
825
        if (array_key_exists('condition', $condition)) {
826
            $command['group']['cond'] = $condition['condition'];
827
        }
828
        if (array_key_exists('finalize', $condition)) {
829
            if ($condition['finalize'] instanceof MongoCode) {
830
                $condition['finalize'] = (string)$condition['finalize'];
831
            }
832
            $command['group']['finalize'] = $condition['finalize'];
833
        }
834
835
        return $this->db->command($command);
836
    }
837
838
    /**
839
     * Returns an array of cursors to iterator over a full collection in parallel
840
     *
841
     * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
842
     * @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.
843
     * @return MongoCommandCursor[]
844
     */
845
    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...
846
    {
847
        $this->notImplemented();
848
    }
849
850
    protected function notImplemented()
851
    {
852
        throw new \Exception('Not implemented');
853
    }
854
855
    /**
856
     * @return \MongoDB\Collection
857
     */
858 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...
859
    {
860
        $options = [
861
            'readPreference' => $this->readPreference,
862
            'writeConcern' => $this->writeConcern,
863
        ];
864
865
        if ($this->collection === null) {
866
            $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
867
        } else {
868
            $this->collection = $this->collection->withOptions($options);
869
        }
870
    }
871
872
    /**
873
     * Converts legacy write concern options to a WriteConcern object
874
     *
875
     * @param array $options
876
     * @return array
877
     */
878
    private function convertWriteConcernOptions(array $options)
879
    {
880
        if (isset($options['safe'])) {
881
            $options['w'] = ($options['safe']) ? 1 : 0;
882
        }
883
884
        if (isset($options['wtimeout']) && !isset($options['wTimeoutMS'])) {
885
            $options['wTimeoutMS'] = $options['wtimeout'];
886
        }
887
888
        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
889
            $collectionWriteConcern = $this->getWriteConcern();
890
            $writeConcern = $this->createWriteConcernFromParameters(
891
                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
892
                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
893
            );
894
895
            $options['writeConcern'] = $writeConcern;
896
        }
897
898
        unset($options['safe']);
899
        unset($options['w']);
900
        unset($options['wTimeout']);
901
        unset($options['wTimeoutMS']);
902
903
        return $options;
904
    }
905
906
    /**
907
     * @param array|object $document
908
     * @return MongoId
909
     */
910
    private function ensureDocumentHasMongoId(&$document)
911
    {
912
        $checkKeys = function($array) {
913
            foreach (array_keys($array) as $key) {
914
                if (is_int($key) || empty($key) || strpos($key, '*') === 1) {
915
                    throw new \MongoException('document contain invalid key');
916
                }
917
            }
918
        };
919
920
        if (is_array($document)) {
921
            if (empty($document)) {
922
                throw new \MongoException('document cannot be empty');
923
            }
924
            if (! isset($document['_id'])) {
925
                $document['_id'] = new \MongoId();
926
            }
927
928
            $checkKeys($document);
929
930
            return $document['_id'];
931
        } elseif (is_object($document)) {
932
            if (empty((array) $document)) {
933
                throw new \MongoException('document cannot be empty');
934
            }
935
            if (! isset($document->_id)) {
936
                $document->_id = new \MongoId();
937
            }
938
939
            $checkKeys((array) $document);
940
941
            return $document->_id;
942
        }
943
944
        return null;
945
    }
946
947
    private function checkCollectionName($name)
948
    {
949
        if (empty($name)) {
950
            throw new Exception('Collection name cannot be empty');
951
        } elseif (strpos($name, chr(0)) !== false) {
952
            throw new Exception('Collection name cannot contain null bytes');
953
        }
954
    }
955
}
956
957