Completed
Push — master ( c139c0...8415fd )
by Andreas
10s
created

MongoCollection::mustBeArrayOrObject()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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