Completed
Pull Request — master (#1714)
by
unknown
10:42
created

DocumentPersister::executeUpsert()   C

Complexity

Conditions 10
Paths 96

Size

Total Lines 60
Code Lines 29

Duplication

Lines 13
Ratio 21.67 %

Code Coverage

Tests 24
CRAP Score 10.5124

Importance

Changes 0
Metric Value
dl 13
loc 60
ccs 24
cts 29
cp 0.8276
rs 6.5333
c 0
b 0
f 0
cc 10
eloc 29
nc 96
nop 2
crap 10.5124

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Persisters;
4
5
use Doctrine\Common\EventManager;
6
use Doctrine\Common\Persistence\Mapping\MappingException;
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
12
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
13
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
14
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
15
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
16
use Doctrine\ODM\MongoDB\LockException;
17
use Doctrine\ODM\MongoDB\LockMode;
18
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
19
use Doctrine\ODM\MongoDB\MongoDBException;
20
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
21
use Doctrine\ODM\MongoDB\Proxy\Proxy;
22
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
23
use Doctrine\ODM\MongoDB\Query\Query;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
use Doctrine\ODM\MongoDB\UnitOfWork;
26
use MongoDB\Collection;
27
use MongoDB\Driver\Cursor;
28
use MongoDB\Driver\Exception\Exception as DriverException;
29
use MongoDB\Driver\Exception\WriteException;
30
31
/**
32
 * The DocumentPersister is responsible for persisting documents.
33
 *
34
 * @since       1.0
35
 */
36
class DocumentPersister
37
{
38
    /**
39
     * The PersistenceBuilder instance.
40
     *
41
     * @var PersistenceBuilder
42
     */
43
    private $pb;
44
45
    /**
46
     * The DocumentManager instance.
47
     *
48
     * @var DocumentManager
49
     */
50
    private $dm;
51
52
    /**
53
     * The EventManager instance
54
     *
55
     * @var EventManager
56
     */
57
    private $evm;
58
59
    /**
60
     * The UnitOfWork instance.
61
     *
62
     * @var UnitOfWork
63
     */
64
    private $uow;
65
66
    /**
67
     * The ClassMetadata instance for the document type being persisted.
68
     *
69
     * @var ClassMetadata
70
     */
71
    private $class;
72
73
    /**
74
     * The MongoCollection instance for this document.
75
     *
76
     * @var Collection
77
     */
78
    private $collection;
79
80
    /**
81
     * Array of queued inserts for the persister to insert.
82
     *
83
     * @var array
84
     */
85
    private $queuedInserts = array();
86
87
    /**
88
     * Array of queued inserts for the persister to insert.
89
     *
90
     * @var array
91
     */
92
    private $queuedUpserts = array();
93
94
    /**
95
     * The CriteriaMerger instance.
96
     *
97
     * @var CriteriaMerger
98
     */
99
    private $cm;
100
101
    /**
102
     * The CollectionPersister instance.
103
     *
104
     * @var CollectionPersister
105
     */
106
    private $cp;
107
108
    /**
109
     * The HydratorFactory instance.
110
     *
111
     * @var HydratorFactory
112
     */
113
    private $hydratorFactory;
114
115
    /**
116
     * Initializes this instance.
117
     *
118
     * @param PersistenceBuilder $pb
119
     * @param DocumentManager $dm
120
     * @param EventManager $evm
121
     * @param UnitOfWork $uow
122
     * @param HydratorFactory $hydratorFactory
123
     * @param ClassMetadata $class
124
     * @param CriteriaMerger $cm
125
     */
126 1074
    public function __construct(
127
        PersistenceBuilder $pb,
128
        DocumentManager $dm,
129
        EventManager $evm,
130
        UnitOfWork $uow,
131
        HydratorFactory $hydratorFactory,
132
        ClassMetadata $class,
133
        CriteriaMerger $cm = null
134
    ) {
135 1074
        $this->pb = $pb;
136 1074
        $this->dm = $dm;
137 1074
        $this->evm = $evm;
138 1074
        $this->cm = $cm ?: new CriteriaMerger();
139 1074
        $this->uow = $uow;
140 1074
        $this->hydratorFactory = $hydratorFactory;
141 1074
        $this->class = $class;
142 1074
        $this->collection = $dm->getDocumentCollection($class->name);
143 1074
        $this->cp = $this->uow->getCollectionPersister();
144 1074
    }
145
146
    /**
147
     * @return array
148
     */
149
    public function getInserts()
150
    {
151
        return $this->queuedInserts;
152
    }
153
154
    /**
155
     * @param object $document
156
     * @return bool
157
     */
158
    public function isQueuedForInsert($document)
159
    {
160
        return isset($this->queuedInserts[spl_object_hash($document)]);
161
    }
162
163
    /**
164
     * Adds a document to the queued insertions.
165
     * The document remains queued until {@link executeInserts} is invoked.
166
     *
167
     * @param object $document The document to queue for insertion.
168
     */
169 476
    public function addInsert($document)
170
    {
171 476
        $this->queuedInserts[spl_object_hash($document)] = $document;
172 476
    }
173
174
    /**
175
     * @return array
176
     */
177
    public function getUpserts()
178
    {
179
        return $this->queuedUpserts;
180
    }
181
182
    /**
183
     * @param object $document
184
     * @return boolean
185
     */
186
    public function isQueuedForUpsert($document)
187
    {
188
        return isset($this->queuedUpserts[spl_object_hash($document)]);
189
    }
190
191
    /**
192
     * Adds a document to the queued upserts.
193
     * The document remains queued until {@link executeUpserts} is invoked.
194
     *
195
     * @param object $document The document to queue for insertion.
196
     */
197 84
    public function addUpsert($document)
198
    {
199 84
        $this->queuedUpserts[spl_object_hash($document)] = $document;
200 84
    }
201
202
    /**
203
     * Gets the ClassMetadata instance of the document class this persister is used for.
204
     *
205
     * @return ClassMetadata
206
     */
207
    public function getClassMetadata()
208
    {
209
        return $this->class;
210
    }
211
212
    /**
213
     * Executes all queued document insertions.
214
     *
215
     * Queued documents without an ID will inserted in a batch and queued
216
     * documents with an ID will be upserted individually.
217
     *
218
     * If no inserts are queued, invoking this method is a NOOP.
219
     *
220
     * @param array $options Options for batchInsert() and update() driver methods
221
     */
222 476
    public function executeInserts(array $options = array())
223
    {
224 476
        if ( ! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
225
            return;
226
        }
227
228 476
        $inserts = array();
229 476
        $options = $this->getWriteOptions($options);
230 476
        foreach ($this->queuedInserts as $oid => $document) {
231 476
            $data = $this->pb->prepareInsertData($document);
232
233
            // Set the initial version for each insert
234 475 View Code Duplication
            if ($this->class->isVersioned) {
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...
235 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
236 20
                $nextVersion = null;
237 20
                if ($versionMapping['type'] === 'int') {
238 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
239 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
240 2
                } elseif ($versionMapping['type'] === 'date') {
241 2
                    $nextVersionDateTime = new \DateTime();
242 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
243 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
244
                }
245 20
                $data[$versionMapping['name']] = $nextVersion;
246
            }
247
248 475
            $inserts[] = $data;
249
        }
250
251 475
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
252
            try {
253 475
                $this->collection->insertMany($inserts, $options);
254 6
            } catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
255 6
                $this->queuedInserts = array();
256 6
                throw $e;
257
            }
258
        }
259
260
        /* All collections except for ones using addToSet have already been
261
         * saved. We have left these to be handled separately to avoid checking
262
         * collection for uniqueness on PHP side.
263
         */
264 475
        foreach ($this->queuedInserts as $document) {
265 475
            $this->handleCollections($document, $options);
266
        }
267
268 475
        $this->queuedInserts = array();
269 475
    }
