Completed
Pull Request — master (#195)
by
unknown
02:40
created

MongoCollection::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 4
c 0
b 0
f 0
cc 1
eloc 2
nop 0
rs 10
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 */
15
16
if (class_exists('MongoCollection', false)) {
17
    return;
18
}
19
20
use Alcaeus\MongoDbAdapter\Helper;
21
use Alcaeus\MongoDbAdapter\TypeConverter;
22
use Alcaeus\MongoDbAdapter\ExceptionConverter;
23
24
/**
25
 * Represents a database collection.
26
 * @link http://www.php.net/manual/en/class.mongocollection.php
27
 */
28
class MongoCollection
29
{
30
    use Helper\ReadPreference;
31
    use Helper\SlaveOkay;
32
    use Helper\WriteConcern;
33
34
    const ASCENDING = 1;
35
    const DESCENDING = -1;
36
37
    /**
38
     * @var MongoDB
39
     */
40
    public $db = null;
41
42
    /**
43
     * @var string
44
     */
45
    protected $name;
46
47
    /**
48
     * @var \MongoDB\Collection
49
     */
50
    protected $collection;
51
52
    /**
53
     * Creates a new collection
54
     *
55
     * @link http://www.php.net/manual/en/mongocollection.construct.php
56
     * @param MongoDB $db Parent database.
57
     * @param string $name Name for this collection.
58
     * @throws Exception
59
     */
60
    public function __construct(MongoDB $db, $name)
61
    {
62
        $this->checkCollectionName($name);
63
        $this->db = $db;
64
        $this->name = (string) $name;
65
66
        $this->setReadPreferenceFromArray($db->getReadPreference());
67
        $this->setWriteConcernFromArray($db->getWriteConcern());
68
69
        $this->createCollectionObject();
70
    }
71
72
    /**
73
     * Gets the underlying collection for this object
74
     *
75
     * @internal This part is not of the ext-mongo API and should not be used
76
     * @return \MongoDB\Collection
77
     */
78
    public function getCollection()
79
    {
80
        return $this->collection;
81
    }
82
83
    /**
84
     * String representation of this collection
85
     *
86
     * @link http://www.php.net/manual/en/mongocollection.--tostring.php
87
     * @return string Returns the full name of this collection.
88
     */
89
    public function __toString()
90
    {
91
        return (string) $this->db . '.' . $this->name;
92
    }
93
94
    /**
95
     * Gets a collection
96
     *
97
     * @link http://www.php.net/manual/en/mongocollection.get.php
98
     * @param string $name The next string in the collection name.
99
     * @return MongoCollection
100
     */
101
    public function __get($name)
102
    {
103
        // Handle w and wtimeout properties that replicate data stored in $readPreference
104
        if ($name === 'w' || $name === 'wtimeout') {
105
            return $this->getWriteConcern()[$name];
106
        }
107
108
        return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
109
    }
110
111
    /**
112
     * @param string $name
113
     * @param mixed $value
114
     */
115
    public function __set($name, $value)
116
    {
117
        if ($name === 'w' || $name === 'wtimeout') {
118
            $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
119
            $this->createCollectionObject();
120
        }
121
    }
122
123
    /**
124
     * Perform an aggregation using the aggregation framework
125
     *
126
     * @link http://www.php.net/manual/en/mongocollection.aggregate.php
127
     * @param array $pipeline
128
     * @param array $op
129
     * @return array
130
     */
131
    public function aggregate(array $pipeline, array $op = [])
132
    {
133
        if (! TypeConverter::isNumericArray($pipeline)) {
134
            $operators = func_get_args();
135
            $pipeline = [];
136
            $options = [];
137
138
            $i = 0;
139
            foreach ($operators as $operator) {
140
                $i++;
141
                if (! is_array($operator)) {
142
                    trigger_error("Argument $i is not an array", E_USER_WARNING);
143
                    return;
144
                }
145
146
                $pipeline[] = $operator;
147
            }
148
        } else {
149
            $options = $op;
150
        }
151
152
        if (isset($options['cursor'])) {
153
            $options['useCursor'] = true;
154
155
            if (isset($options['cursor']['batchSize'])) {
156
                $options['batchSize'] = $options['cursor']['batchSize'];
157
            }
158
159
            unset($options['cursor']);
160
        } else {
161
            $options['useCursor'] = false;
162
        }
163
164
        try {
165
            $cursor = $this->collection->aggregate(TypeConverter::fromLegacy($pipeline), $options);
166
167
            return [
168
                'ok' => 1.0,
169
                'result' => TypeConverter::toLegacy($cursor),
170
                'waitedMS' => 0,
171
            ];
172
        } catch (\MongoDB\Driver\Exception\Exception $e) {
173
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
174
        }
175
    }
176
177
    /**
178
     * Execute an aggregation pipeline command and retrieve results through a cursor
179
     *
180
     * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
181
     * @param array $pipeline
182
     * @param array $options
183
     * @return MongoCommandCursor
184
     */
185
    public function aggregateCursor(array $pipeline, array $options = [])
186
    {
187
        // Build command manually, can't use mongo-php-library here
188
        $command = [
189
            'aggregate' => $this->name,
190
            'pipeline' => $pipeline
191
        ];
192
193
        // Convert cursor option
194
        if (! isset($options['cursor'])) {
195
            $options['cursor'] = new \stdClass();
196
        }
197
198
        $command += $options;
199
200
        $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
201
        $cursor->setReadPreference($this->getReadPreference());
0 ignored issues
show
Documentation introduced by
$this->getReadPreference() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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