Completed
Pull Request — master (#2104)
by
unknown
37:46 queued 12:45
created

DocumentPersister::prepareQueryOrNewObj()   C

Complexity

Conditions 13
Paths 5

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 13

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 18
cts 18
cp 1
rs 6.6166
c 0
b 0
f 0
cc 13
nc 5
nop 2
crap 13

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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 1206
    public function __construct(
114
        PersistenceBuilder $pb,
115
        DocumentManager $dm,
116
        UnitOfWork $uow,
117
        HydratorFactory $hydratorFactory,
118
        ClassMetadata $class,
119
        ?CriteriaMerger $cm = null
120
    ) {
121 1206
        $this->pb              = $pb;
122 1206
        $this->dm              = $dm;
123 1206
        $this->cm              = $cm ?: new CriteriaMerger();
124 1206
        $this->uow             = $uow;
125 1206
        $this->hydratorFactory = $hydratorFactory;
126 1206
        $this->class           = $class;
127 1206
        $this->cp              = $this->uow->getCollectionPersister();
128
129 1206
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
130 95
            return;
131
        }
132
133 1203
        $this->collection = $dm->getDocumentCollection($class->name);
134
135 1203
        if (! $class->isFile) {
136 1190
            return;
137
        }
138
139 21
        $this->bucket = $dm->getDocumentBucket($class->name);
140 21
    }
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 367
    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 367
        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 367
        $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 367
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
484 367
        $criteria = $this->addFilterToPreparedQuery($criteria);
485
486 367
        $options = [];
487 367
        if ($sort !== null) {
488 95
            $options['sort'] = $this->prepareSort($sort);
489
        }
490 367
        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 367
        $result = $this->collection->findOne($criteria, $options);
492 367
        $result = $result !== null ? (array) $result : null;
493
494 367
        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 366
        if ($result === null) {
502 115
            return null;
503
        }
504
505 322
        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 322
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
629
    {
630 322
        if ($document !== null) {
631 27
            $hints[Query::HINT_REFRESH] = true;
632 27
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
633 27
            $this->uow->registerManaged($document, $id, $result);
634
        }
635
636 322
        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 524
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
956
    {
957 524
        if (isset($preparedQuery[$this->class->discriminatorField]) || $this->class->discriminatorField === null) {
958 501
            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 525
    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 525
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
992 525
        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 525
        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 600
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
1005
    {
1006 600
        $preparedQuery = [];
1007
1008 600
        foreach ($query as $key => $value) {
1009
            // Recursively prepare logical query clauses
1010 556
            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 556
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1018 74
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1019 74
                continue;
1020
            }
1021
1022 556
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1023 556
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1024 556
                if (is_array($preparedValue)) {
1025 153
                    $preparedQuery[$preparedKey] = array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $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 seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1026 505
1027
                    continue;
1028
                }
1029
1030 600
                $preparedValue = Type::convertPHPToDatabaseValue($preparedValue);
1031
1032
                if ($this->class->hasField($key)) {
1033
                    $mapping = $this->class->fieldMappings[$key];
1034
                    $typeName = $mapping['type'];
1035
                    if (Type::hasType($typeName) && !in_array($typeName, ['collection', 'hash'])) {
1036
                        $type = Type::getType($mapping['type']);
1037
                        $preparedValue = $type->convertToDatabaseValue($preparedValue);
1038
                    }
1039
                }
1040
1041
                $preparedQuery[$preparedKey] = $preparedValue;
1042 978
            }
1043
        }
1044 978
1045
        return $preparedQuery;
1046
    }
1047
1048
    /**
1049 978
     * Prepares a query value and converts the PHP value to the database value
1050 281
     * if it is an identifier.
1051 281
     *
1052
     * It also handles converting $fieldName to the database name if they are
1053 281
     * different.
1054 77
     *
1055
     * @param mixed $value
1056
     */
1057
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1058 214
    {
1059 214
        $class = $class ?? $this->class;
1060 3
1061
        // @todo Consider inlining calls to ClassMetadata methods
1062
1063 212
        // Process all non-identifier fields by translating field names
1064
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1065 14
            $mapping   = $class->fieldMappings[$fieldName];
1066 1
            $fieldName = $mapping['name'];
1067
1068
            if (! $prepareValue) {
1069
                return [[$fieldName, $value]];
1070
            }
1071
1072 199
            // Prepare mapped, embedded objects
1073 132
            if (! empty($mapping['embedded']) && is_object($value) &&
1074
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1075
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1076
            }
1077 94
1078
            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...
1079 94
                try {
1080 90
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1081
                } 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...
1082
                    // do nothing in case passed object is not mapped document
1083
                }
1084 6
            }
