Completed
Pull Request — master (#1790)
by Andreas
16:34
created

DocumentPersister::getInserts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Doctrine\Common\Persistence\Mapping\MappingException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
10
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
11
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
12
use Doctrine\ODM\MongoDB\Iterator\Iterator;
13
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
14
use Doctrine\ODM\MongoDB\LockException;
15
use Doctrine\ODM\MongoDB\LockMode;
16
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
17
use Doctrine\ODM\MongoDB\MongoDBException;
18
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
19
use Doctrine\ODM\MongoDB\Proxy\Proxy;
20
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
21
use Doctrine\ODM\MongoDB\Query\Query;
22
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
23
use Doctrine\ODM\MongoDB\Types\Type;
24
use Doctrine\ODM\MongoDB\UnitOfWork;
25
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
26
use MongoDB\BSON\ObjectId;
27
use MongoDB\Collection;
28
use MongoDB\Driver\Cursor;
29
use MongoDB\Driver\Exception\Exception as DriverException;
30
use MongoDB\Driver\Exception\WriteException;
31
use MongoDB\GridFS\Bucket;
32
use function array_combine;
33
use function array_fill;
34
use function array_intersect_key;
35
use function array_keys;
36
use function array_map;
37
use function array_merge;
38
use function array_search;
39
use function array_slice;
40
use function array_values;
41
use function count;
42
use function explode;
43
use function get_class;
44
use function get_object_vars;
45
use function implode;
46
use function in_array;
47
use function is_array;
48
use function is_object;
49
use function is_scalar;
50
use function max;
51
use function spl_object_hash;
52
use function sprintf;
53
use function strpos;
54
use function strtolower;
55
56
/**
57
 * The DocumentPersister is responsible for persisting documents.
58
 *
59
 */
60
class DocumentPersister
61
{
62
    /**
63
     * The PersistenceBuilder instance.
64
     *
65
     * @var PersistenceBuilder
66
     */
67
    private $pb;
68
69
    /**
70
     * The DocumentManager instance.
71
     *
72
     * @var DocumentManager
73
     */
74
    private $dm;
75
76
    /**
77
     * The UnitOfWork instance.
78
     *
79
     * @var UnitOfWork
80
     */
81
    private $uow;
82
83
    /**
84
     * The ClassMetadata instance for the document type being persisted.
85
     *
86
     * @var ClassMetadata
87
     */
88
    private $class;
89
90
    /**
91
     * The MongoCollection instance for this document.
92
     *
93
     * @var Collection
94
     */
95
    private $collection;
96
97
    /** @var Bucket|null */
98
    private $bucket;
99
100
    /**
101
     * Array of queued inserts for the persister to insert.
102
     *
103
     * @var array
104
     */
105
    private $queuedInserts = [];
106
107
    /**
108
     * Array of queued inserts for the persister to insert.
109
     *
110
     * @var array
111
     */
112
    private $queuedUpserts = [];
113
114
    /**
115
     * The CriteriaMerger instance.
116
     *
117
     * @var CriteriaMerger
118
     */
119
    private $cm;
120
121
    /**
122
     * The CollectionPersister instance.
123
     *
124
     * @var CollectionPersister
125
     */
126
    private $cp;
127
128
    /**
129
     * The HydratorFactory instance.
130
     *
131
     * @var HydratorFactory
132
     */
133
    private $hydratorFactory;
134
135
    /**
136
     * Initializes this instance.
137
     *
138
     */
139 1080
    public function __construct(
140
        PersistenceBuilder $pb,
141
        DocumentManager $dm,
142
        UnitOfWork $uow,
143
        HydratorFactory $hydratorFactory,
144
        ClassMetadata $class,
145
        ?CriteriaMerger $cm = null
146
    ) {
147 1080
        $this->pb = $pb;
148 1080
        $this->dm = $dm;
149 1080
        $this->cm = $cm ?: new CriteriaMerger();
150 1080
        $this->uow = $uow;
151 1080
        $this->hydratorFactory = $hydratorFactory;
152 1080
        $this->class = $class;
153 1080
        $this->collection = $dm->getDocumentCollection($class->name);
154 1080
        $this->cp = $this->uow->getCollectionPersister();
155
156 1080
        if (! $class->isFile) {
157 1075
            return;
158
        }
159
160 7
        $this->bucket = $dm->getDocumentBucket($class->name);
161 7
    }
162
163
    /**
164
     * @return array
165
     */
166
    public function getInserts()
167
    {
168
        return $this->queuedInserts;
169
    }
170
171
    /**
172
     * @param object $document
173
     * @return bool
174
     */
175
    public function isQueuedForInsert($document)
176
    {
177
        return isset($this->queuedInserts[spl_object_hash($document)]);
178
    }
179
180
    /**
181
     * Adds a document to the queued insertions.
182
     * The document remains queued until {@link executeInserts} is invoked.
183
     *
184
     * @param object $document The document to queue for insertion.
185
     */
186 478
    public function addInsert($document)
187
    {
188 478
        $this->queuedInserts[spl_object_hash($document)] = $document;
189 478
    }
190
191
    /**
192
     * @return array
193
     */
194
    public function getUpserts()
195
    {
196
        return $this->queuedUpserts;
197
    }
198
199
    /**
200
     * @param object $document
201
     * @return bool
202
     */
203
    public function isQueuedForUpsert($document)
204
    {
205
        return isset($this->queuedUpserts[spl_object_hash($document)]);
206
    }
207
208
    /**
209
     * Adds a document to the queued upserts.
210
     * The document remains queued until {@link executeUpserts} is invoked.
211
     *
212
     * @param object $document The document to queue for insertion.
213
     */
214 83
    public function addUpsert($document)
215
    {
216 83
        $this->queuedUpserts[spl_object_hash($document)] = $document;
217 83
    }
218
219
    /**
220
     * Gets the ClassMetadata instance of the document class this persister is used for.
221
     *
222
     * @return ClassMetadata
223
     */
224
    public function getClassMetadata()
225
    {
226
        return $this->class;
227
    }
228
229
    /**
230
     * Executes all queued document insertions.
231
     *
232
     * Queued documents without an ID will inserted in a batch and queued
233
     * documents with an ID will be upserted individually.
234
     *
235
     * If no inserts are queued, invoking this method is a NOOP.
236
     *
237
     * @param array $options Options for batchInsert() and update() driver methods
238
     */
239 478
    public function executeInserts(array $options = [])
240
    {
241 478
        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...
242
            return;
243
        }
244
245 478
        $inserts = [];
246 478
        $options = $this->getWriteOptions($options);
247 478
        foreach ($this->queuedInserts as $oid => $document) {
248 478
            $data = $this->pb->prepareInsertData($document);
249
250
            // Set the initial version for each insert
251 477
            if ($this->class->isVersioned) {
252 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
253 20
                $nextVersion = null;
254 20
                if ($versionMapping['type'] === 'int') {
255 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
256 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
257 2
                } elseif ($versionMapping['type'] === 'date') {
258 2
                    $nextVersionDateTime = new \DateTime();
259 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
260 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
261
                }
262 20
                $data[$versionMapping['name']] = $nextVersion;
263
            }
264
265 477
            $inserts[] = $data;
266
        }
267
268 477
        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...
269
            try {
270 477
                $this->collection->insertMany($inserts, $options);
271 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...
272 6
                $this->queuedInserts = [];
273 6
                throw $e;
274
            }
275
        }
276
277
        /* All collections except for ones using addToSet have already been
278
         * saved. We have left these to be handled separately to avoid checking
279
         * collection for uniqueness on PHP side.
280
         */
281 477
        foreach ($this->queuedInserts as $document) {
282 477
            $this->handleCollections($document, $options);
283
        }
284
285 477
        $this->queuedInserts = [];
286 477
    }
