Completed
Pull Request — master (#2073)
by Andreas
29:11 queued 06:42
created

DocumentPersister::loadEmbedManyCollection()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

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