Completed
Pull Request — master (#2128)
by Maciej
24:11
created

DocumentPersister::update()   F

Complexity

Conditions 17
Paths 240

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 17

Importance

Changes 0
Metric Value
dl 0
loc 67
ccs 35
cts 35
cp 1
rs 3.8833
c 0
b 0
f 0
cc 17
nc 240
nop 2
crap 17

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use BadMethodCallException;
8
use DateTime;
9
use 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 1210
    public function __construct(
115
        PersistenceBuilder $pb,
116
        DocumentManager $dm,
117
        UnitOfWork $uow,
118
        HydratorFactory $hydratorFactory,
119
        ClassMetadata $class,
120
        ?CriteriaMerger $cm = null
121
    ) {
122 1210
        $this->pb              = $pb;
123 1210
        $this->dm              = $dm;
124 1210
        $this->cm              = $cm ?: new CriteriaMerger();
125 1210
        $this->uow             = $uow;
126 1210
        $this->hydratorFactory = $hydratorFactory;
127 1210
        $this->class           = $class;
128 1210
        $this->cp              = $this->uow->getCollectionPersister();
129
130 1210
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
131 95
            return;
132
        }
133
134 1207
        $this->collection = $dm->getDocumentCollection($class->name);
135
136 1207
        if (! $class->isFile) {
137 1194
            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
                } elseif ($versionMapping['type'] === Type::DECIMAL128) {
223 42
                    $current = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
224
                    $nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
225
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
226 525
                }
227
                $data[$versionMapping['name']] = $nextVersion;
228
            }
229 525
230
            $inserts[] = $data;
231 525
        }
232 525
233 6
        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 6
            try {
235 6
                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
                $this->collection->insertMany($inserts, $options);
237
            } 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
                $this->queuedInserts = [];
239
                throw $e;
240
            }
241
        }
242
243 525
        /* All collections except for ones using addToSet have already been
244 525
         * saved. We have left these to be handled separately to avoid checking
245
         * collection for uniqueness on PHP side.
246
         */
247 525
        foreach ($this->queuedInserts as $document) {
248 525
            $this->handleCollections($document, $options);
249
        }
250
251
        $this->queuedInserts = [];
252
    }
253
254
    /**
255
     * Executes all queued document upserts.
256
     *
257 86
     * Queued documents with an ID are upserted individually.
258
     *
259 86
     * If no upserts are queued, invoking this method is a NOOP.
260
     */
261
    public function executeUpserts(array $options = []) : void
262
    {
263 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...
264 86
            return;
265
        }
266 86
267 86
        $options = $this->getWriteOptions($options);
268 86
        foreach ($this->queuedUpserts as $oid => $document) {
269
            try {
270
                $this->executeUpsert($document, $options);
271
                $this->handleCollections($document, $options);
272
                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 86
                unset($this->queuedUpserts[$oid]);
275
                throw $e;
276
            }
277
        }
278
    }
279 86
280
    /**
281 86
     * Executes a single upsert in {@link executeUpserts}
282 86
     */
283
    private function executeUpsert(object $document, array $options) : void
284 86
    {
285
        $options['upsert'] = true;
286
        $criteria          = $this->getQueryForDocument($document);
287 86
288 4
        $data = $this->pb->prepareUpsertData($document);
289 4
290 4
        // Set the initial version for each upsert
291 2
        if ($this->class->isVersioned) {
292 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
293 2
            $nextVersion    = null;
294 2
            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
            } elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
298 4
                $nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
299
                $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
300
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
301 86
            } elseif ($versionMapping['type'] === Type::DECIMAL128) {
302 86
                $current = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
303 86
                $nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
304 86
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
305
            }
306
            $data['$set'][$versionMapping['name']] = $nextVersion;
307
        }
308 86
309 86
        foreach (array_keys($criteria) as $field) {
310 71
            unset($data['$set'][$field]);
311
            unset($data['$inc'][$field]);
312
            unset($data['$setOnInsert'][$field]);
313 86
        }
314
315
        // Do not send empty update operators
316
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
317
            if (! empty($data[$operator])) {
318
                continue;
319
            }
320
321
            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 86
         *
330 16
         * This will fail for versions before MongoDB 2.6, which require an
331 16
         * 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 86
         * See: https://jira.mongodb.org/browse/SERVER-12266
336 86
         */