287
288
    /**
289
     * Executes all queued document upserts.
290
     *
291
     * Queued documents with an ID are upserted individually.
292
     *
293
     * If no upserts are queued, invoking this method is a NOOP.
294
     *
295
     * @param array $options Options for batchInsert() and update() driver methods
296
     */
297 83
    public function executeUpserts(array $options = [])
298
    {
299 83
        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...
300
            return;
301
        }
302
303 83
        $options = $this->getWriteOptions($options);
304 83
        foreach ($this->queuedUpserts as $oid => $document) {
305
            try {
306 83
                $this->executeUpsert($document, $options);
307 83
                $this->handleCollections($document, $options);
308 83
                unset($this->queuedUpserts[$oid]);
309
            } 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...
310
                unset($this->queuedUpserts[$oid]);
311 83
                throw $e;
312
            }
313
        }
314 83
    }
315
316
    /**
317
     * Executes a single upsert in {@link executeUpserts}
318
     *
319
     * @param object $document
320
     * @param array  $options
321
     */
322 83
    private function executeUpsert($document, array $options)
323
    {
324 83
        $options['upsert'] = true;
325 83
        $criteria = $this->getQueryForDocument($document);
326
327 83
        $data = $this->pb->prepareUpsertData($document);
328
329
        // Set the initial version for each upsert
330 83
        if ($this->class->isVersioned) {
331 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
332 2
            $nextVersion = null;
333 2
            if ($versionMapping['type'] === 'int') {
334 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
335 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
336 1
            } elseif ($versionMapping['type'] === 'date') {
337 1
                $nextVersionDateTime = new \DateTime();
338 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
339 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
340
            }
341 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
342
        }
343
344 83
        foreach (array_keys($criteria) as $field) {
345 83
            unset($data['$set'][$field]);
346 83
            unset($data['$inc'][$field]);
347 83
            unset($data['$setOnInsert'][$field]);
348
        }
349
350
        // Do not send empty update operators
351 83
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
352 83
            if (! empty($data[$operator])) {
353 68
                continue;
354
            }
355
356 83
            unset($data[$operator]);
357
        }
358
359
        /* If there are no modifiers remaining, we're upserting a document with
360
         * an identifier as its only field. Since a document with the identifier
361
         * may already exist, the desired behavior is "insert if not exists" and
362
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
363
         * the identifier to the same value in our criteria.
364
         *
365
         * This will fail for versions before MongoDB 2.6, which require an
366
         * empty $set modifier. The best we can do (without attempting to check
367
         * server versions in advance) is attempt the 2.6+ behavior and retry
368
         * after the relevant exception.
369
         *
370
         * See: https://jira.mongodb.org/browse/SERVER-12266
371
         */
372 83
        if (empty($data)) {
373 16
            $retry = true;
374 16
            $data = ['$set' => ['_id' => $criteria['_id']]];
375
        }
