Completed
Pull Request — master (#216)
by
unknown
08:27
created

MongoCollection::update()   C

Complexity

Conditions 9
Paths 1

Size

Total Lines 48
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 33
nc 1
nop 3
dl 0
loc 48
rs 5.5102
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
    use MongoAnalytics;
35
36
    const ASCENDING = 1;
37
    const DESCENDING = -1;
38
39
    /**
40
     * @var MongoDB
41
     */
42
    public $db = null;
43
44
    /**
45
     * @var string
46
     */
47
    protected $name;
48
49
    /**
50
     * @var \MongoDB\Collection
51
     */
52
    protected $collection;
53
54
55
    /**
56
     * @var Array|null
57
     */
58
    protected static $debugConfig;
59
60
    /**
61
     * Creates a new collection
62
     *
63
     * @link http://www.php.net/manual/en/mongocollection.construct.php
64
     * @param MongoDB $db Parent database.
65
     * @param string $name Name for this collection.
66
     * @throws Exception
67
     */
68
    public function __construct(MongoDB $db, $name)
69
    {
70
        $this->checkCollectionName($name);
71
        $this->db = $db;
72
        $this->name = (string) $name;
73
74
        $this->setReadPreferenceFromArray($db->getReadPreference());
75
        $this->setWriteConcernFromArray($db->getWriteConcern());
76
77
        $this->createCollectionObject();
78
    }
79
80
    /**
81
     * Gets the underlying collection for this object
82
     *
83
     * @internal This part is not of the ext-mongo API and should not be used
84
     * @return \MongoDB\Collection
85
     */
86
    public function getCollection()
87
    {
88
        return $this->collection;
89
    }
90
91
    /**
92
     * String representation of this collection
93
     *
94
     * @link http://www.php.net/manual/en/mongocollection.--tostring.php
95
     * @return string Returns the full name of this collection.
96
     */
97
    public function __toString()
98
    {
99
        return (string) $this->db . '.' . $this->name;
100
    }
101
102
    /**
103
     * Gets a collection
104
     *
105
     * @link http://www.php.net/manual/en/mongocollection.get.php
106
     * @param string $name The next string in the collection name.
107
     * @return MongoCollection
108
     */
109
    public function __get($name)
110
    {
111
        // Handle w and wtimeout properties that replicate data stored in $readPreference
112
        if ($name === 'w' || $name === 'wtimeout') {
113
            return $this->getWriteConcern()[$name];
114
        }
115
116
        return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
117
    }
118
119
    /**
120
     * @param string $name
121
     * @param mixed $value
122
     */
123
    public function __set($name, $value)
124
    {
125
        if ($name === 'w' || $name === 'wtimeout') {
126
            $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
127
            $this->createCollectionObject();
128
        }
129
    }
130
131
    /**
132
     * Perform an aggregation using the aggregation framework
133
     *
134
     * @link http://www.php.net/manual/en/mongocollection.aggregate.php
135
     * @param array $pipeline
136
     * @param array $op
137
     * @return array
138
     */
139
    public function aggregate(array $pipeline, array $op = [])
140
    {
141
        return $this->eavesdrop([
142
          'criteria' => 0,
143
          'options' => 1,
144
          'operation' => 'aggregate'
145
        ], function () use ($pipeline, $op) {
146
            if (! TypeConverter::isNumericArray($pipeline)) {
147
                $operators = func_get_args();
148
                $pipeline = [];
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $pipeline, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

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

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
540
          try {
541
              if (isset($options['remove'])) {
542
                  unset($options['remove']);
543
                  $document = $this->collection->findOneAndDelete($query, $options);
544
              } else {
545
                  $update = is_array($update) ? $update : [];
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $update, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
546
                  if (isset($options['update']) && is_array($options['update'])) {
547
                      $update = $options['update'];
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $update, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
548
                      unset($options['update']);
549
                  }
550
551
                  $update = TypeConverter::fromLegacy($update);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $update, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
552
553
                  if (isset($options['new'])) {
554
                      $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
555
                      unset($options['new']);
556
                  }
557
558
                  $options['projection'] = TypeConverter::convertProjection($fields);
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 532 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...
559
560
                  if (! \MongoDB\is_first_key_operator($update)) {
561
                      $document = $this->collection->findOneAndReplace($query, $update, $options);
562
                  } else {
563
                      $document = $this->collection->findOneAndUpdate($query, $update, $options);
564
                  }
565
              }
566
          } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
567
              throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
568
          } catch (\MongoDB\Driver\Exception\Exception $e) {
569
              throw ExceptionConverter::toLegacy($e, 'MongoResultException');
570
          }
571
572
          if ($document) {
573
              $document = TypeConverter::toLegacy($document);
574
          }
575
576
          return $document;
577
      }, func_get_args());
578
    }
579
580
    /**
581
     * Querys this collection, returning a single element
582
     *
583
     * @link http://www.php.net/manual/en/mongocollection.findone.php
584
     * @param array $query The fields for which to search.
585
     * @param array $fields Fields of the results to return.
586
     * @param array $options
587
     * @return array|null
588
     */
589
    public function findOne($query = [], array $fields = [], array $options = [])
590
    {
591
        return $this->eavesdrop([
592
          'criteria' => 0,
593
          'options' => 2,
594
          'operation' => 'findOne'
595
      ], function () use ($query, $fields, $options) {
596
          // Can't typehint for array since MongoGridFS extends and accepts strings
597
        if (! is_array($query)) {
598
            trigger_error(sprintf('MongoCollection::findOne(): expects parameter 1 to be an array or object, %s given', gettype($query)), E_USER_WARNING);
599
            return;
600
        }
601
602
          $options = ['projection' => TypeConverter::convertProjection($fields)] + $options;
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $options, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
603
          try {
604
              $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
605
          } catch (\MongoDB\Driver\Exception\Exception $e) {
606
              throw ExceptionConverter::toLegacy($e);
607
          }
608
609
          if ($document !== null) {
610
              $document = TypeConverter::toLegacy($document);
611
          }
612
613
          return $document;
614
      }, func_get_args());
615
    }
616
617
    /**
618
     * Creates an index on the given field(s), or does nothing if the index already exists
619
     *
620
     * @link http://www.php.net/manual/en/mongocollection.createindex.php
621
     * @param array $keys Field or fields to use as index.
622
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
623
     * @return array Returns the database response.
624
     */
625
    public function createIndex($keys, array $options = [])
626
    {
627
        if (is_string($keys)) {
628
            if (empty($keys)) {
629
                throw new MongoException('empty string passed as key field');
630
            }
631
            $keys = [$keys => 1];
632
        }
633
634
        if (is_object($keys)) {
635
            $keys = (array) $keys;
636
        }
637
638
        if (! is_array($keys) || ! count($keys)) {
639
            throw new MongoException('index specification has no elements');
640
        }
641
642
        if (! isset($options['name'])) {
643
            $options['name'] = \MongoDB\generate_index_name($keys);
644
        }
645
646
        $indexes = iterator_to_array($this->collection->listIndexes());
647
        $indexCount = count($indexes);
648
        $collectionExists = true;
649
        $indexExists = false;
650
651
        // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
652
        if ($indexCount === 0) {
653
            $collectionExists = false;
654
            $indexCount = 1;
655
        }
656
657
        foreach ($indexes as $index) {
658
            if ($index->getKey() === $keys || $index->getName() === $options['name']) {
659
                $indexExists = true;
660
                break;
661
            }
662
        }
663
664
        try {
665
            foreach (['w', 'wTimeoutMS', 'safe', 'timeout', 'wtimeout'] as $invalidOption) {
666
                if (isset($options[$invalidOption])) {
667
                    unset($options[$invalidOption]);
668
                }
669
            }
670
671
            $this->collection->createIndex($keys, $options);
672
        } catch (\MongoDB\Driver\Exception\Exception $e) {
673
            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
674
        }
675
676
        $result = [
677
            'createdCollectionAutomatically' => !$collectionExists,
678
            'numIndexesBefore' => $indexCount,
679
            'numIndexesAfter' => $indexCount,
680
            'note' => 'all indexes already exist',
681
            'ok' => 1.0,
682
        ];
683
684
        if (! $indexExists) {
685
            $result['numIndexesAfter']++;
686
            unset($result['note']);
687
        }
688
689
        return $result;
690
    }
691
692
    /**
693
     * Creates an index on the given field(s), or does nothing if the index already exists
694
     *
695
     * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
696
     * @param array $keys Field or fields to use as index.
697
     * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
698
     * @return array Returns the database response.
699
     * @deprecated Use MongoCollection::createIndex() instead.
700
     */
701
    public function ensureIndex(array $keys, array $options = [])
702
    {
703
        return $this->createIndex($keys, $options);
704
    }
705
706
    /**
707
     * Deletes an index from this collection
708
     *
709
     * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
710
     * @param string|array $keys Field or fields from which to delete the index.
711
     * @return array Returns the database response.
712
     */
713
    public function deleteIndex($keys)
714
    {
715
        if (is_string($keys)) {
716
            $indexName = $keys;
717
            if (! preg_match('#_-?1$#', $indexName)) {
718
                $indexName .= '_1';
719
            }
720
        } elseif (is_array($keys)) {
721
            $indexName = \MongoDB\generate_index_name($keys);
722
        } else {
723
            throw new \InvalidArgumentException();
724
        }
725
726
        try {
727
            return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
728
        } catch (\MongoDB\Driver\Exception\Exception $e) {
729
            return ExceptionConverter::toResultArray($e) + ['nIndexesWas' => count($this->getIndexInfo())];
730
        }
731
    }
732
733
    /**
734
     * Delete all indexes for this collection
735
     *
736
     * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
737
     * @return array Returns the database response.
738
     */
739
    public function deleteIndexes()
740
    {
741
        try {
742
            return TypeConverter::toLegacy($this->collection->dropIndexes());
743
        } catch (\MongoDB\Driver\Exception\Exception $e) {
744
            return ExceptionConverter::toResultArray($e);
745
        }
746
    }
747
748
    /**
749
     * Returns an array of index names for this collection
750
     *
751
     * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
752
     * @return array Returns a list of index names.
753
     */
754
    public function getIndexInfo()
755
    {
756
        $convertIndex = function (\MongoDB\Model\IndexInfo $indexInfo) {
757
            $infos = [
758
                'v' => $indexInfo->getVersion(),
759
                'key' => $indexInfo->getKey(),
760
                'name' => $indexInfo->getName(),
761
                'ns' => $indexInfo->getNamespace(),
762
            ];
763
764
            $additionalKeys = [
765
                'unique',
766
                'sparse',
767
                'partialFilterExpression',
768
                'expireAfterSeconds',
769
                'storageEngine',
770
                'weights',
771
                'default_language',
772
                'language_override',
773
                'textIndexVersion',
774
                'collation',
775
                '2dsphereIndexVersion',
776
                'bucketSize'
777
            ];
778
779
            foreach ($additionalKeys as $key) {
780
                if (! isset($indexInfo[$key])) {
781
                    continue;
782
                }
783
784
                $infos[$key] = $indexInfo[$key];
785
            }
786
787
            return $infos;
788
        };
789
790
        return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
791
    }
792
793
    /**
794
     * Counts the number of documents in this collection
795
     *
796
     * @link http://www.php.net/manual/en/mongocollection.count.php
797
     * @param array|stdClass $query
798
     * @param array $options
799
     * @return int Returns the number of documents matching the query.
800
     */
801
    public function count($query = [], $options = [])
802
    {
803
        return $this->eavesdrop([
804
          'criteria' => 0,
805
          'options' => 1,
806
          'operation' => 'count'
807
        ], function () use ($query, $options) {
808
            try {
809
                // Handle legacy mode - limit and skip as second and third parameters, respectively
810
            if (! is_array($options)) {
811
                $limit = $options;
812
                $options = [];
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $options, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
813
814
                if ($limit !== null) {
815
                    $options['limit'] = (int) $limit;
816
                }
817
818
                if (func_num_args() > 2) {
819
                    $options['skip'] = (int) func_get_args()[2];
820
                }
821
            }
822
823
                return $this->collection->count(TypeConverter::fromLegacy($query), $options);
824
            } catch (\MongoDB\Driver\Exception\Exception $e) {
825
                throw ExceptionConverter::toLegacy($e);
826
            }
827
        }, func_get_args());
828
    }
829
830
    /**
831
     * Saves an object to this collection
832
     *
833
     * @link http://www.php.net/manual/en/mongocollection.save.php
834
     * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
835
     * @param array $options Options for the save.
836
     * @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.
837
     * @throws MongoCursorException if the "w" option is set and the write fails.
838
     * @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.
839
     * @return array|boolean If w was set, returns an array containing the status of the save.
840
     * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
841
     */
842
    public function save(&$a, array $options = [])
843
    {
844
        $id = $this->ensureDocumentHasMongoId($a);
845
846
        $document = (array) $a;
847
848
        $options['upsert'] = true;
849
850
        try {
851
            /** @var \MongoDB\UpdateResult $result */
852
            $result = $this->collection->replaceOne(
853
                TypeConverter::fromLegacy(['_id' => $id]),
854
                TypeConverter::fromLegacy($document),
855
                $this->convertWriteConcernOptions($options)
856
            );
857
858
            if (! $result->isAcknowledged()) {
859
                return true;
860
            }
861
862
            $resultArray = [
863
                'ok' => 1.0,
864
                'nModified' => $result->getModifiedCount(),
865
                'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
866
                'err' => null,
867
                'errmsg' => null,
868
                'updatedExisting' => $result->getUpsertedCount() == 0 && $result->getModifiedCount() > 0,
869
            ];
870
            if ($result->getUpsertedId() !== null) {
871
                $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
872
            }
873
874
            return $resultArray;
875
        } catch (\MongoDB\Driver\Exception\Exception $e) {
876
            throw ExceptionConverter::toLegacy($e);
877
        }
878
    }
879
880
    /**
881
     * Creates a database reference
882
     *
883
     * @link http://www.php.net/manual/en/mongocollection.createdbref.php
884
     * @param array|object $document_or_id Object to which to create a reference.
885
     * @return array Returns a database reference array.
886
     */
887
    public function createDBRef($document_or_id)
888
    {
889 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...
890
            $id = $document_or_id;
891
        } elseif (is_object($document_or_id)) {
892
            if (! isset($document_or_id->_id)) {
893
                return null;
894
            }
895
896
            $id = $document_or_id->_id;
897
        } elseif (is_array($document_or_id)) {
898
            if (! isset($document_or_id['_id'])) {
899
                return null;
900
            }
901
902
            $id = $document_or_id['_id'];
903
        } else {
904
            $id = $document_or_id;
905
        }
906
907
        return MongoDBRef::create($this->name, $id);
908
    }