270
271
    /**
272
     * Executes all queued document upserts.
273
     *
274
     * Queued documents with an ID are upserted individually.
275
     *
276
     * If no upserts are queued, invoking this method is a NOOP.
277
     *
278
     * @param array $options Options for batchInsert() and update() driver methods
279
     */
280 84
    public function executeUpserts(array $options = array())
281
    {
282 84
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
283
            return;
284
        }
285
286 84
        $options = $this->getWriteOptions($options);
287 84
        foreach ($this->queuedUpserts as $oid => $document) {
288
            try {
289 84
                $this->executeUpsert($document, $options);
290 84
                $this->handleCollections($document, $options);
291 84
                unset($this->queuedUpserts[$oid]);
292
            } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
293
                unset($this->queuedUpserts[$oid]);
294 84
                throw $e;
295
            }
296
        }
297 84
    }
298
299
    /**
300
     * Executes a single upsert in {@link executeUpserts}
301
     *
302
     * @param object $document
303
     * @param array  $options
304
     */
305 84
    private function executeUpsert($document, array $options)
306
    {
307 84
        $options['upsert'] = true;
308 84
        $criteria = $this->getQueryForDocument($document);
309
310 84
        $data = $this->pb->prepareUpsertData($document);
311
312
        // Set the initial version for each upsert
313 84 View Code Duplication
        if ($this->class->isVersioned) {
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...
314 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
315 2
            $nextVersion = null;
316 2
            if ($versionMapping['type'] === 'int') {
317 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
318 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
319 1
            } elseif ($versionMapping['type'] === 'date') {
320 1
                $nextVersionDateTime = new \DateTime();
321 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
322 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
323
            }
324 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
325
        }
326
327 84
        foreach (array_keys($criteria) as $field) {
328 84
            unset($data['$set'][$field]);
329
        }
330
331
        // Do not send an empty $set modifier
332 84
        if (empty($data['$set'])) {
333 17
            unset($data['$set']);
334
        }
335
336
        /* If there are no modifiers remaining, we're upserting a document with
337
         * an identifier as its only field. Since a document with the identifier
338
         * may already exist, the desired behavior is "insert if not exists" and
339
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
340
         * the identifier to the same value in our criteria.
341
         *
342
         * This will fail for versions before MongoDB 2.6, which require an
343
         * empty $set modifier. The best we can do (without attempting to check
344
         * server versions in advance) is attempt the 2.6+ behavior and retry
345
         * after the relevant exception.
346
         *
347
         * See: https://jira.mongodb.org/browse/SERVER-12266
348
         */
349 84
        if (empty($data)) {
350 17
            $retry = true;
351 17
            $data = array('$set' => array('_id' => $criteria['_id']));
352
        }
353
354
        try {
355 84
            $this->collection->updateOne($criteria, $data, $options);
356 84
            return;
357
        } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
358
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
359
                throw $e;
360
            }