376
377
        try {
378 83
            $this->collection->updateOne($criteria, $data, $options);
379 83
            return;
380
        } 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...
381
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
382
                throw $e;
383
            }
384
        }
385
386
        $this->collection->updateOne($criteria, ['$set' => new \stdClass()], $options);
387
    }
388
389
    /**
390
     * Updates the already persisted document if it has any new changesets.
391
     *
392
     * @param object $document
393
     * @param array  $options  Array of options to be used with update()
394
     * @throws LockException
395
     */
396 197
    public function update($document, array $options = [])
397
    {
398 197
        $update = $this->pb->prepareUpdateData($document);
399
400 197
        $query = $this->getQueryForDocument($document);
401
402 197
        foreach (array_keys($query) as $field) {
403 197
            unset($update['$set'][$field]);
404
        }
405
406 197
        if (empty($update['$set'])) {
407 90
            unset($update['$set']);
408
        }
409
410
        // Include versioning logic to set the new version value in the database
411
        // and to ensure the version has not changed since this document object instance
412
        // was fetched from the database
413 197
        $nextVersion = null;
414 197
        if ($this->class->isVersioned) {
415 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
416 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
417 13
            if ($versionMapping['type'] === 'int') {
418 10
                $nextVersion = $currentVersion + 1;
419 10
                $update['$inc'][$versionMapping['name']] = 1;
420 10
                $query[$versionMapping['name']] = $currentVersion;
421 3
            } elseif ($versionMapping['type'] === 'date') {
422 3
                $nextVersion = new \DateTime();
423 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
424 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
425
            }
426
        }
427
428 197
        if (! empty($update)) {
429
            // Include locking logic so that if the document object in memory is currently
430
            // locked then it will remove it, otherwise it ensures the document is not locked.
431 130
            if ($this->class->isLockable) {
432 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
433 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
434 11
                if ($isLocked) {
435 2
                    $update['$unset'] = [$lockMapping['name'] => true];
436
                } else {
437 9
                    $query[$lockMapping['name']] = ['$exists' => false];
438
                }
439
            }
440
441 130
            $options = $this->getWriteOptions($options);
442
443 130
            $result = $this->collection->updateOne($query, $update, $options);
444
445 130
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
446 4
                throw LockException::lockFailed($document);
447 126
            } elseif ($this->class->isVersioned) {
448 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
449
            }
450
        }
451
452 193
        $this->handleCollections($document, $options);
453 193
    }
454
455
    /**
456
     * Removes document from mongo
457
     *
458
     * @param mixed $document
459
     * @param array $options  Array of options to be used with remove()
460
     * @throws LockException
461
     */
462 33
    public function delete($document, array $options = [])
463
    {
464 33
        if ($this->bucket instanceof Bucket) {
0 ignored issues
show
Bug introduced by
The class MongoDB\GridFS\Bucket 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...
465 1
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
466 1
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
467
468 1
            $this->bucket->delete($databaseIdentifier);
469
470 1
            return;
471
        }
472
473 32
        $query = $this->getQueryForDocument($document);
474
475 32
        if ($this->class->isLockable) {
476 2
            $query[$this->class->lockField] = ['$exists' => false];
477
        }
478
479 32
        $options = $this->getWriteOptions($options);
480
481 32
        $result = $this->collection->deleteOne($query, $options);
482
483 32
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
484 2
            throw LockException::lockFailed($document);
485
        }
486 30
    }
487
488
    /**
489
     * Refreshes a managed document.
490
     *
491
     * @param object $document The document to refresh.
492
     */
493 20
    public function refresh($document)
494
    {
495 20
        $query = $this->getQueryForDocument($document);
496 20
        $data = $this->collection->findOne($query);
497 20
        $data = $this->hydratorFactory->hydrate($document, $data);
498 20
        $this->uow->setOriginalDocumentData($document, $data);
499 20
    }
500
501
    /**
502
     * Finds a document by a set of criteria.
503
     *
504
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
505
     * be used to match an _id value.
506
     *
507
     * @param mixed  $criteria Query criteria
508
     * @param object $document Document to load the data into. If not specified, a new document is created.
509
     * @param array  $hints    Hints for document creation
510
     * @param int    $lockMode
511
     * @param array  $sort     Sort array for Cursor::sort()
512
     * @throws LockException
513
     * @return object|null The loaded and managed document instance or null if no document was found
514
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
515
     */
516 348
    public function load($criteria, $document = null, array $hints = [], $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...
517
    {
518
        // TODO: remove this
519 348
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof 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...
520
            $criteria = ['_id' => $criteria];
521
        }
522
523 348
        $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...
524 348
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
525 348
        $criteria = $this->addFilterToPreparedQuery($criteria);
526
527 348
        $options = [];
528 348
        if ($sort !== null) {
529 92
            $options['sort'] = $this->prepareSort($sort);
530
        }
531 348
        $result = $this->collection->findOne($criteria, $options);
532
533 348
        if ($this->class->isLockable) {
534 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
535 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
536 1
                throw LockException::lockFailed($result);
537
            }
538
        }
539
540 347
        return $this->createDocument($result, $document, $hints);
541
    }
542
543
    /**
544
     * Finds documents by a set of criteria.
545
     *
546
     * @param array    $criteria Query criteria
547
     * @param array    $sort     Sort array for Cursor::sort()
548
     * @param int|null $limit    Limit for Cursor::limit()
549
     * @param int|null $skip     Skip for Cursor::skip()
550
     * @return Iterator
551
     */
