DocumentPersister::prepareQueryElement()   F
last analyzed

Complexity

Conditions 50
Paths 229

Size

Total Lines 193

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 90
CRAP Score 50.0255

Importance

Changes 0
Metric Value
dl 0
loc 193
ccs 90
cts 92
cp 0.9783
rs 2.3766
c 0
b 0
f 0
cc 50
nc 229
nop 5
crap 50.0255

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 Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
10
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
11
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
12
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
13
use Doctrine\ODM\MongoDB\Iterator\Iterator;
14
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
15
use Doctrine\ODM\MongoDB\LockException;
16
use Doctrine\ODM\MongoDB\LockMode;
17
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
18
use Doctrine\ODM\MongoDB\MongoDBException;
19
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
20
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
21
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
22
use Doctrine\ODM\MongoDB\Query\Query;
23
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
use Doctrine\ODM\MongoDB\Types\Versionable;
26
use Doctrine\ODM\MongoDB\UnitOfWork;
27
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
28
use Doctrine\Persistence\Mapping\MappingException;
29
use InvalidArgumentException;
30
use MongoDB\BSON\ObjectId;
31
use MongoDB\Collection;
32
use MongoDB\Driver\Cursor;
33
use MongoDB\Driver\Exception\Exception as DriverException;
34
use MongoDB\Driver\Exception\WriteException;
35
use MongoDB\GridFS\Bucket;
36
use ProxyManager\Proxy\GhostObjectInterface;
37
use stdClass;
38
use function array_combine;
39
use function array_fill;
40
use function array_intersect_key;
41
use function array_keys;
42
use function array_map;
43
use function array_merge;
44
use function array_search;
45
use function array_slice;
46
use function array_values;
47
use function assert;
48
use function count;
49
use function explode;
50
use function get_class;
51
use function get_object_vars;
52
use function gettype;
53
use function implode;
54
use function in_array;
55
use function is_array;
56
use function is_object;
57
use function is_scalar;
58
use function is_string;
59
use function spl_object_hash;
60
use function sprintf;
61
use function strpos;
62
use function strtolower;
63
64
/**
65
 * The DocumentPersister is responsible for persisting documents.
66
 *
67
 * @internal
68
 */
69
final class DocumentPersister
70
{
71
    /** @var PersistenceBuilder */
72
    private $pb;
73
74
    /** @var DocumentManager */
75
    private $dm;
76
77
    /** @var UnitOfWork */
78
    private $uow;
79
80
    /** @var ClassMetadata */
81
    private $class;
82
83
    /** @var Collection|null */
84
    private $collection;
85
86
    /** @var Bucket|null */
87
    private $bucket;
88
89
    /**
90
     * Array of queued inserts for the persister to insert.
91
     *
92
     * @var array
93
     */
94
    private $queuedInserts = [];
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedUpserts = [];
102
103
    /** @var CriteriaMerger */
104
    private $cm;
105
106
    /** @var CollectionPersister */
107
    private $cp;
108
109
    /** @var HydratorFactory */
110
    private $hydratorFactory;
111
112 1234
    public function __construct(
113
        PersistenceBuilder $pb,
114
        DocumentManager $dm,
115
        UnitOfWork $uow,
116
        HydratorFactory $hydratorFactory,
117
        ClassMetadata $class,
118
        ?CriteriaMerger $cm = null
119
    ) {
120 1234
        $this->pb              = $pb;
121 1234
        $this->dm              = $dm;
122 1234
        $this->cm              = $cm ?: new CriteriaMerger();
123 1234
        $this->uow             = $uow;
124 1234
        $this->hydratorFactory = $hydratorFactory;
125 1234
        $this->class           = $class;
126 1234
        $this->cp              = $this->uow->getCollectionPersister();
127
128 1234
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
129 95
            return;
130
        }
131
132 1231
        $this->collection = $dm->getDocumentCollection($class->name);
133
134 1231
        if (! $class->isFile) {
135 1218
            return;
136
        }
137
138 21
        $this->bucket = $dm->getDocumentBucket($class->name);
139 21
    }