909
910
    /**
911
     * Fetches the document pointed to by a database reference
912
     *
913
     * @link http://www.php.net/manual/en/mongocollection.getdbref.php
914
     * @param array $ref A database reference.
915
     * @return array Returns the database document pointed to by the reference.
916
     */
917
    public function getDBRef(array $ref)
918
    {
919
        return $this->db->getDBRef($ref);
920
    }
921
922
    /**
923
     * Performs an operation similar to SQL's GROUP BY command
924
     *
925
     * @link http://www.php.net/manual/en/mongocollection.group.php
926
     * @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.
927
     * @param array $initial Initial value of the aggregation counter object.
928
     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
929
     * @param array $condition An condition that must be true for a row to be considered.
930
     * @return array
931
     */
932
    public function group($keys, array $initial, $reduce, array $condition = [])
933
    {
934
        if (is_string($reduce)) {
935
            $reduce = new MongoCode($reduce);
936
        }
937
938
        $command = [
939
            'group' => [
940
                'ns' => $this->name,
941
                '$reduce' => (string) $reduce,
942
                'initial' => $initial,
943
                'cond' => $condition,
944
            ],
945
        ];
946
947
        if ($keys instanceof MongoCode) {
948
            $command['group']['$keyf'] = (string) $keys;
949
        } else {
950
            $command['group']['key'] = $keys;
951
        }
952
        if (array_key_exists('condition', $condition)) {
953
            $command['group']['cond'] = $condition['condition'];
954
        }
955
        if (array_key_exists('finalize', $condition)) {
956
            if ($condition['finalize'] instanceof MongoCode) {
957
                $condition['finalize'] = (string) $condition['finalize'];
958
            }
959
            $command['group']['finalize'] = $condition['finalize'];
960
        }
961
962
        return $this->db->command($command);
963
    }