552 22
    public function loadAll(array $criteria = [], ?array $sort = null, $limit = null, $skip = null)
553
    {
554 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
555 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
556 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
557
558 22
        $options = [];
559 22
        if ($sort !== null) {
560 11
            $options['sort'] = $this->prepareSort($sort);
561
        }
562
563 22
        if ($limit !== null) {
564 10
            $options['limit'] = $limit;
565
        }
566
567 22
        if ($skip !== null) {
568 1
            $options['skip'] = $skip;
569
        }
570
571 22
        $baseCursor = $this->collection->find($criteria, $options);
572 22
        $cursor = $this->wrapCursor($baseCursor);
573
574 22
        return $cursor;
575
    }
576
577
    /**
578
     * @param object $document
579
     *
580
     * @return array
581
     * @throws MongoDBException
582
     */
583 269
    private function getShardKeyQuery($document)
584
    {
585 269
        if (! $this->class->isSharded()) {
586 265
            return [];
587
        }
588
589 4
        $shardKey = $this->class->getShardKey();
590 4
        $keys = array_keys($shardKey['keys']);
591 4
        $data = $this->uow->getDocumentActualData($document);
592
593 4
        $shardKeyQueryPart = [];
594 4
        foreach ($keys as $key) {
595 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
596 4
            $this->guardMissingShardKey($document, $key, $data);
597
598 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
599 1
                $reference = $this->prepareReference(
600 1
                    $key,
601 1
                    $data[$mapping['fieldName']],
602 1
                    $mapping,
603 1
                    false
604
                );
605 1
                foreach ($reference as $keyValue) {
606 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
607
                }
608
            } else {
609 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
610 4
                $shardKeyQueryPart[$key] = $value;
611
            }
612
        }
613
614 4
        return $shardKeyQueryPart;
615
    }
616
617
    /**
618
     * Wraps the supplied base cursor in the corresponding ODM class.
619
     *
620
     */
621 22
    private function wrapCursor(Cursor $baseCursor): Iterator
622
    {
623 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
624
    }
625
626
    /**
627
     * Checks whether the given managed document exists in the database.
628
     *
629
     * @param object $document
630
     * @return bool TRUE if the document exists in the database, FALSE otherwise.
631
     */
632 3
    public function exists($document)
633
    {
634 3
        $id = $this->class->getIdentifierObject($document);
635 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
636
    }
637
638
    /**
639
     * Locks document by storing the lock mode on the mapped lock field.
640
     *
641
     * @param object $document
642
     * @param int    $lockMode
643
     */
644 5
    public function lock($document, $lockMode)
645
    {
646 5
        $id = $this->uow->getDocumentIdentifier($document);
647 5
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
648 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
649 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
650 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
651 5
    }
652
653
    /**
654
     * Releases any lock that exists on this document.
655
     *
656
     * @param object $document
657
     */
658 1
    public function unlock($document)
659
    {
660 1
        $id = $this->uow->getDocumentIdentifier($document);
661 1
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
662 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
663 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
664 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
665 1
    }
666
667
    /**
668
     * Creates or fills a single document object from an query result.
669
     *
670
     * @param object $result   The query result.
671
     * @param object $document The document object to fill, if any.
672
     * @param array  $hints    Hints for document creation.
673
     * @return object The filled and managed document object or NULL, if the query result is empty.
674
     */
675 347
    private function createDocument($result, $document = null, array $hints = [])
676
    {
677 347
        if ($result === null) {
678 111
            return null;
679
        }
680
681 305
        if ($document !== null) {
682 28
            $hints[Query::HINT_REFRESH] = true;
683 28
            $id = $this->class->getPHPIdentifierValue($result['_id']);
684 28
            $this->uow->registerManaged($document, $id, $result);
685
        }
686
687 305
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
688
    }
689
690
    /**
691
     * Loads a PersistentCollection data. Used in the initialize() method.
692
     *
693
     */
694 164
    public function loadCollection(PersistentCollectionInterface $collection)
695
    {
696 164
        $mapping = $collection->getMapping();
697 164
        switch ($mapping['association']) {
698
            case ClassMetadata::EMBED_MANY:
699 109
                $this->loadEmbedManyCollection($collection);
700 109
                break;
701
702
            case ClassMetadata::REFERENCE_MANY:
703 77
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
704 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
705
                } else {
706 73
                    if ($mapping['isOwningSide']) {
707 61
                        $this->loadReferenceManyCollectionOwningSide($collection);
708
                    } else {
709 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
710
                    }
711
                }
712 77
                break;
713
        }
714 164
    }
715
716 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
717
    {
718 109
        $embeddedDocuments = $collection->getMongoData();
719 109
        $mapping = $collection->getMapping();
720 109
        $owner = $collection->getOwner();
721 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...
722 58
            return;
723
        }
724
725 82
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
726 82
            $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
727 82
            $embeddedMetadata = $this->dm->getClassMetadata($className);
728 82
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
729
730 82
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
731
732 82
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
733 82
            $id = $data[$embeddedMetadata->identifier] ?? null;
734
735 82
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
736 81
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
737
            }
738 82
            if (CollectionHelper::isHash($mapping['strategy'])) {
739 10
                $collection->set($key, $embeddedDocumentObject);
740
            } else {
741 82
                $collection->add($embeddedDocumentObject);
742
            }