361
        }
362
363
        $this->collection->updateOne($criteria, array('$set' => new \stdClass), $options);
364
    }
365
366
    /**
367
     * Updates the already persisted document if it has any new changesets.
368
     *
369
     * @param object $document
370
     * @param array $options Array of options to be used with update()
371
     * @throws \Doctrine\ODM\MongoDB\LockException
372
     */
373 195
    public function update($document, array $options = array())
374
    {
375 195
        $update = $this->pb->prepareUpdateData($document);
376
377 195
        $query = $this->getQueryForDocument($document);
378
379 195
        foreach (array_keys($query) as $field) {
380 195
            unset($update['$set'][$field]);
381
        }
382
383 195
        if (empty($update['$set'])) {
384 89
            unset($update['$set']);
385
        }
386
387
388
        // Include versioning logic to set the new version value in the database
389
        // and to ensure the version has not changed since this document object instance
390
        // was fetched from the database
391 195
        $nextVersion = null;
392 195
        if ($this->class->isVersioned) {
393 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
394 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
395 13
            if ($versionMapping['type'] === 'int') {
396 10
                $nextVersion = $currentVersion + 1;
397 10
                $update['$inc'][$versionMapping['name']] = 1;
398 10
                $query[$versionMapping['name']] = $currentVersion;
399 3
            } elseif ($versionMapping['type'] === 'date') {
400 3
                $nextVersion = new \DateTime();
401 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
402 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
403
            }
404
        }
405
406 195
        if ( ! empty($update)) {
407
            // Include locking logic so that if the document object in memory is currently
408
            // locked then it will remove it, otherwise it ensures the document is not locked.
409 129
            if ($this->class->isLockable) {
410 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
411 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
412 11
                if ($isLocked) {
413 2
                    $update['$unset'] = array($lockMapping['name'] => true);
414
                } else {
415 9
                    $query[$lockMapping['name']] = array('$exists' => false);
416
                }
417
            }
418
419 129
            $options = $this->getWriteOptions($options);
420
421 129
            $result = $this->collection->updateOne($query, $update, $options);
422
423 129
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
424 4
                throw LockException::lockFailed($document);
425 125
            } elseif ($this->class->isVersioned) {
426 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
427
            }
428
        }
429
430 191
        $this->handleCollections($document, $options);
431 191
    }
432
433
    /**
434
     * Removes document from mongo
435
     *
436
     * @param mixed $document
437
     * @param array $options Array of options to be used with remove()
438
     * @throws \Doctrine\ODM\MongoDB\LockException
439
     */
440 32
    public function delete($document, array $options = array())
441
    {
442 32
        $query = $this->getQueryForDocument($document);
443
444 32
        if ($this->class->isLockable) {
445 2
            $query[$this->class->lockField] = array('$exists' => false);
446
        }
447
448 32
        $options = $this->getWriteOptions($options);
449
450 32
        $result = $this->collection->deleteOne($query, $options);
451
452 32
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
453 2
            throw LockException::lockFailed($document);
454
        }
455 30
    }
456
457
    /**
458
     * Refreshes a managed document.
459
     *
460
     * @param object $document The document to refresh.
461
     */
462 20
    public function refresh($document)
463
    {
464 20
        $query = $this->getQueryForDocument($document);
465 20
        $data = $this->collection->findOne($query);
466 20
        $data = $this->hydratorFactory->hydrate($document, $data);
467 20
        $this->uow->setOriginalDocumentData($document, $data);
468 20
    }
469
470
    /**
471
     * Finds a document by a set of criteria.
472
     *
473
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
474
     * be used to match an _id value.
475
     *
476
     * @param mixed   $criteria Query criteria
477
     * @param object  $document Document to load the data into. If not specified, a new document is created.
478
     * @param array   $hints    Hints for document creation
479
     * @param integer $lockMode
480
     * @param array   $sort     Sort array for Cursor::sort()
481
     * @throws \Doctrine\ODM\MongoDB\LockException
482
     * @return object|null The loaded and managed document instance or null if no document was found
483
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
484
     */
485 342
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode 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...
486
    {
487
        // TODO: remove this
488 342
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoDB\BSON\ObjectId) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
489
            $criteria = array('_id' => $criteria);
490
        }
491
492 342
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
493 342
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
494 342
        $criteria = $this->addFilterToPreparedQuery($criteria);
495
496 342
        $options = [];
497 342
        if (null !== $sort) {
498 92
            $options['sort'] = $this->prepareSort($sort);
499
        }
500 342
        $result = $this->collection->findOne($criteria, $options);
501
502 342
        if ($this->class->isLockable) {
503 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
504 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
505 1
                throw LockException::lockFailed($result);
506
            }
507
        }
508
509 341
        return $this->createDocument($result, $document, $hints);
510
    }
511
512
    /**
513
     * Finds documents by a set of criteria.
514
     *
515
     * @param array        $criteria Query criteria
516
     * @param array        $sort     Sort array for Cursor::sort()
517
     * @param integer|null $limit    Limit for Cursor::limit()
518
     * @param integer|null $skip     Skip for Cursor::skip()
519
     * @return Iterator
520
     */
