Completed
Push — master ( 22bf09...6ecd27 )
by Andreas
15s queued 10s
created

DocumentPersister::update()   D

Complexity

Conditions 16
Paths 200

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 16

Importance

Changes 0
Metric Value
dl 0
loc 61
ccs 35
cts 35
cp 1
rs 4.7333
c 0
b 0
f 0
cc 16
nc 200
nop 2
crap 16

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
                }
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 601
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
1006
    {
1007 601
        $preparedQuery = [];
1008
1009 601
        foreach ($query as $key => $value) {
1010
            // Recursively prepare logical query clauses
1011 557
            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 557
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1019 74
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1020 74
                continue;
1021
            }
1022
1023 557
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1024 557
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1025 557
                $preparedQuery[$preparedKey] = is_array($preparedValue)
0 ignored issues
show
Bug introduced by
The variable $preparedKey does not exist. Did you forget to declare it?

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

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

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

Loading history...
1026 153
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1027 506
                    : Type::convertPHPToDatabaseValue($preparedValue);
1028
            }
1029
        }
1030
1031 601
        return $preparedQuery;
1032
    }
1033
1034
    /**
1035
     * Prepares a query value and converts the PHP value to the database value
1036
     * if it is an identifier.
1037
     *
1038
     * It also handles converting $fieldName to the database name if they are
1039
     * different.
1040
     *
1041
     * @param mixed $value
1042
     */
1043 979
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1044
    {
1045 979
        $class = $class ?? $this->class;
1046
1047
        // @todo Consider inlining calls to ClassMetadata methods
1048
1049
        // Process all non-identifier fields by translating field names
1050 979
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1051 281
            $mapping   = $class->fieldMappings[$fieldName];
1052 281
            $fieldName = $mapping['name'];
1053
1054 281
            if (! $prepareValue) {
1055 77
                return [[$fieldName, $value]];
1056
            }
1057
1058
            // Prepare mapped, embedded objects
1059 214
            if (! empty($mapping['embedded']) && is_object($value) &&
1060 214
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1061 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1062
            }
1063
1064 212
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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