743
        }
744 82
    }
745
746 61
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
747
    {
748 61
        $hints = $collection->getHints();
749 61
        $mapping = $collection->getMapping();
750 61
        $groupedIds = [];
751
752 61
        $sorted = isset($mapping['sort']) && $mapping['sort'];
753
754 61
        foreach ($collection->getMongoData() as $key => $reference) {
755 55
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
756 55
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
757 55
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
758
759
            // create a reference to the class and id
760 55
            $reference = $this->dm->getReference($className, $id);
761
762
            // no custom sort so add the references right now in the order they are embedded
763 55
            if (! $sorted) {
764 54
                if (CollectionHelper::isHash($mapping['strategy'])) {
765 2
                    $collection->set($key, $reference);
766
                } else {
767 52
                    $collection->add($reference);
768
                }
769
            }
770
771
            // only query for the referenced object if it is not already initialized or the collection is sorted
772 55
            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...
773 22
                continue;
774
            }
775
776 40
            $groupedIds[$className][] = $identifier;
777
        }
778 61
        foreach ($groupedIds as $className => $ids) {
779 40
            $class = $this->dm->getClassMetadata($className);
780 40
            $mongoCollection = $this->dm->getDocumentCollection($className);
781 40
            $criteria = $this->cm->merge(
782 40
                ['_id' => ['$in' => array_values($ids)]],
783 40
                $this->dm->getFilterCollection()->getFilterCriteria($class),
784 40
                $mapping['criteria'] ?? []
785
            );
786 40
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
787
788 40
            $options = [];
789 40
            if (isset($mapping['sort'])) {
790 40
                $options['sort'] = $this->prepareSort($mapping['sort']);
791
            }
792 40
            if (isset($mapping['limit'])) {
793
                $options['limit'] = $mapping['limit'];
794
            }
795 40
            if (isset($mapping['skip'])) {
796
                $options['skip'] = $mapping['skip'];
797
            }
798 40
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
799
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
800
            }
801
802 40
            $cursor = $mongoCollection->find($criteria, $options);
803 40
            $documents = $cursor->toArray();
804 40
            foreach ($documents as $documentData) {
805 39
                $document = $this->uow->getById($documentData['_id'], $class);
806 39
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
807 39
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
808 39
                    $this->uow->setOriginalDocumentData($document, $data);
809 39
                    $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...
810
                }
811 39
                if (! $sorted) {
812 38
                    continue;
813
                }
814
815 40
                $collection->add($document);
816
            }
817
        }
818 61
    }
819
820 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
821
    {
822 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
823 17
        $documents = $query->execute()->toArray();
824 17
        foreach ($documents as $key => $document) {
825 16
            $collection->add($document);
826
        }
827 17
    }
828
829
    /**
830
     *
831
     * @return Query
832
     */
833 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
834
    {
835 17
        $hints = $collection->getHints();
836 17
        $mapping = $collection->getMapping();
837 17
        $owner = $collection->getOwner();
838 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
839 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
840 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
841 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
842
843 17
        $criteria = $this->cm->merge(
844 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
845 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
846 17
            $mapping['criteria'] ?? []
847
        );
848 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
849 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
850 17
            ->setQueryArray($criteria);
851
852 17
        if (isset($mapping['sort'])) {
853 17
            $qb->sort($mapping['sort']);
854
        }
855 17
        if (isset($mapping['limit'])) {
856 2
            $qb->limit($mapping['limit']);
857
        }
858 17
        if (isset($mapping['skip'])) {
859
            $qb->skip($mapping['skip']);
860
        }
861
862 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
863
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
864
        }
865
866 17
        foreach ($mapping['prime'] as $field) {
867 4
            $qb->field($field)->prime(true);
868
        }
869
870 17
        return $qb->getQuery();
871
    }
872
873 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
874
    {
875 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
876 5
        $mapping = $collection->getMapping();
877 5
        $documents = $cursor->toArray();
878 5
        foreach ($documents as $key => $obj) {
879 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
880 1
                $collection->set($key, $obj);
881
            } else {
882 5
                $collection->add($obj);
883
            }
884
        }
885 5
    }
886
887
    /**
888
     *
889
     * @return \Iterator
890
     */
891 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
892
    {
893 5
        $mapping = $collection->getMapping();
894 5
        $repositoryMethod = $mapping['repositoryMethod'];
895 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
896 5
            ->$repositoryMethod($collection->getOwner());
897
898 5
        if (! $cursor instanceof Iterator) {
899
            throw new \BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
900
        }
901
902 5
        if (! empty($mapping['prime'])) {
903 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
904 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
905 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
906
907 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
908
        }
909
910 5
        return $cursor;
911
    }
912
913
    /**
914
     * Prepare a projection array by converting keys, which are PHP property
915
     * names, to MongoDB field names.
916
     *
917
     * @param array $fields
918
     * @return array
919
     */
920 14
    public function prepareProjection(array $fields)
921
    {
922 14
        $preparedFields = [];
923
924 14
        foreach ($fields as $key => $value) {
925 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
926
        }
927
928 14
        return $preparedFields;
929
    }
930
931
    /**
932
     * @param string $sort
933
     * @return int
934
     */
935 25
    private function getSortDirection($sort)
