Completed
Pull Request — master (#1803)
by Andreas
15:44
created

createReferenceManyWithRepositoryMethodCursor()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3.0032

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 13
cts 14
cp 0.9286
rs 9.552
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.0032
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use BadMethodCallException;
8
use DateTime;
9
use Doctrine\Common\Persistence\Mapping\MappingException;
10
use Doctrine\ODM\MongoDB\DocumentManager;
11
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
12
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
13
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
14
use Doctrine\ODM\MongoDB\Iterator\Iterator;
15
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
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\Query\CriteriaMerger;
22
use Doctrine\ODM\MongoDB\Query\Query;
23
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
use Doctrine\ODM\MongoDB\UnitOfWork;
26
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
27
use InvalidArgumentException;
28
use MongoDB\BSON\ObjectId;
29
use MongoDB\Collection;
30
use MongoDB\Driver\Cursor;
31
use MongoDB\Driver\Exception\Exception as DriverException;
32
use MongoDB\Driver\Exception\WriteException;
33
use MongoDB\GridFS\Bucket;
34
use ProxyManager\Proxy\GhostObjectInterface;
35
use stdClass;
36
use function array_combine;
37
use function array_fill;
38
use function array_intersect_key;
39
use function array_keys;
40
use function array_map;
41
use function array_merge;
42
use function array_search;
43
use function array_slice;
44
use function array_values;
45
use function assert;
46
use function count;
47
use function explode;
48
use function get_class;
49
use function get_object_vars;
50
use function implode;
51
use function in_array;
52
use function is_array;
53
use function is_object;
54
use function is_scalar;
55
use function is_string;
56
use function max;
57
use function spl_object_hash;
58
use function sprintf;
59
use function strpos;
60
use function strtolower;
61
62
/**
63
 * The DocumentPersister is responsible for persisting documents.
64
 */
65
class DocumentPersister
66
{
67
    /** @var PersistenceBuilder */
68
    private $pb;
69
70
    /** @var DocumentManager */
71
    private $dm;
72
73
    /** @var UnitOfWork */
74
    private $uow;
75
76
    /** @var ClassMetadata */
77
    private $class;
78
79
    /** @var Collection */
80
    private $collection;
81
82
    /** @var Bucket|null */
83
    private $bucket;
84
85
    /**
86
     * Array of queued inserts for the persister to insert.
87
     *
88
     * @var array
89
     */
90
    private $queuedInserts = [];
91
92
    /**
93
     * Array of queued inserts for the persister to insert.
94
     *
95
     * @var array
96
     */
97
    private $queuedUpserts = [];
98
99
    /** @var CriteriaMerger */
100
    private $cm;
101
102
    /** @var CollectionPersister */
103
    private $cp;
104
105
    /** @var HydratorFactory */
106
    private $hydratorFactory;
107
108 1121
    public function __construct(
109
        PersistenceBuilder $pb,
110
        DocumentManager $dm,
111
        UnitOfWork $uow,
112
        HydratorFactory $hydratorFactory,
113
        ClassMetadata $class,
114
        ?CriteriaMerger $cm = null
115
    ) {
116 1121
        $this->pb              = $pb;
117 1121
        $this->dm              = $dm;
118 1121
        $this->cm              = $cm ?: new CriteriaMerger();
119 1121
        $this->uow             = $uow;
120 1121
        $this->hydratorFactory = $hydratorFactory;
121 1121
        $this->class           = $class;
122 1121
        $this->collection      = $dm->getDocumentCollection($class->name);
123 1121
        $this->cp              = $this->uow->getCollectionPersister();
124
125 1121
        if (! $class->isFile) {
126 1113
            return;
127
        }
128
129 10
        $this->bucket = $dm->getDocumentBucket($class->name);
130 10
    }
131
132
    public function getInserts() : array
133
    {
134
        return $this->queuedInserts;
135
    }
136
137
    public function isQueuedForInsert(object $document) : bool
138
    {
139
        return isset($this->queuedInserts[spl_object_hash($document)]);
140
    }
141
142
    /**
143
     * Adds a document to the queued insertions.
144
     * The document remains queued until {@link executeInserts} is invoked.
145
     */
146 512
    public function addInsert(object $document) : void
147
    {
148 512
        $this->queuedInserts[spl_object_hash($document)] = $document;
149 512
    }
150
151
    public function getUpserts() : array
152
    {
153
        return $this->queuedUpserts;
154
    }
155
156
    public function isQueuedForUpsert(object $document) : bool
157
    {
158
        return isset($this->queuedUpserts[spl_object_hash($document)]);
159
    }
160
161
    /**
162
     * Adds a document to the queued upserts.
163
     * The document remains queued until {@link executeUpserts} is invoked.
164
     */
165 85
    public function addUpsert(object $document) : void
166
    {
167 85
        $this->queuedUpserts[spl_object_hash($document)] = $document;
168 85
    }
169
170
    /**
171
     * Gets the ClassMetadata instance of the document class this persister is
172
     * used for.
173
     */
174
    public function getClassMetadata() : ClassMetadata
175
    {
176
        return $this->class;
177
    }
178
179
    /**
180
     * Executes all queued document insertions.
181
     *
182
     * Queued documents without an ID will inserted in a batch and queued
183
     * documents with an ID will be upserted individually.
184
     *
185
     * If no inserts are queued, invoking this method is a NOOP.
186
     *
187
     * @throws DriverException
188
     */
189 512
    public function executeInserts(array $options = []) : void
190
    {
191 512
        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...
192
            return;
193
        }
194
195 512
        $inserts = [];
196 512
        $options = $this->getWriteOptions($options);
197 512
        foreach ($this->queuedInserts as $oid => $document) {
198 512
            $data = $this->pb->prepareInsertData($document);
199
200
            // Set the initial version for each insert
201 511
            if ($this->class->isVersioned) {
202 40
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
203 40
                $nextVersion    = null;
204 40
                if ($versionMapping['type'] === 'int') {
205 38
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
206 38
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
207 2
                } elseif ($versionMapping['type'] === 'date') {
208 2
                    $nextVersionDateTime = new DateTime();
209 2
                    $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
210 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
211
                }
212 40
                $data[$versionMapping['name']] = $nextVersion;
213
            }
214
215 511
            $inserts[] = $data;
216
        }
217
218 511
        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...
219
            try {
220 511
                $this->collection->insertMany($inserts, $options);
221 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...
222 6
                $this->queuedInserts = [];
223 6
                throw $e;
224
            }
225
        }
226
227
        /* All collections except for ones using addToSet have already been
228
         * saved. We have left these to be handled separately to avoid checking
229
         * collection for uniqueness on PHP side.
230
         */
231 511
        foreach ($this->queuedInserts as $document) {
232 511
            $this->handleCollections($document, $options);
233
        }
234
235 511
        $this->queuedInserts = [];
236 511
    }