521 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
522
    {
523 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
524 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
525 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
526
527 22
        $options = [];
528 22
        if (null !== $sort) {
529 11
            $options['sort'] = $this->prepareSort($sort);
530
        }
531
532 22
        if (null !== $limit) {
533 10
            $options['limit'] = $limit;
534
        }
535
536 22
        if (null !== $skip) {
537 1
            $options['skip'] = $skip;
538
        }
539
540 22
        $baseCursor = $this->collection->find($criteria, $options);
541 22
        $cursor = $this->wrapCursor($baseCursor);
542
543 22
        return $cursor;
544
    }
545
546
    /**
547
     * @param object $document
548
     *
549
     * @return array
550
     * @throws MongoDBException
551
     */
552 268
    private function getShardKeyQuery($document)
553
    {
554 268
        if ( ! $this->class->isSharded()) {
555 264
            return array();
556
        }
557
558 4
        $shardKey = $this->class->getShardKey();
559 4
        $keys = array_keys($shardKey['keys']);
560 4
        $data = $this->uow->getDocumentActualData($document);
561
562 4
        $shardKeyQueryPart = array();
563 4
        foreach ($keys as $key) {
564 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
565 4
            $this->guardMissingShardKey($document, $key, $data);
566
567 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
568 1
                $reference = $this->prepareReference(
569 1
                    $key,
570 1
                    $data[$mapping['fieldName']],
571 1
                    $mapping,
572 1
                    false
573
                );
574 1
                foreach ($reference as $keyValue) {
575 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
576
                }
577
            } else {
578 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
579 4
                $shardKeyQueryPart[$key] = $value;
580
            }
581
        }
582
583 4
        return $shardKeyQueryPart;
584
    }
585
586
    /**
587
     * Wraps the supplied base cursor in the corresponding ODM class.
588
     *
589
     * @param Cursor $baseCursor
590
     * @return Iterator
591
     */
592 22
    private function wrapCursor(Cursor $baseCursor): Iterator
593
    {
594 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
595
    }
596
597
    /**
598
     * Checks whether the given managed document exists in the database.
599
     *
600
     * @param object $document
601
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
602
     */
603 3
    public function exists($document)
604
    {
605 3
        $id = $this->class->getIdentifierObject($document);
606 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
607
    }
608
609
    /**
610
     * Locks document by storing the lock mode on the mapped lock field.
611
     *
612
     * @param object $document
613
     * @param int $lockMode
614
     */
615 5
    public function lock($document, $lockMode)
616
    {
617 5
        $id = $this->uow->getDocumentIdentifier($document);
618 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
619 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
620 5
        $this->collection->updateOne($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
621 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
622 5
    }
623
624
    /**
625
     * Releases any lock that exists on this document.
626
     *
627
     * @param object $document
628
     */
629 1
    public function unlock($document)
630
    {
631 1
        $id = $this->uow->getDocumentIdentifier($document);
632 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
633 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
634 1
        $this->collection->updateOne($criteria, array('$unset' => array($lockMapping['name'] => true)));
635 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
636 1
    }
637
638
    /**
639
     * Creates or fills a single document object from an query result.
640
     *
641
     * @param object $result The query result.
642
     * @param object $document The document object to fill, if any.
643
     * @param array $hints Hints for document creation.
644
     * @return object The filled and managed document object or NULL, if the query result is empty.
645
     */
646 341
    private function createDocument($result, $document = null, array $hints = array())
647
    {
648 341
        if ($result === null) {
649 111
            return null;
650
        }
651
652 300
        if ($document !== null) {
653 38
            $hints[Query::HINT_REFRESH] = true;
654 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
655 38
            $this->uow->registerManaged($document, $id, $result);
656
        }
657
658 300
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
659
    }
660
661
    /**
662
     * Loads a PersistentCollection data. Used in the initialize() method.
663
     *
664
     * @param PersistentCollectionInterface $collection
665
     */
666 163
    public function loadCollection(PersistentCollectionInterface $collection)
667
    {
668 163
        $mapping = $collection->getMapping();
669 163
        switch ($mapping['association']) {
670
            case ClassMetadata::EMBED_MANY:
671 109
                $this->loadEmbedManyCollection($collection);
672 109
                break;
673
674
            case ClassMetadata::REFERENCE_MANY:
675 76
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
676 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
677
                } else {
678 72
                    if ($mapping['isOwningSide']) {
679 60
                        $this->loadReferenceManyCollectionOwningSide($collection);
680
                    } else {
681 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
682
                    }
683
                }
684 76
                break;
685
        }
686 163
    }
687
688 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
689
    {
690 109
        $embeddedDocuments = $collection->getMongoData();
691 109
        $mapping = $collection->getMapping();
692 109
        $owner = $collection->getOwner();
693 109
        if ($embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
694 82
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
695 82
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
696 82
                $embeddedMetadata = $this->dm->getClassMetadata($className);
697 82
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
698
699 82
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
700
701 82
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
702 82
                $id = $embeddedMetadata->identifier && $data[$embeddedMetadata->identifier] ?? null;
703
704 82
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
705 81
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
706
                }
707 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
708 10
                    $collection->set($key, $embeddedDocumentObject);
709
                } else {
710 82
                    $collection->add($embeddedDocumentObject);
711
                }