936
    {
937 25
        switch (strtolower((string) $sort)) {
938 25
            case 'desc':
939 15
                return -1;
940
941 22
            case 'asc':
942 13
                return 1;
943
        }
944
945 12
        return $sort;
946
    }
947
948
    /**
949
     * Prepare a sort specification array by converting keys to MongoDB field
950
     * names and changing direction strings to int.
951
     *
952
     * @param array $fields
953
     * @return array
954
     */
955 142
    public function prepareSort(array $fields)
956
    {
957 142
        $sortFields = [];
958
959 142
        foreach ($fields as $key => $value) {
960 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
961
        }
962
963 142
        return $sortFields;
964
    }
965
966
    /**
967
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
968
     *
969
     * @param string $fieldName
970
     * @return string
971
     */
972 433
    public function prepareFieldName($fieldName)
973
    {
974 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
975
976 433
        return $fieldNames[0][0];
977
    }
978
979
    /**
980
     * Adds discriminator criteria to an already-prepared query.
981
     *
982
     * This method should be used once for query criteria and not be used for
983
     * nested expressions. It should be called before
984
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
985
     *
986
     * @param array $preparedQuery
987
     * @return array
988
     */
989 499
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
990
    {
991
        /* If the class has a discriminator field, which is not already in the
992
         * criteria, inject it now. The field/values need no preparation.
993
         */
994 499
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
995 27
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
996 27
            if (count($discriminatorValues) === 1) {
997 19
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
998
            } else {
999 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
1000
            }
1001
        }
1002
1003 499
        return $preparedQuery;
1004
    }
1005
1006
    /**
1007
     * Adds filter criteria to an already-prepared query.
1008
     *
1009
     * This method should be used once for query criteria and not be used for
1010
     * nested expressions. It should be called after
1011
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
1012
     *
1013
     * @param array $preparedQuery
1014
     * @return array
1015
     */
1016 500
    public function addFilterToPreparedQuery(array $preparedQuery)
1017
    {
1018
        /* If filter criteria exists for this class, prepare it and merge
1019
         * over the existing query.
1020
         *
1021
         * @todo Consider recursive merging in case the filter criteria and
1022
         * prepared query both contain top-level $and/$or operators.
1023
         */
1024 500
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
1025 500
        if ($filterCriteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filterCriteria 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...
1026 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1027
        }
1028
1029 500
        return $preparedQuery;
1030
    }
1031
1032
    /**
1033
     * Prepares the query criteria or new document object.
1034
     *
1035
     * PHP field names and types will be converted to those used by MongoDB.
1036
     *
1037
     * @param array $query
1038
     * @param bool  $isNewObj
1039
     * @return array
1040
     */
1041 532
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1042
    {
1043 532
        $preparedQuery = [];
1044
1045 532
        foreach ($query as $key => $value) {
1046
            // Recursively prepare logical query clauses
1047 490
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
1048 20
                foreach ($value as $k2 => $v2) {
1049 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1050
                }
1051 20
                continue;
1052
            }
1053
1054 490
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1055 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1056 38
                continue;
1057
            }
1058
1059 490
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1060 490
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1061 490
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1062 134
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1063 490
                    : Type::convertPHPToDatabaseValue($preparedValue);
1064
            }
1065
        }
1066
1067 532
        return $preparedQuery;
1068
    }
1069
1070
    /**
1071
     * Prepares a query value and converts the PHP value to the database value
1072
     * if it is an identifier.
1073
     *
1074
     * It also handles converting $fieldName to the database name if they are different.
1075
     *
1076
     * @param string        $fieldName
1077
     * @param mixed         $value
1078
     * @param ClassMetadata $class        Defaults to $this->class
1079
     * @param bool          $prepareValue Whether or not to prepare the value
1080
     * @param bool          $inNewObj     Whether or not newObj is being prepared
1081
     * @return array An array of tuples containing prepared field names and values
1082
     */
1083 883
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1084
    {
1085 883
        $class = $class ?? $this->class;
1086
1087
        // @todo Consider inlining calls to ClassMetadata methods
1088
1089
        // Process all non-identifier fields by translating field names
1090 883
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1091 250
            $mapping = $class->fieldMappings[$fieldName];
1092 250
            $fieldName = $mapping['name'];
1093
1094 250
            if (! $prepareValue) {
1095 52
                return [[$fieldName, $value]];
1096
            }
1097
1098
            // Prepare mapped, embedded objects
1099 208
            if (! empty($mapping['embedded']) && is_object($value) &&
1100 208
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1101 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1102
            }
1103
1104 206
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof 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...
1105
                try {
1106 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1107 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...
1108
                    // do nothing in case passed object is not mapped document
1109
                }
1110
            }
1111
1112
            // No further preparation unless we're dealing with a simple reference
1113
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1114 193
            $arrayValue = (array) $value;
1115 193
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1116 129
                return [[$fieldName, $value]];
1117
            }
1118
1119
            // Additional preparation for one or more simple reference values
1120 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1121
1122 91
            if (! is_array($value)) {
1123 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1124
            }
1125
1126
            // Objects without operators or with DBRef fields can be converted immediately
1127 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1128 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1129
            }
1130
1131 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1132
        }
1133
1134
        // Process identifier fields
1135 794
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1136 339
            $fieldName = '_id';
1137
1138 339
            if (! $prepareValue) {
1139 42
                return [[$fieldName, $value]];
1140
            }