237
238
    /**
239
     * Executes all queued document upserts.
240
     *
241
     * Queued documents with an ID are upserted individually.
242
     *
243
     * If no upserts are queued, invoking this method is a NOOP.
244
     */
245 85
    public function executeUpserts(array $options = []) : void
246
    {
247 85
        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...
248
            return;
249
        }
250
251 85
        $options = $this->getWriteOptions($options);
252 85
        foreach ($this->queuedUpserts as $oid => $document) {
253
            try {
254 85
                $this->executeUpsert($document, $options);
255 85
                $this->handleCollections($document, $options);
256 85
                unset($this->queuedUpserts[$oid]);
257
            } 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...
258
                unset($this->queuedUpserts[$oid]);
259 85
                throw $e;
260
            }
261
        }
262 85
    }
263
264
    /**
265
     * Executes a single upsert in {@link executeUpserts}
266
     */
267 85
    private function executeUpsert(object $document, array $options) : void
268
    {
269 85
        $options['upsert'] = true;
270 85
        $criteria          = $this->getQueryForDocument($document);
271
272 85
        $data = $this->pb->prepareUpsertData($document);
273
274
        // Set the initial version for each upsert
275 85
        if ($this->class->isVersioned) {
276 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
277 3
            $nextVersion    = null;
278 3
            if ($versionMapping['type'] === 'int') {
279 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
280 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
281 1
            } elseif ($versionMapping['type'] === 'date') {
282 1
                $nextVersionDateTime = new DateTime();
283 1
                $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
284 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
285
            }
286 3
            $data['$set'][$versionMapping['name']] = $nextVersion;
287
        }
288
289 85
        foreach (array_keys($criteria) as $field) {
290 85
            unset($data['$set'][$field]);
291 85
            unset($data['$inc'][$field]);
292 85
            unset($data['$setOnInsert'][$field]);
293
        }
294
295
        // Do not send empty update operators
296 85
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
297 85
            if (! empty($data[$operator])) {
298 70
                continue;
299
            }
300
301 85
            unset($data[$operator]);
302
        }
303
304
        /* If there are no modifiers remaining, we're upserting a document with
305
         * an identifier as its only field. Since a document with the identifier
306
         * may already exist, the desired behavior is "insert if not exists" and
307
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
308
         * the identifier to the same value in our criteria.
309
         *
310
         * This will fail for versions before MongoDB 2.6, which require an
311
         * empty $set modifier. The best we can do (without attempting to check
312
         * server versions in advance) is attempt the 2.6+ behavior and retry
313
         * after the relevant exception.
314
         *
315
         * See: https://jira.mongodb.org/browse/SERVER-12266
316
         */
317 85
        if (empty($data)) {
318 16
            $retry = true;
319 16
            $data  = ['$set' => ['_id' => $criteria['_id']]];
320
        }
321
322
        try {
323 85
            $this->collection->updateOne($criteria, $data, $options);
324 85
            return;
325
        } 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...
326
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
327
                throw $e;
328
            }
329
        }
330
331
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
332
    }
333
334
    /**
335
     * Updates the already persisted document if it has any new changesets.
336
     *
337
     * @throws LockException
338
     */