712
            }
713
        }
714 109
    }
715
716 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
717
    {
718 60
        $hints = $collection->getHints();
719 60
        $mapping = $collection->getMapping();
720 60
        $groupedIds = array();
721
722 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
723
724 60
        foreach ($collection->getMongoData() as $key => $reference) {
725 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
726 54
            $identifier = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
727 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
728
729
            // create a reference to the class and id
730 54
            $reference = $this->dm->getReference($className, $id);
731
732
            // no custom sort so add the references right now in the order they are embedded
733 54
            if ( ! $sorted) {
734 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
735 2
                    $collection->set($key, $reference);
736
                } else {
737 51
                    $collection->add($reference);
738
                }
739
            }
740
741
            // only query for the referenced object if it is not already initialized or the collection is sorted
742 54
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
743 54
                $groupedIds[$className][] = $identifier;
744
            }
745
        }
746 60
        foreach ($groupedIds as $className => $ids) {
747 39
            $class = $this->dm->getClassMetadata($className);
748 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
749 39
            $criteria = $this->cm->merge(
750 39
                array('_id' => array('$in' => array_values($ids))),
751 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
752 39
                $mapping['criteria'] ?? array()
753
            );
754 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
755
756 39
            $options = [];
757 39
            if (isset($mapping['sort'])) {
758 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
759
            }
760 39
            if (isset($mapping['limit'])) {
761
                $options['limit'] = $mapping['limit'];
762
            }
763 39
            if (isset($mapping['skip'])) {
764
                $options['skip'] = $mapping['skip'];
765
            }
766 39
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
767
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
768
            }
769
770 39
            $cursor = $mongoCollection->find($criteria, $options);
771 39
            $documents = $cursor->toArray();
772 39
            foreach ($documents as $documentData) {
773 38
                $document = $this->uow->getById($documentData['_id'], $class);
774 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
775 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
776 38
                    $this->uow->setOriginalDocumentData($document, $data);
777 38
                    $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
778
                }
779 38
                if ($sorted) {
780 39
                    $collection->add($document);
781
                }
782
            }
783
        }
784 60
    }
785
786 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
787
    {
788 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
789 17
        $documents = $query->execute()->toArray();
790 17
        foreach ($documents as $key => $document) {
791 16
            $collection->add($document);
792
        }
793 17
    }
794
795
    /**
796
     * @param PersistentCollectionInterface $collection
797
     *
798
     * @return Query
799
     */
800 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
801
    {
802 17
        $hints = $collection->getHints();
803 17
        $mapping = $collection->getMapping();
804 17
        $owner = $collection->getOwner();
805 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
806 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
807 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? array();
808 17
        $mappedByFieldName = ClassMetadataInfo::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
809
810 17
        $criteria = $this->cm->merge(
811 17
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
812 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
813 17
            $mapping['criteria'] ?? array()
814
        );
815 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
816 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
817 17
            ->setQueryArray($criteria);
818
819 17
        if (isset($mapping['sort'])) {
820 17
            $qb->sort($mapping['sort']);
821
        }
822 17
        if (isset($mapping['limit'])) {
823 2
            $qb->limit($mapping['limit']);
824
        }
825 17
        if (isset($mapping['skip'])) {
826
            $qb->skip($mapping['skip']);
827
        }
828
829 17
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
830
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
831
        }
832
833 17
        foreach ($mapping['prime'] as $field) {
834 4
            $qb->field($field)->prime(true);
835
        }
836
837 17
        return $qb->getQuery();
838
    }
839
840 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
841
    {
842 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
843 5
        $mapping = $collection->getMapping();
844 5
        $documents = $cursor->toArray();
845 5
        foreach ($documents as $key => $obj) {
846 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
847 1
                $collection->set($key, $obj);
848
            } else {
849 5
                $collection->add($obj);
850
            }
851
        }
852 5
    }
853
854
    /**
855
     * @param PersistentCollectionInterface $collection
856
     *
857
     * @return \Iterator
858
     */
859 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
860
    {
861 5
        $mapping = $collection->getMapping();
862 5
        $repositoryMethod = $mapping['repositoryMethod'];
863 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
864 5
            ->$repositoryMethod($collection->getOwner());
865
866 5
        if ( ! $cursor instanceof Iterator) {
867
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object");
868
        }
869
870 5
        if (!empty($mapping['prime'])) {
871 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
872 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
873 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
874
875 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
876
        }
877
878 5
        return $cursor;
879
    }
880
881
    /**
882
     * Prepare a projection array by converting keys, which are PHP property
883
     * names, to MongoDB field names.
884
     *
885
     * @param array $fields
886
     * @return array
887
     */
888 14
    public function prepareProjection(array $fields)
889
    {
890 14
        $preparedFields = array();
891
892 14
        foreach ($fields as $key => $value) {
893 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
894
        }
895
896 14
        return $preparedFields;
897
    }
898
899
    /**
900
     * @param string $sort
901
     * @return int
902
     */
903 25
    private function getSortDirection($sort)
904
    {
905 25
        switch (strtolower($sort)) {
906
            case 'desc':
907 15
                return -1;
908
909
            case 'asc':
910 13
                return 1;
911
        }
912
913 12
        return $sort;
914
    }