337
        if (empty($data)) {
338 86
            $retry = true;
339
            $data  = ['$set' => ['_id' => $criteria['_id']]];
340
        }
341
342
        try {
343
            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
            $this->collection->updateOne($criteria, $data, $options);
345
346
            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 237
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
355
    }
356 237
357
    /**
358 237
     * Updates the already persisted document if it has any new changesets.
359
     *
360 235
     * @throws LockException
361 235
     */
362
    public function update(object $document, array $options = []) : void
363
    {
364 235
        $update = $this->pb->prepareUpdateData($document);
365 101
366
        $query = $this->getQueryForDocument($document);
367
368
        foreach (array_keys($query) as $field) {
369
            unset($update['$set'][$field]);
370
        }
371 235
372 235
        if (empty($update['$set'])) {
373 36
            unset($update['$set']);
374 36
        }
375 36
376 30
        // Include versioning logic to set the new version value in the database
377 30
        // and to ensure the version has not changed since this document object instance
378 30
        // was fetched from the database
379 6
        $nextVersion = null;
380 6
        if ($this->class->isVersioned) {
381 6
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
382 6
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
383
            if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
384
                $nextVersion                             = $currentVersion + 1;
385
                $update['$inc'][$versionMapping['name']] = 1;
386 235
                $query[$versionMapping['name']]          = $currentVersion;
387
            } elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
388
                $nextVersion                             = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
389 161
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
390 14
                $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
391 14
            } elseif ($versionMapping['type'] === Type::DECIMAL128) {
392 14
                $current = $this->class->reflFields[$this->class->versionField]->getValue($document);
393 2
                $nextVersion = bcadd($current, '1');
394
                $type = Type::getType(Type::DECIMAL128);
395 12
                $update['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
396
                $query[$versionMapping['name']]          = $type->convertPHPToDatabaseValue($currentVersion);
397
            }
398
        }
399 161
400
        if (! empty($update)) {
401 161
            // Include locking logic so that if the document object in memory is currently
402 161
            // locked then it will remove it, otherwise it ensures the document is not locked.
403
            if ($this->class->isLockable) {
404 161
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
405 7
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
406
                if ($isLocked) {
407
                    $update['$unset'] = [$lockMapping['name'] => true];
408 155
                } else {
409 30
                    $query[$lockMapping['name']] = ['$exists' => false];
410
                }
411
            }
412
413 229
            $options = $this->getWriteOptions($options);
414 229
415
            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
            $result = $this->collection->updateOne($query, $update, $options);
417
418
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
419
                throw LockException::lockFailed($document);
420
            }
421 36
422
            if ($this->class->isVersioned) {
423 36
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
424 1
            }
425 1
        }
426
427 1
        $this->handleCollections($document, $options);
428
    }
429 1
430
    /**
431
     * Removes document from mongo
432 35
     *
433
     * @throws LockException
434 35
     */
435 2
    public function delete(object $document, array $options = []) : void
436
    {
437
        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 35
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
439
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
440 35
441 35
            $this->bucket->delete($databaseIdentifier);
442
443 35
            return;
444 2
        }
445
446 33
        $query = $this->getQueryForDocument($document);
447
448
        if ($this->class->isLockable) {
449
            $query[$this->class->lockField] = ['$exists' => false];
450
        }
451 23
452
        $options = $this->getWriteOptions($options);
453 23
454 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...
455 23
        $result = $this->collection->deleteOne($query, $options);
456 23
457
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
458
            throw LockException::lockFailed($document);
459 23
        }
460 23
    }
461 23
462
    /**
463
     * Refreshes a managed document.
464
     */
465
    public function refresh(object $document) : void
466
    {
467
        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
        $query = $this->getQueryForDocument($document);
469
        $data  = $this->collection->findOne($query);
470
        if ($data === null) {
471
            throw MongoDBException::cannotRefreshDocument();
472
        }
473
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
474
        $this->uow->setOriginalDocumentData($document, $data);
475
    }
476 368
477
    /**
478
     * Finds a document by a set of criteria.
479 368
     *
480
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
481
     * be used to match an _id value.
482
     *
483 368
     * @param mixed $criteria Query criteria
484 368
     *
485 368
     * @throws LockException
486
     *
487 368
     * @todo Check identity map? loadById method? Try to guess whether
488 368
     *     $criteria is the id?
489 95
     */
490
    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 368
    {
492 368
        // 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 368
        }