339 221
    public function update(object $document, array $options = []) : void
340
    {
341 221
        $update = $this->pb->prepareUpdateData($document);
342
343 221
        $query = $this->getQueryForDocument($document);
344
345 219
        foreach (array_keys($query) as $field) {
346 219
            unset($update['$set'][$field]);
347
        }
348
349 219
        if (empty($update['$set'])) {
350 94
            unset($update['$set']);
351
        }
352
353
        // Include versioning logic to set the new version value in the database
354
        // and to ensure the version has not changed since this document object instance
355
        // was fetched from the database
356 219
        $nextVersion = null;
357 219
        if ($this->class->isVersioned) {
358 33
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
359 33
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
360 33
            if ($versionMapping['type'] === 'int') {
361 30
                $nextVersion                             = $currentVersion + 1;
362 30
                $update['$inc'][$versionMapping['name']] = 1;
363 30
                $query[$versionMapping['name']]          = $currentVersion;
364 3
            } elseif ($versionMapping['type'] === 'date') {
365 3
                $nextVersion                             = new DateTime();
366 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
367 3
                $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
368
            }
369
        }
370
371 219
        if (! empty($update)) {
372
            // Include locking logic so that if the document object in memory is currently
373
            // locked then it will remove it, otherwise it ensures the document is not locked.
374 151
            if ($this->class->isLockable) {
375 11
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
376 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
377 11
                if ($isLocked) {
378 2
                    $update['$unset'] = [$lockMapping['name'] => true];
379
                } else {
380 9
                    $query[$lockMapping['name']] = ['$exists' => false];
381
                }
382
            }
383
384 151
            $options = $this->getWriteOptions($options);
385
386 151
            $result = $this->collection->updateOne($query, $update, $options);
387
388 151
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
389 6
                throw LockException::lockFailed($document);
390 146
            } elseif ($this->class->isVersioned) {
391 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
392
            }
393
        }
394
395 214
        $this->handleCollections($document, $options);
396 214
    }
397
398
    /**
399
     * Removes document from mongo
400
     *
401
     * @throws LockException
402
     */
403 36
    public function delete(object $document, array $options = []) : void
404
    {
405 36
        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...
406 1
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
407 1
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
408
409 1
            $this->bucket->delete($databaseIdentifier);
410
411 1
            return;
412
        }
413
414 35
        $query = $this->getQueryForDocument($document);
415
416 35
        if ($this->class->isLockable) {
417 2
            $query[$this->class->lockField] = ['$exists' => false];
418
        }
419
420 35
        $options = $this->getWriteOptions($options);
421
422 35
        $result = $this->collection->deleteOne($query, $options);
423
424 35
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
425 2
            throw LockException::lockFailed($document);
426
        }
427 33
    }
428
429
    /**
430
     * Refreshes a managed document.
431
     */
432 23
    public function refresh(object $document) : void
433
    {
434 23
        $query = $this->getQueryForDocument($document);
435 23
        $data  = $this->collection->findOne($query);
436 23
        if ($data === null) {
437
            throw MongoDBException::cannotRefreshDocument();
438
        }
439 23
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
440 23
        $this->uow->setOriginalDocumentData($document, $data);
441 23
    }
442
443
    /**
444
     * Finds a document by a set of criteria.
445
     *
446
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
447
     * be used to match an _id value.
448
     *
449
     * @param mixed $criteria Query criteria
450
     *
451
     * @throws LockException
452
     *
453
     * @todo Check identity map? loadById method? Try to guess whether
454
     *     $criteria is the id?
455
     */
456 371
    public function load($criteria, ?object $document = null, array $hints = [], int $lockMode = 0, ?array $sort = null) : ?object
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...
457
    {
458
        // TODO: remove this
459 371
        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...
460
            $criteria = ['_id' => $criteria];
461
        }
462
463 371
        $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...
464 371
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
465 371
        $criteria = $this->addFilterToPreparedQuery($criteria);
466
467 371
        $options = [];
468 371
        if ($sort !== null) {
469 95
            $options['sort'] = $this->prepareSort($sort);
470
        }
471 371
        $result = $this->collection->findOne($criteria, $options);
472 371
        $result = $result !== null ? (array) $result : null;
473
474 371
        if ($this->class->isLockable) {
475 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
476 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
477 1
                throw LockException::lockFailed($document);
478
            }
479
        }
480
481 370
        if ($result === null) {
482 116
            return null;
483
        }
484
485 326
        return $this->createDocument($result, $document, $hints);
486
    }
487
488
    /**
489
     * Finds documents by a set of criteria.
490
     */
491 22
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
492
    {
493 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
494 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
495 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
496
497 22
        $options = [];
498 22
        if ($sort !== null) {
499 11
            $options['sort'] = $this->prepareSort($sort);
500
        }
501
502 22
        if ($limit !== null) {
503 10
            $options['limit'] = $limit;
504
        }
505
506 22
        if ($skip !== null) {
507 1
            $options['skip'] = $skip;
508
        }
509
510 22
        $baseCursor = $this->collection->find($criteria, $options);
511 22
        return $this->wrapCursor($baseCursor);
512
    }