140
141
    public function getInserts() : array
142
    {
143
        return $this->queuedInserts;
144
    }
145
146
    public function isQueuedForInsert(object $document) : bool
147
    {
148
        return isset($this->queuedInserts[spl_object_hash($document)]);
149
    }
150
151
    /**
152
     * Adds a document to the queued insertions.
153
     * The document remains queued until {@link executeInserts} is invoked.
154
     */
155 543
    public function addInsert(object $document) : void
156
    {
157 543
        $this->queuedInserts[spl_object_hash($document)] = $document;
158 543
    }
159
160
    public function getUpserts() : array
161
    {
162
        return $this->queuedUpserts;
163
    }
164
165
    public function isQueuedForUpsert(object $document) : bool
166
    {
167
        return isset($this->queuedUpserts[spl_object_hash($document)]);
168
    }
169
170
    /**
171
     * Adds a document to the queued upserts.
172
     * The document remains queued until {@link executeUpserts} is invoked.
173
     */
174 88
    public function addUpsert(object $document) : void
175
    {
176 88
        $this->queuedUpserts[spl_object_hash($document)] = $document;
177 88
    }
178
179
    /**
180
     * Gets the ClassMetadata instance of the document class this persister is
181
     * used for.
182
     */
183
    public function getClassMetadata() : ClassMetadata
184
    {
185
        return $this->class;
186
    }
187
188
    /**
189
     * Executes all queued document insertions.
190
     *
191
     * Queued documents without an ID will inserted in a batch and queued
192
     * documents with an ID will be upserted individually.
193
     *
194
     * If no inserts are queued, invoking this method is a NOOP.
195
     *
196
     * @throws DriverException
197
     */
198 543
    public function executeInserts(array $options = []) : void
199
    {
200 543
        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...
201
            return;
202
        }
203
204 543
        $inserts = [];
205 543
        $options = $this->getWriteOptions($options);
206 543
        foreach ($this->queuedInserts as $oid => $document) {
207 543
            $data = $this->pb->prepareInsertData($document);
208
209
            // Set the initial version for each insert
210 532
            if ($this->class->isVersioned) {
211 44
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
212 44
                $nextVersion    = $this->class->reflFields[$this->class->versionField]->getValue($document);
213 44
                $type           = Type::getType($versionMapping['type']);
214 44
                assert($type instanceof Versionable);
215 44
                if ($nextVersion === null) {
216 24
                    $nextVersion = $type->getNextVersion(null);
217 24
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
218
                }
219 44
                $data[$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
220
            }
221
222 532
            $inserts[] = $data;
223
        }
224
225 532
        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...
226
            try {
227 532
                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...
228 532
                $this->collection->insertMany($inserts, $options);
229 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...
230 6
                $this->queuedInserts = [];
231 6
                throw $e;
232
            }
233
        }
234
235
        /* All collections except for ones using addToSet have already been
236
         * saved. We have left these to be handled separately to avoid checking
237
         * collection for uniqueness on PHP side.
238
         */
239 532
        foreach ($this->queuedInserts as $document) {
240 532
            $this->handleCollections($document, $options);
241
        }
242
243 532
        $this->queuedInserts = [];
244 532
    }
245
246
    /**
247
     * Executes all queued document upserts.
248
     *
249
     * Queued documents with an ID are upserted individually.
250
     *
251
     * If no upserts are queued, invoking this method is a NOOP.
252
     */
253 88
    public function executeUpserts(array $options = []) : void
254
    {
255 88
        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...
256
            return;
257
        }
258
259 88
        $options = $this->getWriteOptions($options);
260 88
        foreach ($this->queuedUpserts as $oid => $document) {
261
            try {
262 88
                $this->executeUpsert($document, $options);
263 88
                $this->handleCollections($document, $options);
264 88
                unset($this->queuedUpserts[$oid]);
265
            } 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...
266
                unset($this->queuedUpserts[$oid]);
267
                throw $e;
268
            }