1085 3
1086
            // No further preparation unless we're dealing with a simple reference
1087
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) {
1088 6
                return [[$fieldName, $value]];
1089
            }
1090
1091
            // Additional preparation for one or more simple reference values
1092 868
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1093 358
1094
            if (! is_array($value)) {
1095 358
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1096 42
            }
1097
1098
            // Objects without operators or with DBRef fields can be converted immediately
1099 319
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1100 293
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1101
            }
1102
1103
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1104 60
        }
1105 6
1106
        // Process identifier fields
1107
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1108 55
            $fieldName = '_id';
1109
1110
            if (! $prepareValue) {
1111
                return [[$fieldName, $value]];
1112 611
            }
1113 463
1114
            if (! is_array($value)) {
1115
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1116
            }
1117
1118
            // Objects without operators or with DBRef fields can be converted immediately
1119
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1120
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1121
            }
1122 160
1123
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1124
        }
1125 160
1126 6
        // No processing for unmapped, non-identifier, non-dotted field names
1127
        if (strpos($fieldName, '.') === false) {
1128
            return [[$fieldName, $value]];
1129 155
        }
1130 155
1131
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1132
         *
1133 155
         * We can limit parsing here, since at most three segments are
1134 1
         * significant: "fieldName.objectProperty" with an optional index or key
1135
         * for collections stored as either BSON arrays or objects.
1136 1
         */
1137
        $e = explode('.', $fieldName, 4);
1138
1139 154
        // No further processing for unmapped fields
1140 154
        if (! isset($class->fieldMappings[$e[0]])) {
1141 1
            return [[$fieldName, $value]];
1142 1
        }
1143 1
1144 153
        $mapping = $class->fieldMappings[$e[0]];
1145 152
        $e[0]    = $mapping['name'];
1146 152
1147 152
        // Hash and raw fields will not be prepared beyond the field name
1148 152
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1149 1
            $fieldName = implode('.', $e);
1150 1
1151 1
            return [[$fieldName, $value]];
1152 1
        }
1153 1
1154
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1155 1
                && isset($e[2])) {
1156
            $objectProperty       = $e[2];
1157 1
            $objectPropertyPrefix = $e[1] . '.';
1158
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1159
        } elseif ($e[1] !== '$') {
1160
            $fieldName            = $e[0] . '.' . $e[1];
1161 154
            $objectProperty       = $e[1];
1162 5
            $objectPropertyPrefix = '';
1163
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1164
        } elseif (isset($e[2])) {
1165
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1166 5
            $objectProperty       = $e[2];
1167
            $objectPropertyPrefix = $e[1] . '.';
1168
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1169 149
        } else {
1170
            $fieldName = $e[0] . '.' . $e[1];
1171
1172 149
            return [[$fieldName, $value]];
1173 26
        }
1174
1175
        // No further processing for fields without a targetDocument mapping
1176
        if (! isset($mapping['targetDocument'])) {
1177 26
            if ($nextObjectProperty) {
1178
                $fieldName .= '.' . $nextObjectProperty;
1179
            }
1180 128
1181 128
            return [[$fieldName, $value]];
1182
        }
1183
1184 128
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1185 108
1186 128
        // No further processing for unmapped targetDocument fields
1187
        if (! $targetClass->hasField($objectProperty)) {
1188
            if ($nextObjectProperty) {
1189 128
                $fieldName .= '.' . $nextObjectProperty;
1190 109
            }
1191 7
1192
            return [[$fieldName, $value]];
1193
        }