513
514
    /**
515
     * @throws MongoDBException
516
     */
517 301
    private function getShardKeyQuery(object $document) : array
518
    {
519 301
        if (! $this->class->isSharded()) {
520 291
            return [];
521
        }
522
523 10
        $shardKey = $this->class->getShardKey();
524 10
        $keys     = array_keys($shardKey['keys']);
525 10
        $data     = $this->uow->getDocumentActualData($document);
526
527 10
        $shardKeyQueryPart = [];
528 10
        foreach ($keys as $key) {
529 10
            assert(is_string($key));
530 10
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
531 10
            $this->guardMissingShardKey($document, $key, $data);
532
533 8
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
534 1
                $reference = $this->prepareReference(
535 1
                    $key,
536 1
                    $data[$mapping['fieldName']],
537 1
                    $mapping,
538 1
                    false
539
                );
540 1
                foreach ($reference as $keyValue) {
541 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
542
                }
543
            } else {
544 7
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
545 8
                $shardKeyQueryPart[$key] = $value;
546
            }
547
        }
548
549 8
        return $shardKeyQueryPart;
550
    }
551
552
    /**
553
     * Wraps the supplied base cursor in the corresponding ODM class.
554
     */
555 22
    private function wrapCursor(Cursor $baseCursor) : Iterator
556
    {
557 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
558
    }
559
560
    /**
561
     * Checks whether the given managed document exists in the database.
562
     */
563 3
    public function exists(object $document) : bool
564
    {
565 3
        $id = $this->class->getIdentifierObject($document);
566 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
567
    }
568
569
    /**
570
     * Locks document by storing the lock mode on the mapped lock field.
571
     */
572 5
    public function lock(object $document, int $lockMode) : void
573
    {
574 5
        $id          = $this->uow->getDocumentIdentifier($document);
575 5
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
576 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
577 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
578 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
579 5
    }
580
581
    /**
582
     * Releases any lock that exists on this document.
583
     */
584 1
    public function unlock(object $document) : void
585
    {
586 1
        $id          = $this->uow->getDocumentIdentifier($document);
587 1
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
588 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
589 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
590 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
591 1
    }
592
593
    /**
594
     * Creates or fills a single document object from an query result.
595
     *
596
     * @param array  $result   The query result.
597
     * @param object $document The document object to fill, if any.
598
     * @param array  $hints    Hints for document creation.
599
     *
600
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
601
     */
602 326
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
603
    {
604 326
        if ($document !== null) {
605 29
            $hints[Query::HINT_REFRESH] = true;
606 29
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
607 29
            $this->uow->registerManaged($document, $id, $result);
608
        }
609
610 326
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
611
    }
612
613
    /**
614
     * Loads a PersistentCollection data. Used in the initialize() method.
615
     */
616 184
    public function loadCollection(PersistentCollectionInterface $collection) : void
617
    {
618 184
        $mapping = $collection->getMapping();
619 184
        switch ($mapping['association']) {
620
            case ClassMetadata::EMBED_MANY:
621 129
                $this->loadEmbedManyCollection($collection);
622 129
                break;
623
624
            case ClassMetadata::REFERENCE_MANY:
625 78
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
626 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
627
                } else {
628 74
                    if ($mapping['isOwningSide']) {
629 62
                        $this->loadReferenceManyCollectionOwningSide($collection);
630
                    } else {
631 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
632
                    }
633
                }
634 78
                break;
635
        }
636 184
    }
637
638 129
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
639
    {
640 129
        $embeddedDocuments = $collection->getMongoData();
641 129
        $mapping           = $collection->getMapping();
642 129
        $owner             = $collection->getOwner();
643 129
        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...
644 75
            return;
645
        }
646
647 100
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
648 100
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
649 100
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
650 100
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
651
652 100
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
653
654 100
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
655 100
            $id   = $data[$embeddedMetadata->identifier] ?? null;
656
657 100
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
658 99
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
659
            }
660 100
            if (CollectionHelper::isHash($mapping['strategy'])) {
661 25
                $collection->set($key, $embeddedDocumentObject);
662
            } else {
663 100
                $collection->add($embeddedDocumentObject);
664
            }
665
        }
666 100
    }
667
668 62
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
669
    {
670 62
        $hints      = $collection->getHints();
671 62
        $mapping    = $collection->getMapping();
672 62
        $groupedIds = [];
673
674 62
        $sorted = isset($mapping['sort']) && $mapping['sort'];
675
676 62
        foreach ($collection->getMongoData() as $key => $reference) {
677 56
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
678 56
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
679 56
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
680
681
            // create a reference to the class and id
682 56
            $reference = $this->dm->getReference($className, $id);
683
684
            // no custom sort so add the references right now in the order they are embedded
685 56
            if (! $sorted) {
686 55
                if (CollectionHelper::isHash($mapping['strategy'])) {
687 2
                    $collection->set($key, $reference);
688
                } else {
689 53
                    $collection->add($reference);
690
                }
691
            }
692
693
            // only query for the referenced object if it is not already initialized or the collection is sorted
694 56
            if (! (($reference instanceof GhostObjectInterface && ! $reference->isProxyInitialized())) && ! $sorted) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
695 22
                continue;
696
            }
697
698 41
            $groupedIds[$className][] = $identifier;
699
        }