964
965
    /**
966
     * Returns an array of cursors to iterator over a full collection in parallel
967
     *
968
     * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
969
     * @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.
970
     * @return MongoCommandCursor[]
971
     */
972
    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...
973
    {
974
        $this->notImplemented();
975
    }
976
977
    protected function notImplemented()
978
    {
979
        throw new \Exception('Not implemented');
980
    }
981
982
    /**
983
     * @return \MongoDB\Collection
984
     */
985 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...
986
    {
987
        $options = [
988
            'readPreference' => $this->readPreference,
989
            'writeConcern' => $this->writeConcern,
990
        ];
991
992
        if ($this->collection === null) {
993
            $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
994
        } else {
995
            $this->collection = $this->collection->withOptions($options);
996
        }
997
    }
998
999
    /**
1000
     * Converts legacy write concern options to a WriteConcern object
1001
     *
1002
     * @param array $options
1003
     * @return array
1004
     */
1005
    private function convertWriteConcernOptions(array $options)
1006
    {
1007
        if (isset($options['safe'])) {
1008
            $options['w'] = ($options['safe']) ? 1 : 0;
1009
        }
1010
1011 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...
1012
            $options['wTimeoutMS'] = $options['wtimeout'];
1013
        }
1014
1015
        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
1016
            $collectionWriteConcern = $this->getWriteConcern();