496 1
497 1
        $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 1
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
499
        $criteria = $this->addFilterToPreparedQuery($criteria);
500
501
        $options = [];
502 367
        if ($sort !== null) {
503 115
            $options['sort'] = $this->prepareSort($sort);
504
        }
505
        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 323
        $result = $this->collection->findOne($criteria, $options);
507
        $result = $result !== null ? (array) $result : null;
508
509
        if ($this->class->isLockable) {
510
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
511
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
512 24
                throw LockException::lockFailed($document);
513
            }
514 24
        }
515 24
516 24
        if ($result === null) {
517
            return null;
518 24
        }
519 24
520 11
        return $this->createDocument($result, $document, $hints);
521
    }
522
523 24
    /**
524 10
     * Finds documents by a set of criteria.
525
     */
526
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
527 24
    {
528 1
        $criteria = $this->prepareQueryOrNewObj($criteria);
529
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
530
        $criteria = $this->addFilterToPreparedQuery($criteria);
531 24
532 24
        $options = [];
533
        if ($sort !== null) {
534 24
            $options['sort'] = $this->prepareSort($sort);
535
        }
536
537
        if ($limit !== null) {
538
            $options['limit'] = $limit;
539
        }
540 317
541
        if ($skip !== null) {
542 317
            $options['skip'] = $skip;
543 307
        }
544
545
        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 10
        $baseCursor = $this->collection->find($criteria, $options);
547 10
548 10
        return $this->wrapCursor($baseCursor);
549
    }
550 10
551 10
    /**
552 10
     * @throws MongoDBException
553 10
     */
554 10
    private function getShardKeyQuery(object $document) : array
555
    {
556 8
        if (! $this->class->isSharded()) {
557 1
            return [];
558 1
        }
559 1
560 1
        $shardKey = $this->class->getShardKey();
561 1
        $keys     = array_keys($shardKey['keys']);
562
        $data     = $this->uow->getDocumentActualData($document);
563 1
564 1
        $shardKeyQueryPart = [];
565
        foreach ($keys as $key) {
566
            assert(is_string($key));
567 7
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
568 7
            $this->guardMissingShardKey($document, $key, $data);
569
570
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
571
                $reference = $this->prepareReference(
572 8
                    $key,
573
                    $data[$mapping['fieldName']],
574
                    $mapping,
575
                    false
576
                );
577
                foreach ($reference as $keyValue) {
578 24
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
579
                }
580 24
            } else {
581
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
582
                $shardKeyQueryPart[$key] = $value;
583
            }
584
        }
585
586 3
        return $shardKeyQueryPart;
587
    }
588 3
589 3
    /**
590
     * Wraps the supplied base cursor in the corresponding ODM class.
591 3
     */
592
    private function wrapCursor(Cursor $baseCursor) : Iterator
593
    {
594
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
595
    }
596
597 5
    /**
598
     * Checks whether the given managed document exists in the database.
599 5
     */
600 5
    public function exists(object $document) : bool
601 5
    {
602 5
        $id = $this->class->getIdentifierObject($document);
603 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...
604 5
605 5
        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 1
     */
611
    public function lock(object $document, int $lockMode) : void
612 1
    {
613 1
        $id          = $this->uow->getDocumentIdentifier($document);
614 1
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
615 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
616 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...
617 1
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
618 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
619
    }
620
621
    /**
622
     * Releases any lock that exists on this document.
623
     */
624
    public function unlock(object $document) : void
625
    {
626
        $id          = $this->uow->getDocumentIdentifier($document);
627
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
628
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
629 323
        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
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
631 323
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
632 28
    }
633 28
634 28
    /**
635
     * Creates or fills a single document object from an query result.
636
     *
637 323
     * @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 180
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
644
    {
645 180
        if ($document !== null) {
646 180
            $hints[Query::HINT_REFRESH] = true;
647
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
648 127
            $this->uow->registerManaged($document, $id, $result);
649 126
        }
650
651
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
652 76
    }
653 5
654
    /**
655 72
     * Loads a PersistentCollection data. Used in the initialize() method.
656 60
     */
657
    public function loadCollection(PersistentCollectionInterface $collection) : void
658 17
    {
659
        $mapping = $collection->getMapping();
660
        switch ($mapping['association']) {
661 75
            case ClassMetadata::EMBED_MANY:
662
                $this->loadEmbedManyCollection($collection);
663 178
                break;
664
665 127
            case ClassMetadata::REFERENCE_MANY:
666
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
667 127
                    $this->loadReferenceManyWithRepositoryMethod($collection);
668 127
                } else {
669 127
                    if ($mapping['isOwningSide']) {
670
                        $this->loadReferenceManyCollectionOwningSide($collection);
671 127
                    } else {
672 75
                        $this->loadReferenceManyCollectionInverseSide($collection);
673
                    }
674
                }
675 98
                break;
676
        }
677
    }