269
        }
270 88
    }
271
272
    /**
273
     * Executes a single upsert in {@link executeUpserts}
274
     */
275 88
    private function executeUpsert(object $document, array $options) : void
276
    {
277 88
        $options['upsert'] = true;
278 88
        $criteria          = $this->getQueryForDocument($document);
279
280 88
        $data = $this->pb->prepareUpsertData($document);
281
282
        // Set the initial version for each upsert
283 88
        if ($this->class->isVersioned) {
284 5
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
285 5
            $nextVersion    = $this->class->reflFields[$this->class->versionField]->getValue($document);
286 5
            $type           = Type::getType($versionMapping['type']);
287 5
            assert($type instanceof Versionable);
288 5
            if ($nextVersion === null) {
289 4
                $nextVersion = $type->getNextVersion(null);
290 4
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
291
            }
292 5
            $data['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
293
        }
294
295 88
        foreach (array_keys($criteria) as $field) {
296 88
            unset($data['$set'][$field]);
297 88
            unset($data['$inc'][$field]);
298 88
            unset($data['$setOnInsert'][$field]);
299
        }
300
301
        // Do not send empty update operators
302 88
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
303 88
            if (! empty($data[$operator])) {
304 73
                continue;
305
            }
306
307 88
            unset($data[$operator]);
308
        }
309
310
        /* If there are no modifiers remaining, we're upserting a document with
311
         * an identifier as its only field. Since a document with the identifier
312
         * may already exist, the desired behavior is "insert if not exists" and
313
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
314
         * the identifier to the same value in our criteria.
315
         *
316
         * This will fail for versions before MongoDB 2.6, which require an
317
         * empty $set modifier. The best we can do (without attempting to check
318
         * server versions in advance) is attempt the 2.6+ behavior and retry
319
         * after the relevant exception.
320
         *
321
         * See: https://jira.mongodb.org/browse/SERVER-12266
322
         */
323 88
        if (empty($data)) {
324 16
            $retry = true;
325 16
            $data  = ['$set' => ['_id' => $criteria['_id']]];
326
        }
327
328
        try {
329 88
            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...
330 88
            $this->collection->updateOne($criteria, $data, $options);
331
332 88
            return;
333
        } 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...
334
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
335
                throw $e;
336
            }
337
        }
338
339
        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...
340
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
341
    }
342
343
    /**
344
     * Updates the already persisted document if it has any new changesets.
345
     *
346
     * @throws LockException
347
     */
348 240
    public function update(object $document, array $options = []) : void
349
    {
350 240
        $update = $this->pb->prepareUpdateData($document);
351
352 240
        $query = $this->getQueryForDocument($document);
353
354 238
        foreach (array_keys($query) as $field) {
355 238
            unset($update['$set'][$field]);
356
        }
357
358 238
        if (empty($update['$set'])) {
359 101
            unset($update['$set']);
360
        }
361
362
        // Include versioning logic to set the new version value in the database
363
        // and to ensure the version has not changed since this document object instance
364
        // was fetched from the database
365 238
        $nextVersion = null;
366 238
        if ($this->class->isVersioned) {
367 39
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
368 39
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
369 39
            $type           = Type::getType($versionMapping['type']);
370 39
            assert($type instanceof Versionable);
371 39
            $nextVersion                             = $type->getNextVersion($currentVersion);
372 39
            $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
373 39
            $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
374
        }
375
376 238
        if (! empty($update)) {
377
            // Include locking logic so that if the document object in memory is currently
378
            // locked then it will remove it, otherwise it ensures the document is not locked.
379 164
            if ($this->class->isLockable) {
380 17
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
381 17
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
382 17
                if ($isLocked) {
383 2
                    $update['$unset'] = [$lockMapping['name'] => true];
384
                } else {
385 15
                    $query[$lockMapping['name']] = ['$exists' => false];
386
                }
387
            }
388
389 164
            $options = $this->getWriteOptions($options);
390
391 164
            assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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