Completed
Pull Request — master (#2104)
by
unknown
19:55
created

loadReferenceManyWithRepositoryMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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