915
916
    /**
917
     * Prepare a sort specification array by converting keys to MongoDB field
918
     * names and changing direction strings to int.
919
     *
920
     * @param array $fields
921
     * @return array
922
     */
923 141
    public function prepareSort(array $fields)
924
    {
925 141
        $sortFields = [];
926
927 141
        foreach ($fields as $key => $value) {
928 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
929
        }
930
931 141
        return $sortFields;
932
    }
933
934
    /**
935
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
936
     *
937
     * @param string $fieldName
938
     * @return string
939
     */
940 433
    public function prepareFieldName($fieldName)
941
    {
942 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
943
944 433
        return $fieldNames[0][0];
945
    }
946
947
    /**
948
     * Adds discriminator criteria to an already-prepared query.
949
     *
950
     * This method should be used once for query criteria and not be used for
951
     * nested expressions. It should be called before
952
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
953
     *
954
     * @param array $preparedQuery
955
     * @return array
956
     */
957 492
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
958
    {
959
        /* If the class has a discriminator field, which is not already in the
960
         * criteria, inject it now. The field/values need no preparation.
961
         */
962 492
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
963 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
964 29
            if (count($discriminatorValues) === 1) {
965 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
966
            } else {
967 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
968
            }
969
        }
970
971 492
        return $preparedQuery;
972
    }
973
974
    /**
975
     * Adds filter criteria to an already-prepared query.
976
     *
977
     * This method should be used once for query criteria and not be used for
978
     * nested expressions. It should be called after
979
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
980
     *
981
     * @param array $preparedQuery
982
     * @return array
983
     */
984 493
    public function addFilterToPreparedQuery(array $preparedQuery)
985
    {
986
        /* If filter criteria exists for this class, prepare it and merge
987
         * over the existing query.
988
         *
989
         * @todo Consider recursive merging in case the filter criteria and
990
         * prepared query both contain top-level $and/$or operators.
991
         */
992 493
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
993 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
994
        }
995
996 493
        return $preparedQuery;
997
    }
998
999
    /**
1000
     * Prepares the query criteria or new document object.
1001
     *
1002
     * PHP field names and types will be converted to those used by MongoDB.
1003
     *
1004
     * @param array $query
1005
     * @param bool $isNewObj
1006
     * @return array
1007
     */
1008 525
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1009
    {
1010 525
        $preparedQuery = array();
1011
1012 525
        foreach ($query as $key => $value) {
1013
            // Recursively prepare logical query clauses
1014 483
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1015 20
                foreach ($value as $k2 => $v2) {
1016 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1017
                }
1018 20
                continue;
1019
            }
1020
1021 483
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1022 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1023 38
                continue;
1024
            }
1025
1026 483
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1027 483
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1028 483
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1029 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1030 483
                    : Type::convertPHPToDatabaseValue($preparedValue);
1031
            }
1032
        }
1033
1034 525
        return $preparedQuery;
1035
    }
1036
1037
    /**
1038
     * Prepares a query value and converts the PHP value to the database value
1039
     * if it is an identifier.
1040
     *
1041
     * It also handles converting $fieldName to the database name if they are different.
1042
     *
1043
     * @param string $fieldName
1044
     * @param mixed $value
1045
     * @param ClassMetadata $class        Defaults to $this->class
1046
     * @param bool $prepareValue Whether or not to prepare the value
1047
     * @param bool $inNewObj Whether or not newObj is being prepared
1048
     * @return array An array of tuples containing prepared field names and values
1049
     */
1050 876
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1051
    {
1052 876
        $class = $class ?? $this->class;
1053
1054
        // @todo Consider inlining calls to ClassMetadataInfo methods
1055
1056
        // Process all non-identifier fields by translating field names
1057 876
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1058 247
            $mapping = $class->fieldMappings[$fieldName];
1059 247
            $fieldName = $mapping['name'];
1060
1061 247
            if ( ! $prepareValue) {
1062 52
                return [[$fieldName, $value]];
1063
            }
1064
1065
            // Prepare mapped, embedded objects
1066 205
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1067 205
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1068 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1069
            }
1070
1071 203
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoDB\BSON\ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1072
                try {
1073 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1074 1
                } catch (MappingException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Persiste...apping\MappingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1075
                    // do nothing in case passed object is not mapped document
1076
                }
1077
            }
1078
1079
            // No further preparation unless we're dealing with a simple reference
1080
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1081 190
            $arrayValue = (array) $value;
1082 190
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1083 126
                return [[$fieldName, $value]];
1084
            }
1085
1086
            // Additional preparation for one or more simple reference values
1087 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1088
1089 91
            if ( ! is_array($value)) {
1090 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1091
            }
1092
1093
            // Objects without operators or with DBRef fields can be converted immediately
1094 6 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
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...
1095 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1096
            }
1097
1098 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1099
        }
1100
1101
        // Process identifier fields
1102 789
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1103 334
            $fieldName = '_id';
1104
1105 334
            if ( ! $prepareValue) {
1106 42
                return [[$fieldName, $value]];
1107
            }
1108
1109 295
            if ( ! is_array($value)) {
1110 272
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1111
            }