1194 102
1195 88
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1196
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1197
1198
        // Prepare DBRef identifiers or the mapped field's property path
1199 16
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1200 6
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1201
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1202
1203 16
        // Process targetDocument identifier fields
1204
        if ($objectPropertyIsId) {
1205
            if (! $prepareValue) {
1206
                return [[$fieldName, $value]];
1207
            }
1208
1209
            if (! is_array($value)) {
1210 19
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1211
            }
1212 16
1213 10
            // Objects without operators or with DBRef fields can be converted immediately
1214 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1215
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1216 16
            }
1217 14
1218
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1219
        }
1220 4
1221 1
        /* The property path may include a third field segment, excluding the
1222
         * collection item pointer. If present, this next object property must
1223 4
         * be processed recursively.
1224
         */
1225
        if ($nextObjectProperty) {
1226
            // Respect the targetDocument's class metadata when recursing
1227 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1228
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1229 16
                : null;
1230 16
1231
            if (empty($targetMapping['reference'])) {
1232
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1233 5
            } else {
1234
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1235
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1236 77
                    $nextObjectProperty = '$' . $nextObjectProperty;
1237
                }
1238 77
                $fieldNames = [[$nextObjectProperty, $value]];
1239
            }
1240 77
1241 16
            return array_map(static function ($preparedTuple) use ($fieldName) {
1242
                [$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...
1243
1244
                return [$fieldName . '.' . $key, $value];
1245 77
            }, $fieldNames);
1246 75
        }
1247 75
1248
        return [[$fieldName, $value]];
1249 75
    }
1250
1251
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1252
    {
1253 18
        foreach ($expression as $k => $v) {
1254 15
            // Ignore query operators whose arguments need no type conversion
1255 15
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1256
                continue;
1257
            }
1258 18
1259
            // Process query operators whose argument arrays need type conversion
1260
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1261 77
                foreach ($v as $k2 => $v2) {
1262
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1263
                }
1264
                continue;
1265
            }
1266
1267
            // Recursively process expressions within a $not operator
1268
            if ($k === '$not' && is_array($v)) {
1269
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1270
                continue;
1271
            }
1272
1273
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1274 78
        }
1275
1276 78
        return $expression;
1277
    }
1278
1279
    /**
1280 78
     * Checks whether the value has DBRef fields.
1281
     *
1282
     * This method doesn't check if the the value is a complete DBRef object,
1283
     * although it should return true for a DBRef. Rather, we're checking that
1284 78
     * the value has one or more fields for a DBref. In practice, this could be
1285 78
     * $elemMatch criteria for matching a DBRef.
1286 4
     *
1287
     * @param mixed $value
1288
     */
1289
    private function hasDBRefFields($value) : bool
1290 77
    {
1291
        if (! is_array($value) && ! is_object($value)) {
1292
            return false;
1293
        }
1294
1295
        if (is_object($value)) {
1296
            $value = get_object_vars($value);
1297
        }
1298 82
1299
        foreach ($value as $key => $_) {
1300 82
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1301
                return true;
1302
            }
1303
        }
1304 82
1305
        return false;
1306
    }
1307
1308 82
    /**
1309 82
     * Checks whether the value has query operators.
1310 78
     *
1311
     * @param mixed $value
1312
     */
1313
    private function hasQueryOperators($value) : bool
1314 11
    {
1315
        if (! is_array($value) && ! is_object($value)) {
1316
            return false;
1317
        }
1318
1319
        if (is_object($value)) {
1320 32
            $value = get_object_vars($value);
1321
        }
1322 32
1323
        foreach ($value as $key => $_) {
1324 32
            if (isset($key[0]) && $key[0] === '$') {
1325 29
                return true;
1326
            }
1327
        }
1328 32
1329 12
        return false;
1330 12
    }
1331
1332
    /**
1333
     * Returns the list of discriminator values for the given ClassMetadata
1334 12
     */
