Completed
Pull Request — master (#2104)
by
unknown
20:15
created

DocumentPersister::handleCollections()   B

Complexity

Conditions 8
Paths 72

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8

Importance

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