Completed
Pull Request — master (#2183)
by Maciej
20:02
created

DocumentPersister::unlock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 8
cts 8
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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
    private $hydratorFactory;
115
116 1234
    public function __construct(
117
        PersistenceBuilder $pb,
118
        DocumentManager $dm,
119
        UnitOfWork $uow,
120
        HydratorFactory $hydratorFactory,
121
        ClassMetadata $class,
122
        ?CriteriaMerger $cm = null
123
    ) {
124 1234
        $this->pb              = $pb;
125 1234
        $this->dm              = $dm;
126 1234
        $this->cm              = $cm ?: new CriteriaMerger();
127 1234
        $this->uow             = $uow;
128 1234
        $this->hydratorFactory = $hydratorFactory;
129 1234
        $this->class           = $class;
130 1234
        $this->cp              = $this->uow->getCollectionPersister();
131
132 1234
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
133 95
            return;
134
        }
135
136 1231
        $this->collection = $dm->getDocumentCollection($class->name);
137
138 1231
        if (! $class->isFile) {
139 1218
            return;
140
        }
141
142 21
        $this->bucket = $dm->getDocumentBucket($class->name);
143 21
    }
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
     * The document remains queued until {@link executeInserts} is invoked.
158
     */
159 543
    public function addInsert(object $document) : void
160
    {
161 543
        $this->queuedInserts[spl_object_hash($document)] = $document;
162 543
    }
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
     * The document remains queued until {@link executeUpserts} is invoked.
177
     */
178 88
    public function addUpsert(object $document) : void
179
    {
180 88
        $this->queuedUpserts[spl_object_hash($document)] = $document;
181 88
    }
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
     * @throws DriverException
201
     */
202 543
    public function executeInserts(array $options = []) : void
203
    {
204 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...
205
            return;
206
        }
207
208 543
        $inserts = [];
209 543
        $options = $this->getWriteOptions($options);
210 543
        foreach ($this->queuedInserts as $oid => $document) {
211 543
            $data = $this->pb->prepareInsertData($document);
212
213
            // Set the initial version for each insert
214 532
            if ($this->class->isVersioned) {
215 44
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
216 44
                $nextVersion    = null;
217 44
                if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Types\Type::INTEGER has been deprecated with message: const was deprecated in 2.1 and will be removed in 3.0. Use Type::INT instead

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
218 38
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
219 38
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
220 6
                } elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
221 4
                    $nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
222 4
                    $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
223 4
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
224 2
                } elseif ($versionMapping['type'] === Type::DECIMAL128) {
225 2
                    $current     = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
226 2
                    $nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
227 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
228
                }
229 44
                $data[$versionMapping['name']] = $nextVersion;
230
            }
231
232 532
            $inserts[] = $data;
233
        }
234
235 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...
236
            try {
237 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...
238 532
                $this->collection->insertMany($inserts, $options);
239 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...
240 6
                $this->queuedInserts = [];
241 6
                throw $e;
242
            }
243
        }
244
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
         * collection for uniqueness on PHP side.
248
         */
249 532
        foreach ($this->queuedInserts as $document) {
250 532
            $this->handleCollections($document, $options);
251
        }
252
253 532
        $this->queuedInserts = [];
254 532
    }
255
256
    /**
257
     * Executes all queued document upserts.
258
     *
259
     * Queued documents with an ID are upserted individually.
260
     *
261
     * If no upserts are queued, invoking this method is a NOOP.
262
     */
263 88
    public function executeUpserts(array $options = []) : void
264
    {
265 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...
266
            return;
267
        }
268
269 88
        $options = $this->getWriteOptions($options);
270 88
        foreach ($this->queuedUpserts as $oid => $document) {
271
            try {
272 88
                $this->executeUpsert($document, $options);
273 88
                $this->handleCollections($document, $options);
274 88
                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
        }
280 88
    }
281
282
    /**
283
     * Executes a single upsert in {@link executeUpserts}
284
     */
285 88
    private function executeUpsert(object $document, array $options) : void
286
    {
287 88
        $options['upsert'] = true;
288 88
        $criteria          = $this->getQueryForDocument($document);
289
290 88
        $data = $this->pb->prepareUpsertData($document);
291
292
        // Set the initial version for each upsert
293 88
        if ($this->class->isVersioned) {
294 5
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
295 5
            $nextVersion    = null;
296 5
            if ($versionMapping['type'] === Type::INT || $versionMapping === Type::INTEGER) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Types\Type::INTEGER has been deprecated with message: const was deprecated in 2.1 and will be removed in 3.0. Use Type::INT instead

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
297 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
298 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
299 3
            } elseif ($versionMapping['type'] === Type::DATE || $versionMapping['type'] === Type::DATE_IMMUTABLE) {
300 2
                $nextVersionDateTime = $versionMapping['type'] === Type::DATE ? new DateTime() : new DateTimeImmutable();
301 2
                $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
302 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
303 1
            } elseif ($versionMapping['type'] === Type::DECIMAL128) {
304 1
                $current     = (string) $this->class->reflFields[$this->class->versionField]->getValue($document);
305 1
                $nextVersion = bccomp('1', $current) === 1 ? '1' : $current;
306 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
307
            }
308 5
            $data['$set'][$versionMapping['name']] = $nextVersion;
309
        }
310
311 88
        foreach (array_keys($criteria) as $field) {
312 88
            unset($data['$set'][$field]);
313 88
            unset($data['$inc'][$field]);
314 88
            unset($data['$setOnInsert'][$field]);
315
        }
316
317
        // Do not send empty update operators
318 88
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
319 88
            if (! empty($data[$operator])) {
320 73
                continue;
321
            }
322
323 88
            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
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
330
         * the identifier to the same value in our criteria.
331
         *
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
         * after the relevant exception.
336
         *
337
         * See: https://jira.mongodb.org/browse/SERVER-12266
338
         */
339 88
        if (empty($data)) {
340 16
            $retry = true;
341 16
            $data  = ['$set' => ['_id' => $criteria['_id']]];
342
        }
343
344
        try {
345 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...
346 88
            $this->collection->updateOne($criteria, $data, $options);
347
348 88
            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
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
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
357
    }
358
359
    /**
360
     * Updates the already persisted document if it has any new changesets.
361
     *
362
     * @throws LockException
363
     */
364 240
    public function update(object $document, array $options = []) : void
365
    {
366 240
        $update = $this->pb->prepareUpdateData($document);
367
368 240
        $query = $this->getQueryForDocument($document);
369
370 238
        foreach (array_keys($query) as $field) {
371 238
            unset($update['$set'][$field]);
372
        }
373
374 238
        if (empty($update['$set'])) {
375 101
            unset($update['$set']);
376
        }
377
378
        // Include versioning logic to set the new version value in the database
379
        // and to ensure the version has not changed since this document object instance
380
        // was fetched from the database
381 238
        $nextVersion = null;
382 238
        if ($this->class->isVersioned) {
383 39
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
384 39
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
385 39
            if ($versionMapping['type'] === Type::INT || $versionMapping['type'] === Type::INTEGER) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Types\Type::INTEGER has been deprecated with message: const was deprecated in 2.1 and will be removed in 3.0. Use Type::INT instead

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

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