700 62
        foreach ($groupedIds as $className => $ids) {
701 41
            $class           = $this->dm->getClassMetadata($className);
702 41
            $mongoCollection = $this->dm->getDocumentCollection($className);
703 41
            $criteria        = $this->cm->merge(
704 41
                ['_id' => ['$in' => array_values($ids)]],
705 41
                $this->dm->getFilterCollection()->getFilterCriteria($class),
706 41
                $mapping['criteria'] ?? []
707
            );
708 41
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
709
710 41
            $options = [];
711 41
            if (isset($mapping['sort'])) {
712 41
                $options['sort'] = $this->prepareSort($mapping['sort']);
713
            }
714 41
            if (isset($mapping['limit'])) {
715
                $options['limit'] = $mapping['limit'];
716
            }
717 41
            if (isset($mapping['skip'])) {
718
                $options['skip'] = $mapping['skip'];
719
            }
720 41
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
721
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
722
            }
723
724 41
            $cursor    = $mongoCollection->find($criteria, $options);
725 41
            $documents = $cursor->toArray();
726 41
            foreach ($documents as $documentData) {
727 40
                $document = $this->uow->getById($documentData['_id'], $class);
728 40
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
729 40
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
730 40
                    $this->uow->setOriginalDocumentData($document, $data);
731
                }
732
733 40
                if (! $sorted) {
734 39
                    continue;
735
                }
736
737 41
                $collection->add($document);
738
            }
739
        }
740 62
    }
741
742 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
743
    {
744 17
        $query    = $this->createReferenceManyInverseSideQuery($collection);
745 17
        $iterator = $query->execute();
746 17
        assert($iterator instanceof Iterator);
747 17
        $documents = $iterator->toArray();
748 17
        foreach ($documents as $key => $document) {
749 16
            $collection->add($document);
750
        }
751 17
    }
752
753 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
754
    {
755 17
        $hints             = $collection->getHints();
756 17
        $mapping           = $collection->getMapping();
757 17
        $owner             = $collection->getOwner();
758 17
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
759 17
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
760 17
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
761 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
762
763 17
        $criteria = $this->cm->merge(
764 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
765 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
766 17
            $mapping['criteria'] ?? []
767
        );
768 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
769 17
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
770 17
            ->setQueryArray($criteria);
771
772 17
        if (isset($mapping['sort'])) {
773 17
            $qb->sort($mapping['sort']);
774
        }
775 17
        if (isset($mapping['limit'])) {
776 2
            $qb->limit($mapping['limit']);
777
        }
778 17
        if (isset($mapping['skip'])) {
779
            $qb->skip($mapping['skip']);
780
        }
781
782 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
783
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
784
        }
785
786 17
        foreach ($mapping['prime'] as $field) {
787 4
            $qb->field($field)->prime(true);
788
        }
789
790 17
        return $qb->getQuery();
791
    }
792
793 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
794
    {
795 5
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
796 5
        $mapping   = $collection->getMapping();
797 5
        $documents = $cursor->toArray();
798 5
        foreach ($documents as $key => $obj) {
799 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
800 1
                $collection->set($key, $obj);
801
            } else {
802 5
                $collection->add($obj);
803
            }
804
        }
805 5
    }
806
807 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
808
    {
809 5
        $mapping          = $collection->getMapping();
810 5
        $repositoryMethod = $mapping['repositoryMethod'];
811 5
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
812 5
            ->$repositoryMethod($collection->getOwner());
813
814 5
        if (! $cursor instanceof Iterator) {
815
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
816
        }
817
818 5
        if (! empty($mapping['prime'])) {
819 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
820 1
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
821 1
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
822
823 1
            assert(is_array($primers));
824
825 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
826
        }
827
828 5
        return $cursor;
829
    }
830
831
    /**
832
     * Prepare a projection array by converting keys, which are PHP property
833
     * names, to MongoDB field names.
834
     */
835 14
    public function prepareProjection(array $fields) : array
836
    {
837 14
        $preparedFields = [];
838
839 14
        foreach ($fields as $key => $value) {
840 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
841
        }
842
843 14
        return $preparedFields;
844
    }
845
846
    /**
847
     * @param int|string $sort
848
     *
849
     * @return int|string|null
850
     */
851 25
    private function getSortDirection($sort)
852
    {
853 25
        switch (strtolower((string) $sort)) {
854 25
            case 'desc':
855 15
                return -1;
856
857 22
            case 'asc':
858 13
                return 1;
859
        }
860
861 12
        return $sort;
862
    }
