Completed
Push — master ( bc5e59...efdde4 )
by Andreas
24:33 queued 11s
created

DocumentPersister::update()   F

Complexity

Conditions 17
Paths 240

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 17

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use BadMethodCallException;
8
use DateTime;
9
use DateTimeImmutable;
10
use Doctrine\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 bcadd;
50
use function bccomp;
51
use function count;
52
use function explode;
53
use function get_class;
54
use function get_object_vars;
55
use function gettype;
56
use function implode;
57
use function in_array;
58
use function is_array;
59
use function is_object;
60
use function is_scalar;
61
use function is_string;
62
use function max;
63
use function spl_object_hash;
64
use function sprintf;
65
use function strpos;
66
use function strtolower;
67
68
/**
69
 * The DocumentPersister is responsible for persisting documents.
70
 *
71
 * @internal
72
 */
73
final class DocumentPersister
74
{
75
    /** @var PersistenceBuilder */
76
    private $pb;
77
78
    /** @var DocumentManager */
79
    private $dm;
80
81
    /** @var UnitOfWork */
82
    private $uow;
83
84
    /** @var ClassMetadata */
85
    private $class;
86
87
    /** @var Collection|null */
88
    private $collection;
89
90
    /** @var Bucket|null */
91
    private $bucket;
92
93
    /**
94
     * Array of queued inserts for the persister to insert.
95
     *
96
     * @var array
97
     */
98
    private $queuedInserts = [];
99
100
    /**
101
     * Array of queued inserts for the persister to insert.
102
     *
103
     * @var array
104
     */
105
    private $queuedUpserts = [];
106
107
    /** @var CriteriaMerger */
108
    private $cm;
109
110
    /** @var CollectionPersister */
111
    private $cp;
112
113
    /** @var HydratorFactory */
114 1231
    private $hydratorFactory;
115
116
    public function __construct(
117
        PersistenceBuilder $pb,
118
        DocumentManager $dm,
119
        UnitOfWork $uow,
120
        HydratorFactory $hydratorFactory,
121
        ClassMetadata $class,
122 1231
        ?CriteriaMerger $cm = null
123 1231
    ) {
124 1231
        $this->pb              = $pb;
125 1231
        $this->dm              = $dm;
126 1231
        $this->cm              = $cm ?: new CriteriaMerger();
127 1231
        $this->uow             = $uow;
128 1231
        $this->hydratorFactory = $hydratorFactory;
129
        $this->class           = $class;
130 1231
        $this->cp              = $this->uow->getCollectionPersister();
131 95
132
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
133
            return;
134 1228
        }
135
136 1228
        $this->collection = $dm->getDocumentCollection($class->name);
137 1215
138
        if (! $class->isFile) {
139
            return;
140 21
        }
141 21
142
        $this->bucket = $dm->getDocumentBucket($class->name);
143
    }
144
145
    public function getInserts() : array
146
    {
147
        return $this->queuedInserts;
148
    }
149
150
    public function isQueuedForInsert(object $document) : bool
151
    {
152
        return isset($this->queuedInserts[spl_object_hash($document)]);
153
    }
154
155
    /**
156
     * Adds a document to the queued insertions.
157 541
     * The document remains queued until {@link executeInserts} is invoked.
158
     */
159 541
    public function addInsert(object $document) : void
160 541
    {
161
        $this->queuedInserts[spl_object_hash($document)] = $document;
162
    }
163
164
    public function getUpserts() : array
165
    {
166
        return $this->queuedUpserts;
167
    }
168
169
    public function isQueuedForUpsert(object $document) : bool
170
    {
171
        return isset($this->queuedUpserts[spl_object_hash($document)]);
172
    }
173
174
    /**
175
     * Adds a document to the queued upserts.
176 87
     * The document remains queued until {@link executeUpserts} is invoked.
177
     */
178 87
    public function addUpsert(object $document) : void
179 87
    {
180
        $this->queuedUpserts[spl_object_hash($document)] = $document;
181
    }
182
183
    /**
184
     * Gets the ClassMetadata instance of the document class this persister is
185
     * used for.
186
     */
187
    public function getClassMetadata() : ClassMetadata
188
    {
189
        return $this->class;
190
    }
191
192
    /**
193
     * Executes all queued document insertions.
194
     *
195
     * Queued documents without an ID will inserted in a batch and queued
196
     * documents with an ID will be upserted individually.
197
     *
198
     * If no inserts are queued, invoking this method is a NOOP.
199
     *
200 541
     * @throws DriverException
201
     */
202 541
    public function executeInserts(array $options = []) : void
203
    {
204
        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...
205
            return;
206 541
        }
207 541
208 541
        $inserts = [];
209 541
        $options = $this->getWriteOptions($options);
210
        foreach ($this->queuedInserts as $oid => $document) {
211
            $data = $this->pb->prepareInsertData($document);
212 530
213 42
            // Set the initial version for each insert
214 42
            if ($this->class->isVersioned) {
215 42
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
216 38
                $nextVersion    = null;
217 38
                if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
218 4
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
219 4
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
220 4
                } elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
221 4
                    $nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
222
                    $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
223 42
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
224
                } elseif ($versionMapping['type'] === Type::DECIMAL128) {
225
                    $current     = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
226 530
                    $nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
227
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
228
                }
229 530
                $data[$versionMapping['name']] = $nextVersion;
230
            }
231 530
232 530
            $inserts[] = $data;
233 6
        }
234 6
235 6
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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