1141
1142 300
            if (! is_array($value)) {
1143 274
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1144
            }
1145
1146
            // Objects without operators or with DBRef fields can be converted immediately
1147 62
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1148 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1149
            }
1150
1151 57
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1152
        }
1153
1154
        // No processing for unmapped, non-identifier, non-dotted field names
1155 553
        if (strpos($fieldName, '.') === false) {
1156 414
            return [[$fieldName, $value]];
1157
        }
1158
1159
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1160
         *
1161
         * We can limit parsing here, since at most three segments are
1162
         * significant: "fieldName.objectProperty" with an optional index or key
1163
         * for collections stored as either BSON arrays or objects.
1164
         */
1165 152
        $e = explode('.', $fieldName, 4);
1166
1167
        // No further processing for unmapped fields
1168 152
        if (! isset($class->fieldMappings[$e[0]])) {
1169 6
            return [[$fieldName, $value]];
1170
        }
1171
1172 147
        $mapping = $class->fieldMappings[$e[0]];
1173 147
        $e[0] = $mapping['name'];
1174
1175
        // Hash and raw fields will not be prepared beyond the field name
1176 147
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1177 1
            $fieldName = implode('.', $e);
1178
1179 1
            return [[$fieldName, $value]];
1180
        }
1181
1182 146
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1183 146
                && isset($e[2])) {
1184 1
            $objectProperty = $e[2];
1185 1
            $objectPropertyPrefix = $e[1] . '.';
1186 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1187 145
        } elseif ($e[1] !== '$') {
1188 144
            $fieldName = $e[0] . '.' . $e[1];
1189 144
            $objectProperty = $e[1];
1190 144
            $objectPropertyPrefix = '';
1191 144
            $nextObjectProperty = implode('.', array_slice($e, 2));
1192 1
        } elseif (isset($e[2])) {
1193 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1194 1
            $objectProperty = $e[2];
1195 1
            $objectPropertyPrefix = $e[1] . '.';
1196 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1197
        } else {
1198 1
            $fieldName = $e[0] . '.' . $e[1];
1199
1200 1
            return [[$fieldName, $value]];
1201
        }
1202
1203
        // No further processing for fields without a targetDocument mapping
1204 146
        if (! isset($mapping['targetDocument'])) {
1205 3
            if ($nextObjectProperty) {
1206
                $fieldName .= '.' . $nextObjectProperty;
1207
            }
1208
1209 3
            return [[$fieldName, $value]];
1210
        }
1211
1212 143
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1213
1214
        // No further processing for unmapped targetDocument fields
1215 143
        if (! $targetClass->hasField($objectProperty)) {
1216 25
            if ($nextObjectProperty) {
1217
                $fieldName .= '.' . $nextObjectProperty;
1218
            }
1219
1220 25
            return [[$fieldName, $value]];
1221
        }
1222
1223 123
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1224 123
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1225
1226
        // Prepare DBRef identifiers or the mapped field's property path
1227 123
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID)
1228 105
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1229 123
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1230
1231
        // Process targetDocument identifier fields
1232 123
        if ($objectPropertyIsId) {
1233 106
            if (! $prepareValue) {
1234 7
                return [[$fieldName, $value]];
1235
            }
1236
1237 99
            if (! is_array($value)) {
1238 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1239
            }
1240
1241
            // Objects without operators or with DBRef fields can be converted immediately
1242 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1243 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1244
            }
1245
1246 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1247
        }
1248
1249
        /* The property path may include a third field segment, excluding the
1250
         * collection item pointer. If present, this next object property must
1251
         * be processed recursively.
1252
         */
1253 17
        if ($nextObjectProperty) {
1254
            // Respect the targetDocument's class metadata when recursing
1255 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1256 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1257 14
                : null;
1258
1259 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1260
1261
            return array_map(function ($preparedTuple) use ($fieldName) {
1262 14
                list($key, $value) = $preparedTuple;
1263
1264 14
                return [$fieldName . '.' . $key, $value];
1265 14
            }, $fieldNames);
1266
        }
1267
1268 5
        return [[$fieldName, $value]];
1269
    }
1270
1271
    /**
1272
     * Prepares a query expression.
1273
     *
1274
     * @param array|object  $expression
1275
     * @param ClassMetadata $class
1276
     * @return array
1277
     */
1278 79
    private function prepareQueryExpression($expression, $class)
1279
    {
1280 79
        foreach ($expression as $k => $v) {
1281
            // Ignore query operators whose arguments need no type conversion
1282 79
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1283 16
                continue;
1284
            }
1285
1286
            // Process query operators whose argument arrays need type conversion
1287 79
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1288 77
                foreach ($v as $k2 => $v2) {
1289 77
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1290
                }
1291 77
                continue;
1292
            }
1293
1294
            // Recursively process expressions within a $not operator
1295 18
            if ($k === '$not' && is_array($v)) {
1296 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1297 15
                continue;
1298
            }
1299
1300 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1301
        }
1302
1303 79
        return $expression;
1304
    }
1305
1306
    /**
1307
     * Checks whether the value has DBRef fields.
1308
     *
1309
     * This method doesn't check if the the value is a complete DBRef object,
1310
     * although it should return true for a DBRef. Rather, we're checking that
1311
     * the value has one or more fields for a DBref. In practice, this could be
1312
     * $elemMatch criteria for matching a DBRef.
1313
     *
1314
     * @param mixed $value
1315
     * @return bool
1316
     */