863
864
    /**
865
     * Prepare a sort specification array by converting keys to MongoDB field
866
     * names and changing direction strings to int.
867
     */
868 144
    public function prepareSort(array $fields) : array
869
    {
870 144
        $sortFields = [];
871
872 144
        foreach ($fields as $key => $value) {
873 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
874
        }
875
876 144
        return $sortFields;
877
    }
878
879
    /**
880
     * Prepare a mongodb field name and convert the PHP property names to
881
     * MongoDB field names.
882
     */
883 436
    public function prepareFieldName(string $fieldName) : string
884
    {
885 436
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
886
887 436
        return $fieldNames[0][0];
888
    }
889
890
    /**
891
     * Adds discriminator criteria to an already-prepared query.
892
     *
893
     * This method should be used once for query criteria and not be used for
894
     * nested expressions. It should be called before
895
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
896
     */
897 525
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
898
    {
899
        /* If the class has a discriminator field, which is not already in the
900
         * criteria, inject it now. The field/values need no preparation.
901
         */
902 525
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
903 27
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
904 27
            if (count($discriminatorValues) === 1) {
905 19
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
906
            } else {
907 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
908
            }
909
        }
910
911 525
        return $preparedQuery;
912
    }
913
914
    /**
915
     * Adds filter criteria to an already-prepared query.
916
     *
917
     * This method should be used once for query criteria and not be used for
918
     * nested expressions. It should be called after
919
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
920
     */
921 526
    public function addFilterToPreparedQuery(array $preparedQuery) : array
922
    {
923
        /* If filter criteria exists for this class, prepare it and merge
924
         * over the existing query.
925
         *
926
         * @todo Consider recursive merging in case the filter criteria and
927
         * prepared query both contain top-level $and/$or operators.
928
         */
929 526
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
930 526
        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...
931 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
932
        }
933
934 526
        return $preparedQuery;
935
    }
936
937
    /**
938
     * Prepares the query criteria or new document object.
939
     *
940
     * PHP field names and types will be converted to those used by MongoDB.
941
     */
942 558
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
943
    {
944 558
        $preparedQuery = [];
945
946 558
        foreach ($query as $key => $value) {
947
            // Recursively prepare logical query clauses
948 517
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
949 20
                foreach ($value as $k2 => $v2) {
950 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
951
                }
952 20
                continue;
953
            }
954
955 517
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
956 40
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
957 40
                continue;
958
            }
959
960 517
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
961 517
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
962 517
                $preparedQuery[$preparedKey] = is_array($preparedValue)
0 ignored issues
show
Bug introduced by
The variable $preparedKey does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $preparedValue does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
963 135
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
964 517
                    : Type::convertPHPToDatabaseValue($preparedValue);
965
            }
966
        }
967
968 558
        return $preparedQuery;
969
    }
970
971
    /**
972
     * Prepares a query value and converts the PHP value to the database value
973
     * if it is an identifier.
974
     *
975
     * It also handles converting $fieldName to the database name if they are
976
     * different.
977
     *
978
     * @param mixed $value
979
     */
980 913
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
981
    {
982 913
        $class = $class ?? $this->class;
983
984
        // @todo Consider inlining calls to ClassMetadata methods
985
986
        // Process all non-identifier fields by translating field names
987 913
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
988 257
            $mapping   = $class->fieldMappings[$fieldName];
989 257
            $fieldName = $mapping['name'];
990
991 257
            if (! $prepareValue) {
992 52
                return [[$fieldName, $value]];
993
            }
994
995
            // Prepare mapped, embedded objects
996 215
            if (! empty($mapping['embedded']) && is_object($value) &&
997 215
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
998 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
999
            }
1000
1001 213
            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...
1002
                try {
1003 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1004 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...
1005
                    // do nothing in case passed object is not mapped document
1006
                }
1007
            }
1008
1009
            // No further preparation unless we're dealing with a simple reference
1010
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1011 200
            $arrayValue = (array) $value;
1012 200
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1013 133
                return [[$fieldName, $value]];
1014
            }
1015
1016
            // Additional preparation for one or more simple reference values
1017 94
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1018
1019 94
            if (! is_array($value)) {
1020 90
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1021
            }
1022
1023
            // Objects without operators or with DBRef fields can be converted immediately
1024 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1025 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1026
            }
1027
1028 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1029
        }
1030
1031
        // Process identifier fields
1032 822
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1033 361
            $fieldName = '_id';
1034
1035 361
            if (! $prepareValue) {
1036 42
                return [[$fieldName, $value]];
1037
            }
1038
1039 322
            if (! is_array($value)) {
1040 296
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1041
            }
1042
1043
            // Objects without operators or with DBRef fields can be converted immediately
1044 63
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1045 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1046
            }
1047
1048 58
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1049
        }
1050
1051
        // No processing for unmapped, non-identifier, non-dotted field names
1052 561
        if (strpos($fieldName, '.') === false) {
1053 416
            return [[$fieldName, $value]];
1054
        }