1112
1113
            // Objects without operators or with DBRef fields can be converted immediately
1114 61 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
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...
1115 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1116
            }
1117
1118 56
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1119
        }
1120
1121
        // No processing for unmapped, non-identifier, non-dotted field names
1122 553
        if (strpos($fieldName, '.') === false) {
1123 414
            return [[$fieldName, $value]];
1124
        }
1125
1126
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1127
         *
1128
         * We can limit parsing here, since at most three segments are
1129
         * significant: "fieldName.objectProperty" with an optional index or key
1130
         * for collections stored as either BSON arrays or objects.
1131
         */
1132 152
        $e = explode('.', $fieldName, 4);
1133
1134
        // No further processing for unmapped fields
1135 152
        if ( ! isset($class->fieldMappings[$e[0]])) {
1136 6
            return [[$fieldName, $value]];
1137
        }
1138
1139 147
        $mapping = $class->fieldMappings[$e[0]];
1140 147
        $e[0] = $mapping['name'];
1141
1142
        // Hash and raw fields will not be prepared beyond the field name
1143 147
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1144 1
            $fieldName = implode('.', $e);
1145
1146 1
            return [[$fieldName, $value]];
1147
        }
1148
1149 146
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1150 146
                && isset($e[2])) {
1151 1
            $objectProperty = $e[2];
1152 1
            $objectPropertyPrefix = $e[1] . '.';
1153 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1154 145
        } elseif ($e[1] != '$') {
1155 144
            $fieldName = $e[0] . '.' . $e[1];
1156 144
            $objectProperty = $e[1];
1157 144
            $objectPropertyPrefix = '';
1158 144
            $nextObjectProperty = implode('.', array_slice($e, 2));
1159 1
        } elseif (isset($e[2])) {
1160 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1161 1
            $objectProperty = $e[2];
1162 1
            $objectPropertyPrefix = $e[1] . '.';
1163 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1164
        } else {
1165 1
            $fieldName = $e[0] . '.' . $e[1];
1166
1167 1
            return [[$fieldName, $value]];
1168
        }
1169
1170
        // No further processing for fields without a targetDocument mapping
1171 146
        if ( ! isset($mapping['targetDocument'])) {
1172 3
            if ($nextObjectProperty) {
1173
                $fieldName .= '.'.$nextObjectProperty;
1174
            }
1175
1176 3
            return [[$fieldName, $value]];
1177
        }
1178
1179 143
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1180
1181
        // No further processing for unmapped targetDocument fields
1182 143
        if ( ! $targetClass->hasField($objectProperty)) {
1183 25
            if ($nextObjectProperty) {
1184
                $fieldName .= '.'.$nextObjectProperty;
1185
            }
1186
1187 25
            return [[$fieldName, $value]];
1188
        }
1189
1190 123
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1191 123
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1192
1193
        // Prepare DBRef identifiers or the mapped field's property path
1194 123
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1195 105
            ? ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $e[0])
1196 123
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1197
1198
        // Process targetDocument identifier fields
1199 123
        if ($objectPropertyIsId) {
1200 106
            if ( ! $prepareValue) {
1201 7
                return [[$fieldName, $value]];
1202
            }
1203
1204 99
            if ( ! is_array($value)) {
1205 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1206
            }
1207
1208
            // Objects without operators or with DBRef fields can be converted immediately
1209 16 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
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...
1210 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1211
            }
1212
1213 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1214
        }
1215
1216
        /* The property path may include a third field segment, excluding the
1217
         * collection item pointer. If present, this next object property must
1218
         * be processed recursively.
1219
         */
1220 17
        if ($nextObjectProperty) {
1221
            // Respect the targetDocument's class metadata when recursing
1222 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1223 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1224 14
                : null;
1225
1226 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1227
1228 14
            return array_map(function ($preparedTuple) use ($fieldName) {
1229 14
                list($key, $value) = $preparedTuple;
1230
1231 14
                return [$fieldName . '.' . $key, $value];
1232 14
            }, $fieldNames);
1233
        }
1234
1235 5
        return [[$fieldName, $value]];
1236
    }
1237
1238
    /**
1239
     * Prepares a query expression.
1240
     *
1241
     * @param array|object  $expression
1242
     * @param ClassMetadata $class
1243
     * @return array
1244
     */
1245 78
    private function prepareQueryExpression($expression, $class)
1246
    {
1247 78
        foreach ($expression as $k => $v) {
1248
            // Ignore query operators whose arguments need no type conversion
1249 78
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1250 16
                continue;
1251
            }
1252
1253
            // Process query operators whose argument arrays need type conversion
1254 78
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1255 76
                foreach ($v as $k2 => $v2) {
1256 76
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1257
                }
1258 76
                continue;
1259
            }
1260
1261
            // Recursively process expressions within a $not operator
1262 18
            if ($k === '$not' && is_array($v)) {
1263 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1264 15
                continue;
1265
            }
1266
1267 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1268
        }
1269
1270 78
        return $expression;
1271
    }
1272
1273
    /**
1274
     * Checks whether the value has DBRef fields.
1275
     *
1276
     * This method doesn't check if the the value is a complete DBRef object,
1277
     * although it should return true for a DBRef. Rather, we're checking that
1278
     * the value has one or more fields for a DBref. In practice, this could be
1279
     * $elemMatch criteria for matching a DBRef.
1280
     *
1281
     * @param mixed $value
1282
     * @return boolean
1283
     */