1335
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1336
    {
1337
        $discriminatorValues = [];
1338 32
1339 3
        if ($metadata->discriminatorValue !== null) {
1340
            $discriminatorValues[] = $metadata->discriminatorValue;
1341
        }
1342 32
1343
        foreach ($metadata->subClasses as $className) {
1344
            $key = array_search($className, $metadata->discriminatorMap);
1345 595
            if (! $key) {
1346
                continue;
1347
            }
1348 595
1349 595
            $discriminatorValues[] = $key;
1350 113
        }
1351 102
1352
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1353
        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...
1354 33
            $discriminatorValues[] = null;
1355
        }
1356 595
1357 33
        return $discriminatorValues;
1358
    }
1359
1360 595
    private function handleCollections(object $document, array $options) : void
1361 595
    {
1362 113
        // Collection deletions (deletions of complete collections)
1363 29
        $collections = [];
1364
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1365
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1366 105
                continue;
1367
            }
1368 595
1369 105
            $collections[] = $coll;
1370
        }
1371
        if (! empty($collections)) {
1372 595
            $this->cp->delete($document, $collections, $options);
1373 252
        }
1374
        // Collection updates (deleteRows, updateRows, insertRows)
1375 595
        $collections = [];
1376
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1377
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1378
                continue;
1379
            }
1380
1381
            $collections[] = $coll;
1382
        }
1383
        if (! empty($collections)) {
1384 10
            $this->cp->update($document, $collections, $options);
1385
        }
1386 10
        // Take new snapshots from visited collections
1387 10
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1388
            $coll->takeSnapshot();
1389 10
        }
1390 10
    }
1391
1392 10
    /**
1393 2
     * If the document is new, ignore shard key field value, otherwise throw an
1394
     * exception. Also, shard key field should be present in actual document
1395
     * data.
1396 8
     *
1397
     * @throws MongoDBException
1398
     */
1399 8
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1400
    {
1401
        $dcs      = $this->uow->getDocumentChangeSet($document);
1402
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1403
1404 310
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1405
        $fieldName    = $fieldMapping['fieldName'];
1406 310
1407 310
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1408
            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...
1409 310
        }
1410
1411 308
        if (! isset($actualDocumentData[$fieldName])) {
1412
            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...
1413
        }
1414 606
    }
1415
1416 606
    /**
1417 606
     * Get shard key aware query for single document.
1418 606
     */
1419 9
    private function getQueryForDocument(object $document) : array
1420
    {
1421
        $id = $this->uow->getDocumentIdentifier($document);
1422 606
        $id = $this->class->getDatabaseIdentifierValue($id);
1423
1424
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1425 15
1426
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1427 15
    }
1428 14
1429 8
    private function getWriteOptions(array $options = []) : array
1430
    {
1431
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1432 6
        $documentOptions = [];
1433
        if ($this->class->hasWriteConcern()) {
1434
            $documentOptions['w'] = $this->class->getWriteConcern();
1435
        }
1436
1437
        return array_merge($defaultOptions, $documentOptions, $options);
1438
    }
1439 6
1440
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1441 6
    {
1442 5
        $reference = $this->dm->createReference($value, $mapping);
1443
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1444
            return [[$fieldName, $reference]];
1445 6
        }
1446 4
1447
        switch ($mapping['storeAs']) {
1448 6
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1449
                $keys = ['id' => true];
1450
                break;
1451
1452
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1453
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1454 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1455 2
1456
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1457
                    unset($keys['$db']);
1458 4
                }
1459
1460 4
                if (isset($mapping['targetDocument'])) {
1461 4
                    unset($keys['$ref'], $keys['$db']);
1462 4
                }
1463
                break;
1464
1465
            default:
1466
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1467
        }
1468
1469
        if ($mapping['type'] === 'many') {
1470
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1471
        }
1472
1473
        return array_map(
1474
            static function ($key) use ($reference, $fieldName) {
1475
                return [$fieldName . '.' . $key, $reference[$key]];
1476
            },
1477
            array_keys($keys)
1478
        );
1479
    }
1480
}
1481