Completed
Pull Request — master (#2128)
by Maciej
20:42 queued 08:45
created

DocumentPersister::getShardKeyQuery()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1008 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1009
        }
1010
1011 526
        return $preparedQuery;
1012
    }
1013
1014
    /**
1015
     * Prepares the query criteria or new document object.
1016
     *
1017
     * PHP field names and types will be converted to those used by MongoDB.
1018
     */
1019 601
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
1020
    {
1021 601
        $preparedQuery = [];
1022
1023 601
        foreach ($query as $key => $value) {
1024
            // Recursively prepare logical query clauses
1025 557
            if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) {
1026 20
                foreach ($value as $k2 => $v2) {
1027 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1028
                }
1029 20
                continue;
1030
            }
1031
1032 557
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1033 74
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1034 74
                continue;
1035
            }
1036
1037 557
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1038 557
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1039 557
                $preparedQuery[$preparedKey] = is_array($preparedValue)
0 ignored issues
show
Bug introduced by
The variable $preparedKey does not exist. Did you forget to declare it?

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

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

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

Loading history...
1040 153
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1041 506
                    : Type::convertPHPToDatabaseValue($preparedValue);
1042
            }
1043
        }
1044
1045 601
        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 979
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1058
    {
1059 979
        $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 979
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1065 281
            $mapping   = $class->fieldMappings[$fieldName];
1066 281
            $fieldName = $mapping['name'];
1067
1068 281
            if (! $prepareValue) {
1069 77
                return [[$fieldName, $value]];
1070
            }
1071
1072
            // Prepare mapped, embedded objects
1073 214
            if (! empty($mapping['embedded']) && is_object($value) &&
1074 214
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1075 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1076
            }
1077
1078 212
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1079
                try {
1080 14
                    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 869
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1108 359
            $fieldName = '_id';
1109
1110 359
            if (! $prepareValue) {
1111 42
                return [[$fieldName, $value]];
1112
            }
1113
1114 320
            if (! is_array($value)) {
1115 294
                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 602
    private function handleCollections(object $document, array $options) : void
1361
    {
1362
        // Collection deletions (deletions of complete collections)
1363 602
        $collections = [];
1364 602
        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 602
        if (! empty($collections)) {
1372 33
            $this->cp->delete($document, $collections, $options);
1373
        }
1374
        // Collection updates (deleteRows, updateRows, insertRows)
1375 602
        $collections = [];
1376 602
        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 602
        if (! empty($collections)) {
1384 105
            $this->cp->update($document, $collections, $options);
1385
        }
1386
        // Take new snapshots from visited collections
1387 602
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1388 252
            $coll->takeSnapshot();
1389
        }
1390 602
    }
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 316
    private function getQueryForDocument(object $document) : array
1420
    {
1421 316
        $id = $this->uow->getDocumentIdentifier($document);
1422 316
        $id = $this->class->getDatabaseIdentifierValue($id);
1423
1424 316
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1425
1426 314
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1427
    }
1428
1429 613
    private function getWriteOptions(array $options = []) : array
1430
    {
1431 613
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1432 613
        $documentOptions = [];
1433 613
        if ($this->class->hasWriteConcern()) {
1434 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1435
        }
1436
1437 613
        return array_merge($defaultOptions, $documentOptions, $options);
1438
    }
1439
1440 15
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1441
    {
1442 15
        $reference = $this->dm->createReference($value, $mapping);
1443 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1444 8
            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