678
679 98
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
680 98
    {
681 98
        $embeddedDocuments = $collection->getMongoData();
682 98
        $mapping           = $collection->getMapping();
683
        $owner             = $collection->getOwner();
684 98
685 1
        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
            return;
687
        }
688 97
689
        if ($owner === null) {
690 97
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
691 97
        }
692
693 97
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
694 96
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
695
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
696 97
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
697 25
698
            if (! is_array($embeddedDocument)) {
699 80
                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 60
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
705
            $id   = $data[$embeddedMetadata->identifier] ?? null;
706 60
707 60
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
708 60
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
709 60
            }
710
            if (CollectionHelper::isHash($mapping['strategy'])) {
711 60
                $collection->set($key, $embeddedDocumentObject);
712
            } else {
713
                $collection->add($embeddedDocumentObject);
714
            }
715 60
        }
716
    }
717 60
718 54
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
719
    {
720 54
        $hints      = $collection->getHints();
721 1
        $mapping    = $collection->getMapping();
722
        $owner      = $collection->getOwner();
723
        $groupedIds = [];
724 53
725 53
        if ($owner === null) {
726
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
727
        }
728 53
729
        $sorted = isset($mapping['sort']) && $mapping['sort'];
730
731 53
        foreach ($collection->getMongoData() as $key => $reference) {
732 52
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
733 2
734
            if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array($reference)) {
735 50
                throw HydratorException::associationItemTypeMismatch(get_class($owner), $mapping['name'], $key, 'array', gettype($reference));
736
            }
737
738
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
739
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
740 53
741 22
            // create a reference to the class and id
742
            $reference = $this->dm->getReference($className, $id);
743
744 38
            // no custom sort so add the references right now in the order they are embedded
745
            if (! $sorted) {
746 59
                if (CollectionHelper::isHash($mapping['strategy'])) {
747 38
                    $collection->set($key, $reference);
748 38
                } else {
749 38
                    $collection->add($reference);
750 38
                }
751 38
            }
752 38
753
            // only query for the referenced object if it is not already initialized or the collection is sorted
754 38
            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
                continue;
756 38
            }
757 38
758 38
            $groupedIds[$className][] = $identifier;
759
        }
760 38
        foreach ($groupedIds as $className => $ids) {
761
            $class           = $this->dm->getClassMetadata($className);
762
            $mongoCollection = $this->dm->getDocumentCollection($className);
763 38
            $criteria        = $this->cm->merge(
764
                ['_id' => ['$in' => array_values($ids)]],
765
                $this->dm->getFilterCollection()->getFilterCriteria($class),
766 38
                $mapping['criteria'] ?? []
767
            );
768
            $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 37
            }
774 37
            if (isset($mapping['limit'])) {
775 37
                $options['limit'] = $mapping['limit'];
776 37
            }
777
            if (isset($mapping['skip'])) {
778
                $options['skip'] = $mapping['skip'];
779 37
            }
780 36
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
781
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
782
            }
783 1
784
            $cursor    = $mongoCollection->find($criteria, $options);
785
            $documents = $cursor->toArray();
786 59
            foreach ($documents as $documentData) {
787
                $document = $this->uow->getById($documentData['_id'], $class);
788 17
                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
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
790 17
                    $this->uow->setOriginalDocumentData($document, $data);
791 17
                }
792 17
793 17
                if (! $sorted) {
794 17
                    continue;
795 16
                }
796
797 17
                $collection->add($document);
798
            }
799 17
        }
800
    }
801 17
802 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
803 17
    {
804
        $query    = $this->createReferenceManyInverseSideQuery($collection);
805 17
        $iterator = $query->execute();
806
        assert($iterator instanceof Iterator);
807
        $documents = $iterator->toArray();
808
        foreach ($documents as $key => $document) {
809 17
            $collection->add($document);
810 17
        }
811 17
    }