1055
1056
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1057
         *
1058
         * We can limit parsing here, since at most three segments are
1059
         * significant: "fieldName.objectProperty" with an optional index or key
1060
         * for collections stored as either BSON arrays or objects.
1061
         */
1062 157
        $e = explode('.', $fieldName, 4);
1063
1064
        // No further processing for unmapped fields
1065 157
        if (! isset($class->fieldMappings[$e[0]])) {
1066 6
            return [[$fieldName, $value]];
1067
        }
1068
1069 152
        $mapping = $class->fieldMappings[$e[0]];
1070 152
        $e[0]    = $mapping['name'];
1071
1072
        // Hash and raw fields will not be prepared beyond the field name
1073 152
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1074 1
            $fieldName = implode('.', $e);
1075
1076 1
            return [[$fieldName, $value]];
1077
        }
1078
1079 151
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1080 151
                && isset($e[2])) {
1081 1
            $objectProperty       = $e[2];
1082 1
            $objectPropertyPrefix = $e[1] . '.';
1083 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1084 150
        } elseif ($e[1] !== '$') {
1085 149
            $fieldName            = $e[0] . '.' . $e[1];
1086 149
            $objectProperty       = $e[1];
1087 149
            $objectPropertyPrefix = '';
1088 149
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1089 1
        } elseif (isset($e[2])) {
1090 1
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1091 1
            $objectProperty       = $e[2];
1092 1
            $objectPropertyPrefix = $e[1] . '.';
1093 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1094
        } else {
1095 1
            $fieldName = $e[0] . '.' . $e[1];
1096
1097 1
            return [[$fieldName, $value]];
1098
        }
1099
1100
        // No further processing for fields without a targetDocument mapping
1101 151
        if (! isset($mapping['targetDocument'])) {
1102 3
            if ($nextObjectProperty) {
1103
                $fieldName .= '.' . $nextObjectProperty;
1104
            }
1105
1106 3
            return [[$fieldName, $value]];
1107
        }
1108
1109 148
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1110
1111
        // No further processing for unmapped targetDocument fields
1112 148
        if (! $targetClass->hasField($objectProperty)) {
1113 25
            if ($nextObjectProperty) {
1114
                $fieldName .= '.' . $nextObjectProperty;
1115
            }
1116
1117 25
            return [[$fieldName, $value]];
1118
        }
1119
1120 128
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1121 128
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1122
1123
        // Prepare DBRef identifiers or the mapped field's property path
1124 128
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1125 108
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1126 128
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1127
1128
        // Process targetDocument identifier fields
1129 128
        if ($objectPropertyIsId) {
1130 109
            if (! $prepareValue) {
1131 7
                return [[$fieldName, $value]];
1132
            }
1133
1134 102
            if (! is_array($value)) {
1135 88
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1136
            }
1137
1138
            // Objects without operators or with DBRef fields can be converted immediately
1139 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1140 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1141
            }
1142
1143 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1144
        }
1145
1146
        /* The property path may include a third field segment, excluding the
1147
         * collection item pointer. If present, this next object property must
1148
         * be processed recursively.
1149
         */
1150 19
        if ($nextObjectProperty) {
1151
            // Respect the targetDocument's class metadata when recursing
1152 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1153 10
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1154 16
                : null;
1155
1156 16
            if (empty($targetMapping['reference'])) {
1157 14
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1158
            } else {
1159
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1160 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1161 1
                    $nextObjectProperty = '$' . $nextObjectProperty;
1162
                }
1163 4
                $fieldNames = [[$nextObjectProperty, $value]];
1164
            }
1165
1166
            return array_map(static function ($preparedTuple) use ($fieldName) {
1167 16
                [$key, $value] = $preparedTuple;
0 ignored issues
show
Bug introduced by
The variable $key does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1168
1169 16
                return [$fieldName . '.' . $key, $value];
1170 16
            }, $fieldNames);
1171
        }
1172
1173 5
        return [[$fieldName, $value]];
1174
    }
1175
1176 80
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1177
    {
1178 80
        foreach ($expression as $k => $v) {
1179
            // Ignore query operators whose arguments need no type conversion
1180 80
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1181 16
                continue;
1182
            }
1183
1184
            // Process query operators whose argument arrays need type conversion
1185 80
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1186 78
                foreach ($v as $k2 => $v2) {
1187 78
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1188
                }
1189 78
                continue;
1190
            }
1191
1192
            // Recursively process expressions within a $not operator
1193 18
            if ($k === '$not' && is_array($v)) {
1194 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1195 15
                continue;
1196
            }
1197
1198 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1199
        }
1200
1201 80
        return $expression;
1202
    }
1203
1204
    /**
1205
     * Checks whether the value has DBRef fields.
1206
     *
1207
     * This method doesn't check if the the value is a complete DBRef object,
1208
     * although it should return true for a DBRef. Rather, we're checking that
1209
     * the value has one or more fields for a DBref. In practice, this could be
1210
     * $elemMatch criteria for matching a DBRef.
1211
     *
1212
     * @param mixed $value
1213
     */