1284 79
    private function hasDBRefFields($value)
1285
    {
1286 79
        if ( ! is_array($value) && ! is_object($value)) {
1287
            return false;
1288
        }
1289
1290 79
        if (is_object($value)) {
1291
            $value = get_object_vars($value);
1292
        }
1293
1294 79
        foreach ($value as $key => $_) {
1295 79
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1296 79
                return true;
1297
            }
1298
        }
1299
1300 78
        return false;
1301
    }
1302
1303
    /**
1304
     * Checks whether the value has query operators.
1305
     *
1306
     * @param mixed $value
1307
     * @return boolean
1308
     */
1309 83
    private function hasQueryOperators($value)
1310
    {
1311 83
        if ( ! is_array($value) && ! is_object($value)) {
1312
            return false;
1313
        }
1314
1315 83
        if (is_object($value)) {
1316
            $value = get_object_vars($value);
1317
        }
1318
1319 83
        foreach ($value as $key => $_) {
1320 83
            if (isset($key[0]) && $key[0] === '$') {
1321 83
                return true;
1322
            }
1323
        }
1324
1325 11
        return false;
1326
    }
1327
1328
    /**
1329
     * Gets the array of discriminator values for the given ClassMetadata
1330
     *
1331
     * @param ClassMetadata $metadata
1332
     * @return array
1333
     */
1334 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1335
    {
1336 29
        $discriminatorValues = array($metadata->discriminatorValue);
1337 29
        foreach ($metadata->subClasses as $className) {
1338 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1339 8
                $discriminatorValues[] = $key;
1340
            }
1341
        }
1342
1343
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1344 29 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
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...
1345 2
            $discriminatorValues[] = null;
1346
        }
1347
1348 29
        return $discriminatorValues;
1349
    }
1350
1351 547
    private function handleCollections($document, $options)
1352
    {
1353
        // Collection deletions (deletions of complete collections)
1354 547
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1355 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1356 103
                $this->cp->delete($coll, $options);
1357
            }
1358
        }
1359
        // Collection updates (deleteRows, updateRows, insertRows)
1360 547
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1361 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1362 103
                $this->cp->update($coll, $options);
1363
            }
1364
        }
1365
        // Take new snapshots from visited collections
1366 547
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1367 226
            $coll->takeSnapshot();
1368
        }
1369 547
    }
1370
1371
    /**
1372
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1373
     * Also, shard key field should be present in actual document data.
1374
     *
1375
     * @param object $document
1376
     * @param string $shardKeyField
1377
     * @param array  $actualDocumentData
1378
     *
1379
     * @throws MongoDBException
1380
     */
1381 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1382
    {
1383 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1384 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1385
1386 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1387 4
        $fieldName = $fieldMapping['fieldName'];
1388
1389 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1390
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1391
        }
1392
1393 4
        if (!isset($actualDocumentData[$fieldName])) {
1394
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1395
        }
1396 4
    }
1397
1398
    /**
1399
     * Get shard key aware query for single document.
1400
     *
1401
     * @param object $document
1402
     *
1403
     * @return array
1404
     */
1405 264
    private function getQueryForDocument($document)
1406
    {
1407 264
        $id = $this->uow->getDocumentIdentifier($document);
1408 264
        $id = $this->class->getDatabaseIdentifierValue($id);
1409
1410 264
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1411 264
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1412
1413 264
        return $query;
1414
    }
1415
1416
    /**
1417
     * @param array $options
1418
     *
1419
     * @return array
1420
     */
1421 548
    private function getWriteOptions(array $options = array())
1422
    {
1423 548
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1424 548
        $documentOptions = [];
1425 548
        if ($this->class->hasWriteConcern()) {
1426 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1427
        }
1428
1429 548
        return array_merge($defaultOptions, $documentOptions, $options);
1430
    }
1431
1432
    /**
1433
     * @param string $fieldName
1434
     * @param mixed $value
1435
     * @param array $mapping
1436
     * @param bool $inNewObj
1437
     * @return array
1438
     */
1439 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1440
    {
1441 15
        $reference = $this->dm->createReference($value, $mapping);
1442 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1443 8
            return [[$fieldName, $reference]];
1444
        }
1445
1446 6
        switch ($mapping['storeAs']) {
1447
            case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
1448
                $keys = ['id' => true];
1449
                break;
1450
1451
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF:
1452
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1453 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1454
1455 6
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF) {
1456 5
                    unset($keys['$db']);
1457
                }
1458
1459 6
                if (isset($mapping['targetDocument'])) {
1460 4
                    unset($keys['$ref'], $keys['$db']);
1461
                }
1462 6
                break;
1463
1464
            default:
1465
                throw new \InvalidArgumentException("Reference type {$mapping['storeAs']} is invalid.");
1466
        }
1467
1468 6
        if ($mapping['type'] === 'many') {
1469 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1470
        }
1471
1472 4
        return array_map(
1473 4
            function ($key) use ($reference, $fieldName) {
1474 4
                return [$fieldName . '.' . $key, $reference[$key]];
1475 4
            },
1476 4
            array_keys($keys)
1477
        );
1478
    }
1479
}
1480