812 17
813
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
814 17
    {
815 17
        $hints   = $collection->getHints();
816 17
        $mapping = $collection->getMapping();
817 17
        $owner   = $collection->getOwner();
818
819 17
        if ($owner === null) {
820 17
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
821 17
        }
822
823 17
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
824 17
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
825
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
826 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
827 2
828
        $criteria = $this->cm->merge(
829 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
830
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
831
            $mapping['criteria'] ?? []
832
        );
833 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
834
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
835
            ->setQueryArray($criteria);
836
837 17
        if (isset($mapping['sort'])) {
838 4
            $qb->sort($mapping['sort']);
839
        }
840
        if (isset($mapping['limit'])) {
841 17
            $qb->limit($mapping['limit']);
842
        }
843
        if (isset($mapping['skip'])) {
844 5
            $qb->skip($mapping['skip']);
845
        }
846 5
847 5
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
848 5
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
849 5
        }
850 5
851 1
        foreach ($mapping['prime'] as $field) {
852
            $qb->field($field)->prime(true);
853 4
        }
854
855
        return $qb->getQuery();
856 5
    }
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
            if (CollectionHelper::isHash($mapping['strategy'])) {
865 5
                $collection->set($key, $obj);
866
            } else {
867
                $collection->add($obj);
868
            }
869 5
        }
870 1
    }
871 1
872 1
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
873
    {
874 1
        $mapping          = $collection->getMapping();
875
        $repositoryMethod = $mapping['repositoryMethod'];
876 1
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
877
            ->$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
        if (! empty($mapping['prime'])) {
884
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
885
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
886 15
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
887
888 15
            assert(is_array($primers));
889
890 15
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
891 15
        }
892
893
        return $cursor;
894 15
    }
895
896
    /**
897
     * Prepare a projection array by converting keys, which are PHP property
898
     * names, to MongoDB field names.
899
     */
900
    public function prepareProjection(array $fields) : array
901
    {
902 26
        $preparedFields = [];
903
904 26
        foreach ($fields as $key => $value) {
905 26
            $preparedFields[$this->prepareFieldName($key)] = $value;
906 15
        }
907 23
908 13
        return $preparedFields;
909
    }
910
911 13
    /**
912
     * @param int|string $sort
913
     *
914
     * @return int|string|null
915
     */
916
    private function getSortDirection($sort)
917
    {
918 142
        switch (strtolower((string) $sort)) {
919
            case 'desc':
920 142
                return -1;
921
            case 'asc':
922 142
                return 1;
923 26
        }
924 1
925
        return $sort;
926 26
    }
927
928
    /**
929
     * Prepare a sort specification array by converting keys to MongoDB field
930 142
     * names and changing direction strings to int.
931
     */
932
    public function prepareSort(array $fields) : array
933
    {
934
        $sortFields = [];
935
936
        foreach ($fields as $key => $value) {
937 462
            if (is_array($value)) {
938
                $sortFields[$this->prepareFieldName($key)] = $value;
939 462
            } else {
940
                $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
941 462
            }
942
        }
943
944
        return $sortFields;
945
    }
946
947
    /**
948
     * Prepare a mongodb field name and convert the PHP property names to
949
     * MongoDB field names.
950
     */
951
    public function prepareFieldName(string $fieldName) : string
952
    {
953
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
954
955
        return $fieldNames[0][0];
956 525
    }
957
958 525
    /**
959 502
     * 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 32
     * possible discriminator values to the query. The list of possible
963
     * discriminator values is based on the discriminatorValue of the class
964 32
     * itself as well as those of all its subclasses.
965 1
     *
966
     * This method should be used once for query criteria and not be used for
967
     * nested expressions. It should be called before
968 32
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
969 21
     */
970
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
971 14
    {
972
        if (isset($preparedQuery[$this->class->discriminatorField]) || $this->class->discriminatorField === null) {
973
            return $preparedQuery;
974 32
        }
975
976
        $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
977
978
        if ($discriminatorValues === []) {
979
            return $preparedQuery;
980
        }
981
982
        if (count($discriminatorValues) === 1) {
983
            $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
984 526
        } else {
985
            $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
986
        }
987
988
        return $preparedQuery;
989
    }
990
991
    /**
992 526
     * Adds filter criteria to an already-prepared query.
993 526
     *
994 18
     * 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 526
     */
998
    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 601
         */
1006
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
1007 601
        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
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1009 601
        }
