Completed
Push — master ( 4655ef...ecb96f )
by Andreas
20s queued 11s
created

DocumentPersister::convertToDatabaseValue()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7.0796

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 15
cts 17
cp 0.8824
rs 8.4906
c 0
b 0
f 0
cc 7
nc 6
nop 2
crap 7.0796
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\ODM\MongoDB\DocumentManager;
11
use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
14
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
15
use Doctrine\ODM\MongoDB\Iterator\Iterator;
16
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
17
use Doctrine\ODM\MongoDB\LockException;
18
use Doctrine\ODM\MongoDB\LockMode;
19
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
20
use Doctrine\ODM\MongoDB\MongoDBException;
21
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
22
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
23
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
24
use Doctrine\ODM\MongoDB\Query\Query;
25
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
26
use Doctrine\ODM\MongoDB\Types\Type;
27
use Doctrine\ODM\MongoDB\UnitOfWork;
28
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
29
use Doctrine\Persistence\Mapping\MappingException;
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 1216
    public function __construct(
115
        PersistenceBuilder $pb,
116
        DocumentManager $dm,
117
        UnitOfWork $uow,
118
        HydratorFactory $hydratorFactory,
119
        ClassMetadata $class,
120
        ?CriteriaMerger $cm = null
121
    ) {
122 1216
        $this->pb              = $pb;
123 1216
        $this->dm              = $dm;
124 1216
        $this->cm              = $cm ?: new CriteriaMerger();
125 1216
        $this->uow             = $uow;
126 1216
        $this->hydratorFactory = $hydratorFactory;
127 1216
        $this->class           = $class;
128 1216
        $this->cp              = $this->uow->getCollectionPersister();
129
130 1216
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
131 95
            return;
132
        }
133
134 1213
        $this->collection = $dm->getDocumentCollection($class->name);
135
136 1213
        if (! $class->isFile) {
137 1200
            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 607
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
1006
    {
1007 607
        $preparedQuery = [];
1008
1009 607
        foreach ($query as $key => $value) {
1010
            // Recursively prepare logical query clauses
1011 563
            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 563
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1019 74
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1020 74
                continue;
1021
            }
1022
1023 563
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1024 563
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1025 563
                $preparedValue = Type::convertPHPToDatabaseValue($preparedValue);
0 ignored issues
show
Bug introduced by
The variable $preparedValue does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1026 563
                if ($this->class->hasField($key)) {
1027 245
                    $preparedValue = $this->convertToDatabaseValue($key, $preparedValue);
1028
                }
1029 563
                $preparedQuery[$preparedKey] = $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...
1030
            }
1031
        }
1032
1033 607
        return $preparedQuery;
1034
    }
1035
1036
    /**
1037
     * Converts a single value to its database representation based on the mapping type
1038
     *
1039
     * @param mixed $value
1040
     *
1041
     * @return mixed
1042
     */
1043 245
    private function convertToDatabaseValue(string $fieldName, $value)
1044
    {
1045 245
        $mapping  = $this->class->fieldMappings[$fieldName];
1046 245
        $typeName = $mapping['type'];
1047
1048 245
        if (is_array($value)) {
1049 60
            foreach ($value as $k => $v) {
1050 59
                $value[$k] = $this->convertToDatabaseValue($fieldName, $v);
1051
            }
1052
1053 60
            return $value;
1054
        }
1055
1056 245
        if (! empty($mapping['reference']) || ! empty($mapping['embedded'])) {
1057 127
            return $value;
1058
        }
1059
1060 165
        if (! Type::hasType($typeName)) {
1061
            throw new InvalidArgumentException(
1062
                sprintf('Mapping type "%s" does not exist', $typeName)
1063
            );
1064
        }
1065 165
        if (in_array($typeName, ['collection', 'hash'])) {
1066 7
            return $value;
1067
        }
1068
1069 160
        $type  = Type::getType($typeName);
1070 160
        $value = $type->convertToDatabaseValue($value);
1071
1072 160
        return $value;
1073
    }
1074
1075
    /**
1076
     * Prepares a query value and converts the PHP value to the database value
1077
     * if it is an identifier.
1078
     *
1079
     * It also handles converting $fieldName to the database name if they are
1080
     * different.
1081
     *
1082
     * @param mixed $value
1083
     */
1084 985
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1085
    {
1086 985
        $class = $class ?? $this->class;
1087
1088
        // @todo Consider inlining calls to ClassMetadata methods
1089
1090
        // Process all non-identifier fields by translating field names
1091 985
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1092 284
            $mapping   = $class->fieldMappings[$fieldName];
1093 284
            $fieldName = $mapping['name'];
1094
1095 284
            if (! $prepareValue) {
1096 77
                return [[$fieldName, $value]];
1097
            }
1098
1099
            // Prepare mapped, embedded objects
1100 217
            if (! empty($mapping['embedded']) && is_object($value) &&
1101 217
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1102 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1103
            }
1104
1105 215
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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