1017
            $writeConcern = $this->createWriteConcernFromParameters(
1018
                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
1019
                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
1020
            );
1021
1022
            $options['writeConcern'] = $writeConcern;
1023
        }
1024
1025
        unset($options['safe']);
1026
        unset($options['w']);
1027
        unset($options['wTimeout']);
1028
        unset($options['wTimeoutMS']);
1029
1030
        return $options;
1031
    }
1032
1033
    private function checkKeys(array $array)
1034
    {
1035
        foreach ($array as $key => $value) {
1036
            if (empty($key) && $key !== 0) {
1037
                throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
1038
            }
1039
1040
            if (is_object($value) || is_array($value)) {
1041
                $this->checkKeys((array) $value);
1042
            }
1043
        }
1044
    }
1045
1046
    /**
1047
     * @param array|object $document
1048
     * @return MongoId
1049
     */
1050
    private function ensureDocumentHasMongoId(&$document)
1051
    {
1052
        if (is_array($document)) {
1053
            if (! isset($document['_id'])) {
1054
                $document['_id'] = new \MongoId();
1055
            }
1056
1057
            $this->checkKeys($document);
1058
1059
            return $document['_id'];
1060
        } elseif (is_object($document)) {
1061
            $reflectionObject = new \ReflectionObject($document);
1062
            foreach ($reflectionObject->getProperties() as $property) {
1063
                if (! $property->isPublic()) {
1064
                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
1065
                }
1066
            }
1067
1068
            if (! isset($document->_id)) {
1069
                $document->_id = new \MongoId();
1070
            }
1071
1072
            $this->checkKeys((array) $document);
1073
1074
            return $document->_id;
1075
        }
1076
1077
        return null;
1078
    }
1079
1080
    private function checkCollectionName($name)
1081
    {
1082
        if (empty($name)) {
1083
            throw new Exception('Collection name cannot be empty');
1084
        } elseif (strpos($name, chr(0)) !== false) {
1085
            throw new Exception('Collection name cannot contain null bytes');
1086
        }
1087
    }
1088
1089
    /**
1090
     * @return array
1091
     */
1092
    public function __sleep()
1093
    {
1094
        return ['db', 'name'];
1095
    }
1096
1097
    private function mustBeArrayOrObject($a)
1098
    {
1099
        if (!is_array($a) && !is_object($a)) {
1100
            throw new \MongoException('document must be an array or object');
1101
        }
1102
    }
1103
}
1104