1010
1011 557
        return $preparedQuery;
1012 20
    }
1013 20
1014
    /**
1015 20
     * Prepares the query criteria or new document object.
1016
     *
1017
     * PHP field names and types will be converted to those used by MongoDB.
1018 557
     */
1019 74
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
1020 74
    {
1021
        $preparedQuery = [];
1022
1023 557
        foreach ($query as $key => $value) {
1024 557
            // Recursively prepare logical query clauses
1025 557
            if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) {
1026 153
                foreach ($value as $k2 => $v2) {
1027 506
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1028
                }
1029
                continue;
1030
            }
1031 601
1032
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1033
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1034
                continue;
1035
            }
1036
1037
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1038
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1039
                $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
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1041
                    : Type::convertPHPToDatabaseValue($preparedValue);
1042
            }
1043 979
        }
1044
1045 979
        return $preparedQuery;
1046
    }
1047
1048
    /**
1049
     * Prepares a query value and converts the PHP value to the database value
1050 979
     * if it is an identifier.
1051 281
     *
1052 281
     * It also handles converting $fieldName to the database name if they are
1053
     * different.
1054 281
     *
1055 77
     * @param mixed $value
1056
     */
1057
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1058
    {
1059 214
        $class = $class ?? $this->class;
1060 214
1061 3
        // @todo Consider inlining calls to ClassMetadata methods
1062
1063
        // Process all non-identifier fields by translating field names
1064 212
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1065
            $mapping   = $class->fieldMappings[$fieldName];
1066 14
            $fieldName = $mapping['name'];
1067 1
1068
            if (! $prepareValue) {
1069
                return [[$fieldName, $value]];
1070
            }
1071
1072
            // Prepare mapped, embedded objects
1073 199
            if (! empty($mapping['embedded']) && is_object($value) &&
1074 132
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1075
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1076
            }
1077
1078 94
            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 94
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1081 90
                } 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 6
1086 3
            // No further preparation unless we're dealing with a simple reference
1087
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) {
1088
                return [[$fieldName, $value]];
1089 6
            }
1090
1091
            // Additional preparation for one or more simple reference values
1092
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1093 869
1094 359
            if (! is_array($value)) {
1095
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1096 359
            }
1097 42
1098
            // Objects without operators or with DBRef fields can be converted immediately
1099
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1100 320
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1101 294
            }
1102
1103
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1104
        }
1105 60
1106 6
        // Process identifier fields
1107
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1108
            $fieldName = '_id';
1109 55
1110
            if (! $prepareValue) {
1111
                return [[$fieldName, $value]];
1112
            }
1113 611
1114 463
            if (! is_array($value)) {
1115
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1116
            }
1117
1118
            // Objects without operators or with DBRef fields can be converted immediately
1119
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1120
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1121
            }
1122
1123 160
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1124
        }
1125
1126 160
        // No processing for unmapped, non-identifier, non-dotted field names
1127 6
        if (strpos($fieldName, '.') === false) {
1128
            return [[$fieldName, $value]];
1129
        }
1130 155
1131 155
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1132
         *
1133
         * We can limit parsing here, since at most three segments are
1134 155
         * significant: "fieldName.objectProperty" with an optional index or key
1135 1
         * for collections stored as either BSON arrays or objects.
1136
         */
1137 1
        $e = explode('.', $fieldName, 4);
1138
1139
        // No further processing for unmapped fields
1140 154
        if (! isset($class->fieldMappings[$e[0]])) {
1141 154
            return [[$fieldName, $value]];
1142 1
        }
1143 1
1144 1
        $mapping = $class->fieldMappings[$e[0]];
1145 153
        $e[0]    = $mapping['name'];
1146 152
1147 152
        // Hash and raw fields will not be prepared beyond the field name
1148 152
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1149 152
            $fieldName = implode('.', $e);
1150 1
1151 1
            return [[$fieldName, $value]];
1152 1
        }
1153 1
1154 1
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1155
                && isset($e[2])) {
1156 1
            $objectProperty       = $e[2];
1157
            $objectPropertyPrefix = $e[1] . '.';
1158 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1159
        } elseif ($e[1] !== '$') {
1160
            $fieldName            = $e[0] . '.' . $e[1];
1161
            $objectProperty       = $e[1];
1162 154
            $objectPropertyPrefix = '';
1163 5
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1164
        } elseif (isset($e[2])) {
1165
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1166
            $objectProperty       = $e[2];
1167 5
            $objectPropertyPrefix = $e[1] . '.';
1168
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1169
        } else {
1170 149
            $fieldName = $e[0] . '.' . $e[1];
1171
1172
            return [[$fieldName, $value]];
1173 149
        }