1214 81
    private function hasDBRefFields($value) : bool
1215
    {
1216 81
        if (! is_array($value) && ! is_object($value)) {
1217
            return false;
1218
        }
1219
1220 81
        if (is_object($value)) {
1221
            $value = get_object_vars($value);
1222
        }
1223
1224 81
        foreach ($value as $key => $_) {
1225 81
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1226 81
                return true;
1227
            }
1228
        }
1229
1230 80
        return false;
1231
    }
1232
1233
    /**
1234
     * Checks whether the value has query operators.
1235
     *
1236
     * @param mixed $value
1237
     */
1238 85
    private function hasQueryOperators($value) : bool
1239
    {
1240 85
        if (! is_array($value) && ! is_object($value)) {
1241
            return false;
1242
        }
1243
1244 85
        if (is_object($value)) {
1245
            $value = get_object_vars($value);
1246
        }
1247
1248 85
        foreach ($value as $key => $_) {
1249 85
            if (isset($key[0]) && $key[0] === '$') {
1250 85
                return true;
1251
            }
1252
        }
1253
1254 11
        return false;
1255
    }
1256
1257
    /**
1258
     * Gets the array of discriminator values for the given ClassMetadata
1259
     */
1260 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1261
    {
1262 27
        $discriminatorValues = [$metadata->discriminatorValue];
1263 27
        foreach ($metadata->subClasses as $className) {
1264 8
            $key = array_search($className, $metadata->discriminatorMap);
1265 8
            if (! $key) {
1266
                continue;
1267
            }
1268
1269 8
            $discriminatorValues[] = $key;
1270
        }
1271
1272
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1273 27
        if ($metadata->defaultDiscriminatorValue && in_array($metadata->defaultDiscriminatorValue, $discriminatorValues)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->defaultDiscriminatorValue of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1274 2
            $discriminatorValues[] = null;
1275
        }
1276
1277 27
        return $discriminatorValues;
1278
    }
1279
1280 584
    private function handleCollections(object $document, array $options) : void
1281
    {
1282
        // Collection deletions (deletions of complete collections)
1283 584
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1284 107
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1285 96
                continue;
1286
            }
1287
1288 31
            $this->cp->delete($coll, $options);
1289
        }
1290
        // Collection updates (deleteRows, updateRows, insertRows)
1291 584
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1292 107
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1293 29
                continue;
1294
            }
1295
1296 99
            $this->cp->update($coll, $options);
1297
        }
1298
        // Take new snapshots from visited collections
1299 584
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1300 250
            $coll->takeSnapshot();
1301
        }
1302 584
    }
1303
1304
    /**
1305
     * If the document is new, ignore shard key field value, otherwise throw an
1306
     * exception. Also, shard key field should be present in actual document
1307
     * data.
1308
     *
1309
     * @throws MongoDBException
1310
     */
1311 10
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1312
    {
1313 10
        $dcs      = $this->uow->getDocumentChangeSet($document);
1314 10
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1315
1316 10
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1317 10
        $fieldName    = $fieldMapping['fieldName'];
1318
1319 10
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1320 2
            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...
1321
        }
1322
1323 8
        if (! isset($actualDocumentData[$fieldName])) {
1324
            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...
1325
        }
1326 8
    }
1327
1328
    /**
1329
     * Get shard key aware query for single document.
1330
     */
1331 297
    private function getQueryForDocument(object $document) : array
1332
    {
1333 297
        $id = $this->uow->getDocumentIdentifier($document);
1334 297
        $id = $this->class->getDatabaseIdentifierValue($id);
1335
1336 297
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1337 295
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1338
    }
1339
1340 585
    private function getWriteOptions(array $options = []) : array
1341
    {
1342 585
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1343 585
        $documentOptions = [];
1344 585
        if ($this->class->hasWriteConcern()) {
1345 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1346
        }
1347
1348 585
        return array_merge($defaultOptions, $documentOptions, $options);
1349
    }
1350
1351 15
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1352
    {
1353 15
        $reference = $this->dm->createReference($value, $mapping);
1354 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1355 8
            return [[$fieldName, $reference]];
1356
        }
1357
1358 6
        switch ($mapping['storeAs']) {
1359
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1360
                $keys = ['id' => true];
1361
                break;
1362
1363
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1364
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1365 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1366
1367 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1368 5
                    unset($keys['$db']);
1369
                }
1370
1371 6
                if (isset($mapping['targetDocument'])) {
1372 4
                    unset($keys['$ref'], $keys['$db']);
1373
                }
1374 6
                break;
1375
1376
            default:
1377
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1378
        }
1379
1380 6
        if ($mapping['type'] === 'many') {
1381 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1382
        }
1383
1384 4
        return array_map(
1385
            static function ($key) use ($reference, $fieldName) {
1386 4
                return [$fieldName . '.' . $key, $reference[$key]];
1387 4
            },
1388 4
            array_keys($keys)
1389
        );
1390
    }
1391
}
1392