Completed
Pull Request — master (#2076)
by Olivier
01:54
created

DocumentPersister::refresh()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

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