1174 26
1175
        // No further processing for fields without a targetDocument mapping
1176
        if (! isset($mapping['targetDocument'])) {
1177
            if ($nextObjectProperty) {
1178 26
                $fieldName .= '.' . $nextObjectProperty;
1179
            }
1180
1181 128
            return [[$fieldName, $value]];
1182 128
        }
1183
1184
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1185 128
1186 108
        // No further processing for unmapped targetDocument fields
1187 128
        if (! $targetClass->hasField($objectProperty)) {
1188
            if ($nextObjectProperty) {
1189
                $fieldName .= '.' . $nextObjectProperty;
1190 128
            }
1191 109
1192 7
            return [[$fieldName, $value]];
1193
        }
1194
1195 102
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1196 88
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1197
1198
        // Prepare DBRef identifiers or the mapped field's property path
1199
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1200 16
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1201 6
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1202
1203
        // Process targetDocument identifier fields
1204 16
        if ($objectPropertyIsId) {
1205
            if (! $prepareValue) {
1206
                return [[$fieldName, $value]];
1207
            }
1208
1209
            if (! is_array($value)) {
1210
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1211 19
            }
1212
1213 16
            // Objects without operators or with DBRef fields can be converted immediately
1214 10
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1215 16
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1216
            }
1217 16
1218 14
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1219
        }
1220
1221 4
        /* The property path may include a third field segment, excluding the
1222 1
         * collection item pointer. If present, this next object property must
1223
         * be processed recursively.
1224 4
         */
1225
        if ($nextObjectProperty) {
1226
            // Respect the targetDocument's class metadata when recursing
1227
            $nextTargetClass = isset($targetMapping['targetDocument'])
1228 16
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1229
                : null;
1230 16
1231 16
            if (empty($targetMapping['reference'])) {
1232
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1233
            } else {
1234 5
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1235
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1236
                    $nextObjectProperty = '$' . $nextObjectProperty;
1237 77
                }
1238
                $fieldNames = [[$nextObjectProperty, $value]];
1239 77
            }
1240
1241 77
            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
                return [$fieldName . '.' . $key, $value];
1245
            }, $fieldNames);
1246 77
        }
1247 75
1248 75
        return [[$fieldName, $value]];
1249
    }
1250 75
1251
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1252
    {
1253
        foreach ($expression as $k => $v) {
1254 18
            // Ignore query operators whose arguments need no type conversion
1255 15
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1256 15
                continue;
1257
            }
1258
1259 18
            // Process query operators whose argument arrays need type conversion
1260
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1261
                foreach ($v as $k2 => $v2) {
1262 77
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1263
                }
1264
                continue;
1265
            }
1266
1267
            // Recursively process expressions within a $not operator
1268
            if ($k === '$not' && is_array($v)) {
1269
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1270
                continue;
1271
            }
1272
1273
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1274
        }
1275 78
1276
        return $expression;
1277 78
    }
1278
1279
    /**
1280
     * Checks whether the value has DBRef fields.
1281 78
     *
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 78
     * $elemMatch criteria for matching a DBRef.
1286 78
     *
1287 4
     * @param mixed $value
1288
     */
1289
    private function hasDBRefFields($value) : bool
1290
    {
1291 77
        if (! is_array($value) && ! is_object($value)) {
1292
            return false;
1293
        }
1294
1295
        if (is_object($value)) {
1296
            $value = get_object_vars($value);
1297
        }
1298
1299 82
        foreach ($value as $key => $_) {
1300
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1301 82
                return true;
1302
            }
1303
        }
1304
1305 82
        return false;
1306
    }
1307
1308
    /**
1309 82
     * Checks whether the value has query operators.
1310 82
     *
1311 78
     * @param mixed $value
1312
     */
1313
    private function hasQueryOperators($value) : bool
1314
    {
1315 11
        if (! is_array($value) && ! is_object($value)) {
1316
            return false;
1317
        }
1318
1319
        if (is_object($value)) {
1320
            $value = get_object_vars($value);
1321 32
        }
1322
1323 32
        foreach ($value as $key => $_) {
1324
            if (isset($key[0]) && $key[0] === '$') {
1325 32
                return true;
1326 29
            }
1327
        }
1328
1329 32
        return false;
1330 12
    }