1317 80
    private function hasDBRefFields($value)
1318
    {
1319 80
        if (! is_array($value) && ! is_object($value)) {
1320
            return false;
1321
        }
1322
1323 80
        if (is_object($value)) {
1324
            $value = get_object_vars($value);
1325
        }
1326
1327 80
        foreach ($value as $key => $_) {
1328 80
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1329 80
                return true;
1330
            }
1331
        }
1332
1333 79
        return false;
1334
    }
1335
1336
    /**
1337
     * Checks whether the value has query operators.
1338
     *
1339
     * @param mixed $value
1340
     * @return bool
1341
     */
1342 84
    private function hasQueryOperators($value)
1343
    {
1344 84
        if (! is_array($value) && ! is_object($value)) {
1345
            return false;
1346
        }
1347
1348 84
        if (is_object($value)) {
1349
            $value = get_object_vars($value);
1350
        }
1351
1352 84
        foreach ($value as $key => $_) {
1353 84
            if (isset($key[0]) && $key[0] === '$') {
1354 84
                return true;
1355
            }
1356
        }
1357
1358 11
        return false;
1359
    }
1360
1361
    /**
1362
     * Gets the array of discriminator values for the given ClassMetadata
1363
     *
1364
     * @return array
1365
     */
1366 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1367
    {
1368 27
        $discriminatorValues = [$metadata->discriminatorValue];
1369 27
        foreach ($metadata->subClasses as $className) {
1370 8
            $key = array_search($className, $metadata->discriminatorMap);
1371 8
            if (! $key) {
1372
                continue;
1373
            }
1374
1375 8
            $discriminatorValues[] = $key;
1376
        }
1377
1378
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1379 27
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
1380 2
            $discriminatorValues[] = null;
1381
        }
1382
1383 27
        return $discriminatorValues;
1384
    }
1385
1386 548
    private function handleCollections($document, $options)
1387
    {
1388
        // Collection deletions (deletions of complete collections)
1389 548
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1390 104
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1391 94
                continue;
1392
            }
1393
1394 30
            $this->cp->delete($coll, $options);
1395
        }
1396
        // Collection updates (deleteRows, updateRows, insertRows)
1397 548
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1398 104
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1399 28
                continue;
1400
            }
1401
1402 97
            $this->cp->update($coll, $options);
1403
        }
1404
        // Take new snapshots from visited collections
1405 548
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1406 227
            $coll->takeSnapshot();
1407
        }
1408 548
    }
1409
1410
    /**
1411
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1412
     * Also, shard key field should be present in actual document data.
1413
     *
1414
     * @param object $document
1415
     * @param string $shardKeyField
1416
     * @param array  $actualDocumentData
1417
     *
1418
     * @throws MongoDBException
1419
     */
1420 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1421
    {
1422 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1423 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1424
1425 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1426 4
        $fieldName = $fieldMapping['fieldName'];
1427
1428 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1429
            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...
1430
        }
1431
1432 4
        if (! isset($actualDocumentData[$fieldName])) {
1433
            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...
1434
        }
1435 4
    }
1436
1437
    /**
1438
     * Get shard key aware query for single document.
1439
     *
1440
     * @param object $document
1441
     *
1442
     * @return array
1443
     */
1444 265
    private function getQueryForDocument($document)
1445
    {
1446 265
        $id = $this->uow->getDocumentIdentifier($document);
1447 265
        $id = $this->class->getDatabaseIdentifierValue($id);
1448
1449 265
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1450 265
        $query = array_merge(['_id' => $id], $shardKeyQueryPart);
1451
1452 265
        return $query;
1453
    }
1454
1455
    /**
1456
     * @param array $options
1457
     *
1458
     * @return array
1459
     */
1460 549
    private function getWriteOptions(array $options = [])
1461
    {
1462 549
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1463 549
        $documentOptions = [];
1464 549
        if ($this->class->hasWriteConcern()) {
1465 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1466
        }
1467
1468 549
        return array_merge($defaultOptions, $documentOptions, $options);
1469
    }
1470
1471
    /**
1472
     * @param string $fieldName
1473
     * @param mixed  $value
1474
     * @param array  $mapping
1475
     * @param bool   $inNewObj
1476
     * @return array
1477
     */
1478 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1479
    {
1480 15
        $reference = $this->dm->createReference($value, $mapping);
1481 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1482 8
            return [[$fieldName, $reference]];
1483
        }
1484
1485 6
        switch ($mapping['storeAs']) {
1486
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1487
                $keys = ['id' => true];
1488
                break;
1489
1490
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1491
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1492 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1493
1494 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1495 5
                    unset($keys['$db']);
1496
                }
1497
1498 6
                if (isset($mapping['targetDocument'])) {
1499 4
                    unset($keys['$ref'], $keys['$db']);
1500
                }
1501 6
                break;
1502
1503
            default:
1504
                throw new \InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1505
        }
1506
1507 6
        if ($mapping['type'] === 'many') {
1508 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1509
        }
1510
1511 4
        return array_map(
1512
            function ($key) use ($reference, $fieldName) {
1513 4
                return [$fieldName . '.' . $key, $reference[$key]];
1514 4
            },
1515 4
            array_keys($keys)
1516
        );
1517
    }
1518
}
1519