1331 12
1332
    /**
1333
     * Returns the list of discriminator values for the given ClassMetadata
1334
     */
1335 12
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1336
    {
1337
        $discriminatorValues = [];
1338
1339 32
        if ($metadata->discriminatorValue !== null) {
1340 3
            $discriminatorValues[] = $metadata->discriminatorValue;
1341
        }
1342
1343 32
        foreach ($metadata->subClasses as $className) {
1344
            $key = array_search($className, $metadata->discriminatorMap);
1345
            if (! $key) {
1346 599
                continue;
1347
            }
1348
1349 599
            $discriminatorValues[] = $key;
1350 599
        }
1351 113
1352 102
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1353
        if ($metadata->defaultDiscriminatorValue && in_array($metadata->defaultDiscriminatorValue, $discriminatorValues)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->defaultDiscriminatorValue of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1354
            $discriminatorValues[] = null;
1355 33
        }
1356
1357 599
        return $discriminatorValues;
1358 33
    }
1359
1360
    private function handleCollections(object $document, array $options) : void
1361 599
    {
1362 599
        // Collection deletions (deletions of complete collections)
1363 113
        $collections = [];
1364 29
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1365
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1366
                continue;
1367 105
            }
1368
1369 599
            $collections[] = $coll;
1370 105
        }
1371
        if (! empty($collections)) {
1372
            $this->cp->delete($document, $collections, $options);
1373 599
        }
1374 252
        // Collection updates (deleteRows, updateRows, insertRows)
1375
        $collections = [];
1376 599
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1377
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1378
                continue;
1379
            }
1380
1381
            $collections[] = $coll;
1382
        }
1383
        if (! empty($collections)) {
1384
            $this->cp->update($document, $collections, $options);
1385 10
        }
1386
        // Take new snapshots from visited collections
1387 10
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1388 10
            $coll->takeSnapshot();
1389
        }
1390 10
    }
1391 10
1392
    /**
1393 10
     * If the document is new, ignore shard key field value, otherwise throw an
1394 2
     * exception. Also, shard key field should be present in actual document
1395
     * data.
1396
     *
1397 8
     * @throws MongoDBException
1398
     */
1399
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1400 8
    {
1401
        $dcs      = $this->uow->getDocumentChangeSet($document);
1402
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1403
1404
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1405 313
        $fieldName    = $fieldMapping['fieldName'];
1406
1407 313
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1408 313
            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 313
1411
        if (! isset($actualDocumentData[$fieldName])) {
1412 311
            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
    }
1415 610
1416
    /**
1417 610
     * Get shard key aware query for single document.
1418 610
     */
1419 610
    private function getQueryForDocument(object $document) : array
1420 9
    {
1421
        $id = $this->uow->getDocumentIdentifier($document);
1422
        $id = $this->class->getDatabaseIdentifierValue($id);
1423 610
1424
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1425
1426 15
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1427
    }
1428 15
1429 14
    private function getWriteOptions(array $options = []) : array
1430 8
    {
1431
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1432
        $documentOptions = [];
1433 6
        if ($this->class->hasWriteConcern()) {
1434
            $documentOptions['w'] = $this->class->getWriteConcern();
1435
        }
1436
1437
        return array_merge($defaultOptions, $documentOptions, $options);
1438
    }
1439
1440 6
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1441
    {
1442 6
        $reference = $this->dm->createReference($value, $mapping);
1443 5
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1444
            return [[$fieldName, $reference]];
1445
        }
1446 6
1447 4
        switch ($mapping['storeAs']) {
1448
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1449 6
                $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
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1455 6
1456 2
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1457
                    unset($keys['$db']);
1458
                }
1459 4
1460
                if (isset($mapping['targetDocument'])) {
1461 4
                    unset($keys['$ref'], $keys['$db']);
1462 4
                }
1463 4
                break;
1464
1465
            default:
1466
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1467
        }
1468
1469
        if ($mapping['type'] === 'many') {
1470
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1471
        }
1472
1473
        return array_map(
1474
            static function ($key) use ($reference, $fieldName) {
1475
                return [$fieldName . '.' . $key, $reference[$key]];
1476
            },
1477
            array_keys($keys)
1478
        );
1479
    }
1480
}
1481