Completed
Pull Request — master (#2184)
by Maciej
25:09
created

DocumentPersister::executeInserts()   B

Complexity

Conditions 8
Paths 25

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.0032

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 26
cts 27
cp 0.963
rs 7.9119
c 0
b 0
f 0
cc 8
nc 25
nop 1
crap 8.0032
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use BadMethodCallException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
10
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
11
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
12
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
13
use Doctrine\ODM\MongoDB\Iterator\Iterator;
14
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
15
use Doctrine\ODM\MongoDB\LockException;
16
use Doctrine\ODM\MongoDB\LockMode;
17
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
18
use Doctrine\ODM\MongoDB\MongoDBException;
19
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
20
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
21
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
22
use Doctrine\ODM\MongoDB\Query\Query;
23
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
use Doctrine\ODM\MongoDB\Types\Versionable;
26
use Doctrine\ODM\MongoDB\UnitOfWork;
27
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
28
use Doctrine\Persistence\Mapping\MappingException;
29
use InvalidArgumentException;
30
use MongoDB\BSON\ObjectId;
31
use MongoDB\Collection;
32
use MongoDB\Driver\Cursor;
33
use MongoDB\Driver\Exception\Exception as DriverException;
34
use MongoDB\Driver\Exception\WriteException;
35
use MongoDB\GridFS\Bucket;
36
use ProxyManager\Proxy\GhostObjectInterface;
37
use stdClass;
38
use function array_combine;
39
use function array_fill;
40
use function array_intersect_key;
41
use function array_keys;
42
use function array_map;
43
use function array_merge;
44
use function array_search;
45
use function array_slice;
46
use function array_values;
47
use function assert;
48
use function count;
49
use function explode;
50
use function get_class;
51
use function get_object_vars;
52
use function gettype;
53
use function implode;
54
use function in_array;
55
use function is_array;
56
use function is_object;
57
use function is_scalar;
58
use function is_string;
59
use function spl_object_hash;
60
use function sprintf;
61
use function strpos;
62
use function strtolower;
63
64
/**
65
 * The DocumentPersister is responsible for persisting documents.
66
 *
67
 * @internal
68
 */
69
final class DocumentPersister
70
{
71
    /** @var PersistenceBuilder */
72
    private $pb;
73
74
    /** @var DocumentManager */
75
    private $dm;
76
77
    /** @var UnitOfWork */
78
    private $uow;
79
80
    /** @var ClassMetadata */
81
    private $class;
82
83
    /** @var Collection|null */
84
    private $collection;
85
86
    /** @var Bucket|null */
87
    private $bucket;
88
89
    /**
90
     * Array of queued inserts for the persister to insert.
91
     *
92
     * @var array
93
     */
94
    private $queuedInserts = [];
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedUpserts = [];
102
103
    /** @var CriteriaMerger */
104
    private $cm;
105
106
    /** @var CollectionPersister */
107
    private $cp;
108
109
    /** @var HydratorFactory */
110
    private $hydratorFactory;
111
112
    public function __construct(
113
        PersistenceBuilder $pb,
114 1231
        DocumentManager $dm,
115
        UnitOfWork $uow,
116
        HydratorFactory $hydratorFactory,
117
        ClassMetadata $class,
118
        ?CriteriaMerger $cm = null
119
    ) {
120
        $this->pb              = $pb;
121
        $this->dm              = $dm;
122 1231
        $this->cm              = $cm ?: new CriteriaMerger();
123 1231
        $this->uow             = $uow;
124 1231
        $this->hydratorFactory = $hydratorFactory;
125 1231
        $this->class           = $class;
126 1231
        $this->cp              = $this->uow->getCollectionPersister();
127 1231
128 1231
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
129
            return;
130 1231
        }
131 95
132
        $this->collection = $dm->getDocumentCollection($class->name);
133
134 1228
        if (! $class->isFile) {
135
            return;
136 1228
        }
137 1215
138
        $this->bucket = $dm->getDocumentBucket($class->name);
139
    }
140 21
141 21
    public function getInserts() : array
142
    {
143
        return $this->queuedInserts;
144
    }
145
146
    public function isQueuedForInsert(object $document) : bool
147
    {
148
        return isset($this->queuedInserts[spl_object_hash($document)]);
149
    }
150
151
    /**
152
     * Adds a document to the queued insertions.
153
     * The document remains queued until {@link executeInserts} is invoked.
154
     */
155
    public function addInsert(object $document) : void
156
    {
157 541
        $this->queuedInserts[spl_object_hash($document)] = $document;
158
    }
159 541
160 541
    public function getUpserts() : array
161
    {
162
        return $this->queuedUpserts;
163
    }
164
165
    public function isQueuedForUpsert(object $document) : bool
166
    {
167
        return isset($this->queuedUpserts[spl_object_hash($document)]);
168
    }
169
170
    /**
171
     * Adds a document to the queued upserts.
172
     * The document remains queued until {@link executeUpserts} is invoked.
173
     */
174
    public function addUpsert(object $document) : void
175
    {
176 87
        $this->queuedUpserts[spl_object_hash($document)] = $document;
177
    }
178 87
179 87
    /**
180
     * Gets the ClassMetadata instance of the document class this persister is
181
     * used for.
182
     */
183
    public function getClassMetadata() : ClassMetadata
184
    {
185
        return $this->class;
186
    }
187
188
    /**
189
     * Executes all queued document insertions.
190
     *
191
     * Queued documents without an ID will inserted in a batch and queued
192
     * documents with an ID will be upserted individually.
193
     *
194
     * If no inserts are queued, invoking this method is a NOOP.
195
     *
196
     * @throws DriverException
197
     */
198
    public function executeInserts(array $options = []) : void
199
    {
200 541
        if (! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
201
            return;
202 541
        }
203
204
        $inserts = [];
205
        $options = $this->getWriteOptions($options);
206 541
        foreach ($this->queuedInserts as $oid => $document) {
207 541
            $data = $this->pb->prepareInsertData($document);
208 541
209 541
            // Set the initial version for each insert
210
            if ($this->class->isVersioned) {
211
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
212 530
                $nextVersion    = $this->class->reflFields[$this->class->versionField]->getValue($document);
213 42
                $type           = Type::getType($this->class->getTypeOfField($this->class->versionField));
214 42
                \assert($type instanceof Versionable);
215 42
                if ($nextVersion === null) {
216 38
                    $nextVersion = $type->getNextVersion(null);
217 38
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
218 4
                }
219 4
                $data[$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
220 4
            }
221 4
222
            $inserts[] = $data;
223 42
        }
224
225
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
226 530
            try {
227
                assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
228
                $this->collection->insertMany($inserts, $options);
229 530
            } catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
230
                $this->queuedInserts = [];
231 530
                throw $e;
232 530
            }
233 6
        }
234 6
235 6
        /* All collections except for ones using addToSet have already been
236
         * saved. We have left these to be handled separately to avoid checking
237
         * collection for uniqueness on PHP side.
238
         */
239
        foreach ($this->queuedInserts as $document) {
240
            $this->handleCollections($document, $options);
241
        }
242
243 530
        $this->queuedInserts = [];
244 530
    }
245
246
    /**
247 530
     * Executes all queued document upserts.
248 530
     *
249
     * Queued documents with an ID are upserted individually.
250
     *
251
     * If no upserts are queued, invoking this method is a NOOP.
252
     */
253
    public function executeUpserts(array $options = []) : void
254
    {
255
        if (! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
256
            return;
257 87
        }
258
259 87
        $options = $this->getWriteOptions($options);
260
        foreach ($this->queuedUpserts as $oid => $document) {
261
            try {
262
                $this->executeUpsert($document, $options);
263 87
                $this->handleCollections($document, $options);
264 87
                unset($this->queuedUpserts[$oid]);
265
            } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
266 87
                unset($this->queuedUpserts[$oid]);
267 87
                throw $e;
268 87
            }
269
        }
270
    }
271
272
    /**
273
     * Executes a single upsert in {@link executeUpserts}
274 87
     */
275
    private function executeUpsert(object $document, array $options) : void
276
    {
277
        $options['upsert'] = true;
278
        $criteria          = $this->getQueryForDocument($document);
279 87
280
        $data = $this->pb->prepareUpsertData($document);
281 87
282 87
        // Set the initial version for each upsert
283
        if ($this->class->isVersioned) {
284 87
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
285
            $nextVersion    = $this->class->reflFields[$this->class->versionField]->getValue($document);
286
            $type           = Type::getType($this->class->getTypeOfField($this->class->versionField));
287 87
            \assert($type instanceof Versionable);
288 4
            if ($nextVersion === null) {
289 4
                $nextVersion = $type->getNextVersion(null);
290 4
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
291 2
            }
292 2
            $data['$set'][$versionMapping['name']] = $type->convertPHPToDatabaseValue($nextVersion);
293 2
        }
294 2
295 2
        foreach (array_keys($criteria) as $field) {
296 2
            unset($data['$set'][$field]);
297
            unset($data['$inc'][$field]);
298 4
            unset($data['$setOnInsert'][$field]);
299
        }
300
301 87
        // Do not send empty update operators
302 87
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
303 87
            if (! empty($data[$operator])) {
304 87
                continue;
305
            }
306
307
            unset($data[$operator]);
308 87
        }
309 87
310 72
        /* If there are no modifiers remaining, we're upserting a document with
311
         * an identifier as its only field. Since a document with the identifier
312
         * may already exist, the desired behavior is "insert if not exists" and
313 87
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
314
         * the identifier to the same value in our criteria.
315
         *
316
         * This will fail for versions before MongoDB 2.6, which require an
317
         * empty $set modifier. The best we can do (without attempting to check
318
         * server versions in advance) is attempt the 2.6+ behavior and retry
319
         * after the relevant exception.
320
         *
321
         * See: https://jira.mongodb.org/browse/SERVER-12266
322
         */
323
        if (empty($data)) {
324
            $retry = true;
325
            $data  = ['$set' => ['_id' => $criteria['_id']]];
326
        }
327
328
        try {
329 87
            assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
330 16
            $this->collection->updateOne($criteria, $data, $options);
331 16
332
            return;
333
        } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
334
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
335 87
                throw $e;
336 87
            }
337
        }
338 87
339
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
340
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
341
    }
342
343
    /**
344
     * Updates the already persisted document if it has any new changesets.
345
     *
346
     * @throws LockException
347
     */
348
    public function update(object $document, array $options = []) : void
349
    {
350
        $update = $this->pb->prepareUpdateData($document);
351
352
        $query = $this->getQueryForDocument($document);
353
354 237
        foreach (array_keys($query) as $field) {
355
            unset($update['$set'][$field]);
356 237
        }
357
358 237
        if (empty($update['$set'])) {
359
            unset($update['$set']);
360 235
        }
361 235
362
        // Include versioning logic to set the new version value in the database
363
        // and to ensure the version has not changed since this document object instance
364 235
        // was fetched from the database
365 101
        $nextVersion = null;
366
        if ($this->class->isVersioned) {
367
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
368
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
369
            $type           = Type::getType($this->class->getTypeOfField($this->class->versionField));
370
            \assert($type instanceof Versionable);
371 235
            $nextVersion                             = $type->getNextVersion($currentVersion);
372 235
            $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
373 36
            $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
374 36
        }
375 36
376 30
        if (! empty($update)) {
377 30
            // Include locking logic so that if the document object in memory is currently
378 30
            // locked then it will remove it, otherwise it ensures the document is not locked.
379 6
            if ($this->class->isLockable) {
380 6
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
381 6
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
382 6
                if ($isLocked) {
383
                    $update['$unset'] = [$lockMapping['name'] => true];
384
                } else {
385
                    $query[$lockMapping['name']] = ['$exists' => false];
386 235
                }
387
            }
388
389 161
            $options = $this->getWriteOptions($options);
390 14
391 14
            assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
392 14
            $result = $this->collection->updateOne($query, $update, $options);
393 2
394
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
395 12
                throw LockException::lockFailed($document);
396
            }
397
398
            if ($this->class->isVersioned) {
399 161
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
400
            }
401 161
        }
402 161
403
        $this->handleCollections($document, $options);
404 161
    }
405 7
406
    /**
407
     * Removes document from mongo
408 155
     *
409 30
     * @throws LockException
410
     */
411
    public function delete(object $document, array $options = []) : void
412
    {
413 229
        if ($this->bucket instanceof Bucket) {
0 ignored issues
show
Bug introduced by
The class MongoDB\GridFS\Bucket does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
414 229
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
415
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
416
417
            $this->bucket->delete($databaseIdentifier);
418
419
            return;
420
        }
421 36
422
        $query = $this->getQueryForDocument($document);
423 36
424 1
        if ($this->class->isLockable) {
425 1
            $query[$this->class->lockField] = ['$exists' => false];
426
        }
427 1
428
        $options = $this->getWriteOptions($options);
429 1
430
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
431
        $result = $this->collection->deleteOne($query, $options);
432 35
433
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
434 35
            throw LockException::lockFailed($document);
435 2
        }
436
    }
437
438 35
    /**
439
     * Refreshes a managed document.
440 35
     */
441 35
    public function refresh(object $document) : void
442
    {
443 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...
444 2
        $query = $this->getQueryForDocument($document);
445
        $data  = $this->collection->findOne($query);
446 33
        if ($data === null) {
447
            throw MongoDBException::cannotRefreshDocument();
448
        }
449
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
450
        $this->uow->setOriginalDocumentData($document, $data);
451 23
    }
452
453 23
    /**
454 23
     * Finds a document by a set of criteria.
455 23
     *
456 23
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
457
     * be used to match an _id value.
458
     *
459 23
     * @param mixed $criteria Query criteria
460 23
     *
461 23
     * @throws LockException
462
     *
463
     * @todo Check identity map? loadById method? Try to guess whether
464
     *     $criteria is the id?
465
     */
466
    public function load($criteria, ?object $document = null, array $hints = [], int $lockMode = 0, ?array $sort = null) : ?object
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
467
    {
468
        // TODO: remove this
469
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof ObjectId) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
470
            $criteria = ['_id' => $criteria];
471
        }
472
473
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
474
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
475
        $criteria = $this->addFilterToPreparedQuery($criteria);
476 369
477
        $options = [];
478
        if ($sort !== null) {
479 369
            $options['sort'] = $this->prepareSort($sort);
480
        }
481
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
482
        $result = $this->collection->findOne($criteria, $options);
483 369
        $result = $result !== null ? (array) $result : null;
484 369
485 369
        if ($this->class->isLockable) {
486
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
487 369
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
488 369
                throw LockException::lockFailed($document);
489 96
            }
490
        }
491 369
492 369
        if ($result === null) {
493 369
            return null;
494
        }
495 369
496 1
        return $this->createDocument($result, $document, $hints);
497 1
    }
498 1
499
    /**
500
     * Finds documents by a set of criteria.
501
     */
502 368
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
503 115
    {
504
        $criteria = $this->prepareQueryOrNewObj($criteria);
505
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
506 324
        $criteria = $this->addFilterToPreparedQuery($criteria);
507
508
        $options = [];
509
        if ($sort !== null) {
510
            $options['sort'] = $this->prepareSort($sort);
511
        }
512 24
513
        if ($limit !== null) {
514 24
            $options['limit'] = $limit;
515 24
        }
516 24
517
        if ($skip !== null) {
518 24
            $options['skip'] = $skip;
519 24
        }
520 11
521
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
522
        $baseCursor = $this->collection->find($criteria, $options);
523 24
524 10
        return $this->wrapCursor($baseCursor);
525
    }
526
527 24
    /**
528 1
     * @throws MongoDBException
529
     */
530
    private function getShardKeyQuery(object $document) : array
531 24
    {
532 24
        if (! $this->class->isSharded()) {
533
            return [];
534 24
        }
535
536
        $shardKey = $this->class->getShardKey();
537
        $keys     = array_keys($shardKey['keys']);
538
        $data     = $this->uow->getDocumentActualData($document);
539
540 318
        $shardKeyQueryPart = [];
541
        foreach ($keys as $key) {
542 318
            assert(is_string($key));
543 308
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
544
            $this->guardMissingShardKey($document, $key, $data);
545
546 10
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
547 10
                $reference = $this->prepareReference(
548 10
                    $key,
549
                    $data[$mapping['fieldName']],
550 10
                    $mapping,
551 10
                    false
552 10
                );
553 10
                foreach ($reference as $keyValue) {
554 10
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
555
                }
556 8
            } else {
557 1
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
558 1
                $shardKeyQueryPart[$key] = $value;
559 1
            }
560 1
        }
561 1
562
        return $shardKeyQueryPart;
563 1
    }
564 1
565
    /**
566
     * Wraps the supplied base cursor in the corresponding ODM class.
567 7
     */
568 7
    private function wrapCursor(Cursor $baseCursor) : Iterator
569
    {
570
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
571
    }
572 8
573
    /**
574
     * Checks whether the given managed document exists in the database.
575
     */
576
    public function exists(object $document) : bool
577
    {
578 24
        $id = $this->class->getIdentifierObject($document);
579
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
580 24
581
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
582
    }
583
584
    /**
585
     * Locks document by storing the lock mode on the mapped lock field.
586 3
     */
587
    public function lock(object $document, int $lockMode) : void
588 3
    {
589 3
        $id          = $this->uow->getDocumentIdentifier($document);
590
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
591 3
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
592
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
593
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
594
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
595
    }
596
597 5
    /**
598
     * Releases any lock that exists on this document.
599 5
     */
600 5
    public function unlock(object $document) : void
601 5
    {
602 5
        $id          = $this->uow->getDocumentIdentifier($document);
603 5
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
604 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
605 5
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
606
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
607
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
608
    }
609
610 1
    /**
611
     * Creates or fills a single document object from an query result.
612 1
     *
613 1
     * @param array  $result   The query result.
614 1
     * @param object $document The document object to fill, if any.
615 1
     * @param array  $hints    Hints for document creation.
616 1
     *
617 1
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
618 1
     */
619
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
620
    {
621
        if ($document !== null) {
622
            $hints[Query::HINT_REFRESH] = true;
623
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
624
            $this->uow->registerManaged($document, $id, $result);
625
        }
626
627
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
628
    }
629 324
630
    /**
631 324
     * Loads a PersistentCollection data. Used in the initialize() method.
632 29
     */
633 29
    public function loadCollection(PersistentCollectionInterface $collection) : void
634 29
    {
635
        $mapping = $collection->getMapping();
636
        switch ($mapping['association']) {
637 324
            case ClassMetadata::EMBED_MANY:
638
                $this->loadEmbedManyCollection($collection);
639
                break;
640
641
            case ClassMetadata::REFERENCE_MANY:
642
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
643 181
                    $this->loadReferenceManyWithRepositoryMethod($collection);
644
                } else {
645 181
                    if ($mapping['isOwningSide']) {
646 181
                        $this->loadReferenceManyCollectionOwningSide($collection);
647
                    } else {
648 127
                        $this->loadReferenceManyCollectionInverseSide($collection);
649 126
                    }
650
                }
651
                break;
652 77
        }
653 5
    }
654
655 73
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
656 61
    {
657
        $embeddedDocuments = $collection->getMongoData();
658 18
        $mapping           = $collection->getMapping();
659
        $owner             = $collection->getOwner();
660
661 76
        if (! $embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
662
            return;
663 179
        }
664
665 127
        if ($owner === null) {
666
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
667 127
        }
668 127
669 127
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
670
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
671 127
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
672 75
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
673
674
            if (! is_array($embeddedDocument)) {
675 98
                throw HydratorException::associationItemTypeMismatch(get_class($owner), $mapping['name'], $key, 'array', gettype($embeddedDocument));
676
            }
677
678
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
679 98
680 98
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
681 98
            $id   = $data[$embeddedMetadata->identifier] ?? null;
682 98
683
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
684 98
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
685 1
            }
686
            if (CollectionHelper::isHash($mapping['strategy'])) {
687
                $collection->set($key, $embeddedDocumentObject);
688 97
            } else {
689
                $collection->add($embeddedDocumentObject);
690 97
            }
691 97
        }
692
    }
693 97
694 96
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
695
    {
696 97
        $hints      = $collection->getHints();
697 25
        $mapping    = $collection->getMapping();
698
        $owner      = $collection->getOwner();
699 80
        $groupedIds = [];
700
701
        if ($owner === null) {
702 97
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
703
        }
704 61
705
        $sorted = isset($mapping['sort']) && $mapping['sort'];
706 61
707 61
        foreach ($collection->getMongoData() as $key => $reference) {
708 61
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
709 61
710
            if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array($reference)) {
711 61
                throw HydratorException::associationItemTypeMismatch(get_class($owner), $mapping['name'], $key, 'array', gettype($reference));
712
            }
713
714
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
715 61
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
716
717 61
            // create a reference to the class and id
718 55
            $reference = $this->dm->getReference($className, $id);
719
720 55
            // no custom sort so add the references right now in the order they are embedded
721 1
            if (! $sorted) {
722
                if (CollectionHelper::isHash($mapping['strategy'])) {
723
                    $collection->set($key, $reference);
724 54
                } else {
725 54
                    $collection->add($reference);
726
                }
727
            }
728 54
729
            // only query for the referenced object if it is not already initialized or the collection is sorted
730
            if (! (($reference instanceof GhostObjectInterface && ! $reference->isProxyInitialized())) && ! $sorted) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
731 54
                continue;
732 53
            }
733 2
734
            $groupedIds[$className][] = $identifier;
735 51
        }
736
        foreach ($groupedIds as $className => $ids) {
737
            $class           = $this->dm->getClassMetadata($className);
738
            $mongoCollection = $this->dm->getDocumentCollection($className);
739
            $criteria        = $this->cm->merge(
740 54
                ['_id' => ['$in' => array_values($ids)]],
741 23
                $this->dm->getFilterCollection()->getFilterCriteria($class),
742
                $mapping['criteria'] ?? []
743
            );
744 39
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
745
746 60
            $options = [];
747 39
            if (isset($mapping['sort'])) {
748 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
749 39
            }
750 39
            if (isset($mapping['limit'])) {
751 39
                $options['limit'] = $mapping['limit'];
752 39
            }
753
            if (isset($mapping['skip'])) {
754 39
                $options['skip'] = $mapping['skip'];
755
            }
756 39
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
757 39
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
758 39
            }
759
760 39
            $cursor    = $mongoCollection->find($criteria, $options);
761
            $documents = $cursor->toArray();
762
            foreach ($documents as $documentData) {
763 39
                $document = $this->uow->getById($documentData['_id'], $class);
764
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
765
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
766 39
                    $this->uow->setOriginalDocumentData($document, $data);
767
                }
768
769
                if (! $sorted) {
770 39
                    continue;
771 39
                }
772 39
773 38
                $collection->add($document);
774 38
            }
775 38
        }
776 38
    }
777
778
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
779 38
    {
780 37
        $query    = $this->createReferenceManyInverseSideQuery($collection);
781
        $iterator = $query->execute();
782
        assert($iterator instanceof Iterator);
783 1
        $documents = $iterator->toArray();
784
        foreach ($documents as $key => $document) {
785
            $collection->add($document);
786 60
        }
787
    }
788 18
789
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
790 18
    {
791 18
        $hints   = $collection->getHints();
792 18
        $mapping = $collection->getMapping();
793 18
        $owner   = $collection->getOwner();
794 18
795 17
        if ($owner === null) {
796
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
797 18
        }
798
799 18
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
800
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
801 18
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
802 18
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
803 18
804
        $criteria = $this->cm->merge(
805 18
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
806
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
807
            $mapping['criteria'] ?? []
808
        );
809 18
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
810 18
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
811 18
            ->setQueryArray($criteria);
812 18
813
        if (isset($mapping['sort'])) {
814 18
            $qb->sort($mapping['sort']);
815 18
        }
816 18
        if (isset($mapping['limit'])) {
817 18
            $qb->limit($mapping['limit']);
818
        }
819 18
        if (isset($mapping['skip'])) {
820 18
            $qb->skip($mapping['skip']);
821 18
        }
822
823 18
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
824 18
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
825
        }
826 18
827 2
        foreach ($mapping['prime'] as $field) {
828
            $qb->field($field)->prime(true);
829 18
        }
830
831
        return $qb->getQuery();
832
    }
833 18
834
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
835
    {
836
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
837 18
        $mapping   = $collection->getMapping();
838 4
        $documents = $cursor->toArray();
839
        foreach ($documents as $key => $obj) {
840
            if (CollectionHelper::isHash($mapping['strategy'])) {
841 18
                $collection->set($key, $obj);
842
            } else {
843
                $collection->add($obj);
844 5
            }
845
        }
846 5
    }
847 5
848 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
849 5
    {
850 5
        $mapping          = $collection->getMapping();
851 1
        $repositoryMethod = $mapping['repositoryMethod'];
852
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
853 4
            ->$repositoryMethod($collection->getOwner());
854
855
        if (! $cursor instanceof Iterator) {
856 5
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
857
        }
858 5
859
        if (! empty($mapping['prime'])) {
860 5
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
861 5
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
862 5
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
863 5
864
            assert(is_array($primers));
865 5
866
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
867
        }
868
869 5
        return $cursor;
870 1
    }
871 1
872 1
    /**
873
     * Prepare a projection array by converting keys, which are PHP property
874 1
     * names, to MongoDB field names.
875
     */
876 1
    public function prepareProjection(array $fields) : array
877
    {
878
        $preparedFields = [];
879 5
880
        foreach ($fields as $key => $value) {
881
            $preparedFields[$this->prepareFieldName($key)] = $value;
882
        }
883
884
        return $preparedFields;
885
    }
886 15
887
    /**
888 15
     * @param int|string $sort
889
     *
890 15
     * @return int|string|null
891 15
     */
892
    private function getSortDirection($sort)
893
    {
894 15
        switch (strtolower((string) $sort)) {
895
            case 'desc':
896
                return -1;
897
            case 'asc':
898
                return 1;
899
        }
900
901
        return $sort;
902 27
    }
903
904 27
    /**
905 27
     * Prepare a sort specification array by converting keys to MongoDB field
906 15
     * names and changing direction strings to int.
907 24
     */
908 13
    public function prepareSort(array $fields) : array
909
    {
910
        $sortFields = [];
911 14
912
        foreach ($fields as $key => $value) {
913
            if (is_array($value)) {
914
                $sortFields[$this->prepareFieldName($key)] = $value;
915
            } else {
916
                $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
917
            }
918 144
        }
919
920 144
        return $sortFields;
921
    }
922 144
923 27
    /**
924 1
     * Prepare a mongodb field name and convert the PHP property names to
925
     * MongoDB field names.
926 27
     */
927
    public function prepareFieldName(string $fieldName) : string
928
    {
929
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
930 144
931
        return $fieldNames[0][0];
932
    }
933
934
    /**
935
     * Adds discriminator criteria to an already-prepared query.
936
     *
937 475
     * If the class we're querying has a discriminator field set, we add all
938
     * possible discriminator values to the query. The list of possible
939 475
     * discriminator values is based on the discriminatorValue of the class
940
     * itself as well as those of all its subclasses.
941 475
     *
942
     * This method should be used once for query criteria and not be used for
943
     * nested expressions. It should be called before
944
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
945
     */
946
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
947
    {
948
        if (isset($preparedQuery[$this->class->discriminatorField]) || $this->class->discriminatorField === null) {
949
            return $preparedQuery;
950
        }
951
952
        $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
953
954
        if ($discriminatorValues === []) {
955
            return $preparedQuery;
956 540
        }
957
958 540
        if (count($discriminatorValues) === 1) {
959 517
            $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
960
        } else {
961
            $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
962 32
        }
963
964 32
        return $preparedQuery;
965 1
    }
966
967
    /**
968 32
     * Adds filter criteria to an already-prepared query.
969 21
     *
970
     * This method should be used once for query criteria and not be used for
971 14
     * nested expressions. It should be called after
972
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
973
     */
974 32
    public function addFilterToPreparedQuery(array $preparedQuery) : array
975
    {
976
        /* If filter criteria exists for this class, prepare it and merge
977
         * over the existing query.
978
         *
979
         * @todo Consider recursive merging in case the filter criteria and
980
         * prepared query both contain top-level $and/$or operators.
981
         */
982
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
983
        if ($filterCriteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filterCriteria of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
984 541
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
985
        }
986
987
        return $preparedQuery;
988
    }
989
990
    /**
991
     * Prepares the query criteria or new document object.
992 541
     *
993 541
     * PHP field names and types will be converted to those used by MongoDB.
994 18
     */
995
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
996
    {
997 541
        $preparedQuery = [];
998
999
        foreach ($query as $key => $value) {
1000
            // Recursively prepare logical query clauses
1001
            if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) {
1002
                foreach ($value as $k2 => $v2) {
1003
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1004
                }
1005 611
                continue;
1006
            }
1007 611
1008
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1009 611
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1010
                continue;
1011 564
            }
1012 20
1013 20
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1014
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1015 20
                $preparedValue = Type::convertPHPToDatabaseValue($preparedValue);
0 ignored issues
show
Bug introduced by
The variable $preparedValue does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1016
                if ($this->class->hasField($key)) {
1017
                    $preparedValue = $this->convertToDatabaseValue($key, $preparedValue);
1018 564
                }
1019 74
                $preparedQuery[$preparedKey] = $preparedValue;
0 ignored issues
show
Bug introduced by
The variable $preparedKey does not exist. Did you forget to declare it?

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

Loading history...
1020 74
            }
1021
        }
1022
1023 564
        return $preparedQuery;
1024 564
    }
1025 564
1026 564
    /**
1027 246
     * Converts a single value to its database representation based on the mapping type
1028
     *
1029 564
     * @param mixed $value
1030
     *
1031
     * @return mixed
1032
     */
1033 611
    private function convertToDatabaseValue(string $fieldName, $value)
1034
    {
1035
        $mapping  = $this->class->fieldMappings[$fieldName];
1036
        $typeName = $mapping['type'];
1037
1038
        if (is_array($value)) {
1039
            foreach ($value as $k => $v) {
1040
                $value[$k] = $this->convertToDatabaseValue($fieldName, $v);
1041
            }
1042
1043 246
            return $value;
1044
        }
1045 246
1046 246
        if (! empty($mapping['reference']) || ! empty($mapping['embedded'])) {
1047
            return $value;
1048 246
        }
1049 60
1050 59
        if (! Type::hasType($typeName)) {
1051
            throw new InvalidArgumentException(
1052
                sprintf('Mapping type "%s" does not exist', $typeName)
1053 60
            );
1054
        }
1055
        if (in_array($typeName, ['collection', 'hash'])) {
1056 246
            return $value;
1057 128
        }
1058
1059
        $type  = Type::getType($typeName);
1060 166
        $value = $type->convertToDatabaseValue($value);
1061
1062
        return $value;
1063
    }
1064
1065 166
    /**
1066 7
     * Prepares a query value and converts the PHP value to the database value
1067
     * if it is an identifier.
1068
     *
1069 161
     * It also handles converting $fieldName to the database name if they are
1070 161
     * different.
1071
     *
1072 161
     * @param mixed $value
1073
     */
1074
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1075
    {
1076
        $class = $class ?? $this->class;
1077
1078
        // @todo Consider inlining calls to ClassMetadata methods
1079
1080
        // Process all non-identifier fields by translating field names
1081
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1082
            $mapping   = $class->fieldMappings[$fieldName];
1083
            $fieldName = $mapping['name'];
1084 998
1085
            if (! $prepareValue) {
1086 998
                return [[$fieldName, $value]];
1087
            }
1088
1089
            // Prepare mapped, embedded objects
1090
            if (! empty($mapping['embedded']) && is_object($value) &&
1091 998
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1092 295
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1093 295
            }
1094
1095 295
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1096 88
                try {
1097
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1098
                } catch (MappingException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Persistence\Mapping\MappingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1099
                    // do nothing in case passed object is not mapped document
1100 218
                }
1101 218
            }
1102 3
1103
            // No further preparation unless we're dealing with a simple reference
1104
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) {
1105 216
                return [[$fieldName, $value]];
1106
            }
1107 15
1108 1
            // Additional preparation for one or more simple reference values
1109
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1110
1111
            if (! is_array($value)) {
1112
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1113
            }
1114 202
1115 133
            // Objects without operators or with DBRef fields can be converted immediately
1116
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1117
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1118
            }
1119 97
1120
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1121 97
        }
1122 91
1123
        // Process identifier fields
1124
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1125
            $fieldName = '_id';
1126 8
1127 3
            if (! $prepareValue) {
1128
                return [[$fieldName, $value]];
1129
            }
1130 8
1131
            if (! is_array($value)) {
1132
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1133
            }
1134 875
1135 365
            // Objects without operators or with DBRef fields can be converted immediately
1136
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1137 365
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1138 44
            }
1139
1140
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1141 324
        }
1142 296
1143
        // No processing for unmapped, non-identifier, non-dotted field names
1144
        if (strpos($fieldName, '.') === false) {
1145
            return [[$fieldName, $value]];
1146 63
        }
1147 6
1148
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1149
         *
1150 58
         * We can limit parsing here, since at most three segments are
1151
         * significant: "fieldName.objectProperty" with an optional index or key
1152
         * for collections stored as either BSON arrays or objects.
1153
         */
1154 613
        $e = explode('.', $fieldName, 4);
1155 465
1156
        // No further processing for unmapped fields
1157
        if (! isset($class->fieldMappings[$e[0]])) {
1158
            return [[$fieldName, $value]];
1159
        }
1160
1161
        $mapping = $class->fieldMappings[$e[0]];
1162
        $e[0]    = $mapping['name'];
1163
1164 160
        // Hash and raw fields will not be prepared beyond the field name
1165
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1166
            $fieldName = implode('.', $e);
1167 160
1168 6
            return [[$fieldName, $value]];
1169
        }
1170
1171 155
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1172 155
                && isset($e[2])) {
1173
            $objectProperty       = $e[2];
1174
            $objectPropertyPrefix = $e[1] . '.';
1175 155
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1176 1
        } elseif ($e[1] !== '$') {
1177
            $fieldName            = $e[0] . '.' . $e[1];
1178 1
            $objectProperty       = $e[1];
1179
            $objectPropertyPrefix = '';
1180
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1181 154
        } elseif (isset($e[2])) {
1182 154
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1183 1
            $objectProperty       = $e[2];
1184 1
            $objectPropertyPrefix = $e[1] . '.';
1185 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1186 153
        } else {
1187 152
            $fieldName = $e[0] . '.' . $e[1];
1188 152
1189 152
            return [[$fieldName, $value]];
1190 152
        }
1191 1
1192 1
        // No further processing for fields without a targetDocument mapping
1193 1
        if (! isset($mapping['targetDocument'])) {
1194 1
            if ($nextObjectProperty) {
1195 1
                $fieldName .= '.' . $nextObjectProperty;
1196
            }
1197 1
1198
            return [[$fieldName, $value]];
1199 1
        }
1200
1201
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1202
1203 154
        // No further processing for unmapped targetDocument fields
1204 5
        if (! $targetClass->hasField($objectProperty)) {
1205
            if ($nextObjectProperty) {
1206
                $fieldName .= '.' . $nextObjectProperty;
1207
            }
1208 5
1209
            return [[$fieldName, $value]];
1210
        }
1211 149
1212
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1213
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1214 149
1215 26
        // Prepare DBRef identifiers or the mapped field's property path
1216
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1217
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1218
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1219 26
1220
        // Process targetDocument identifier fields
1221
        if ($objectPropertyIsId) {
1222 128
            if (! $prepareValue) {
1223 128
                return [[$fieldName, $value]];
1224
            }
1225
1226 128
            if (! is_array($value)) {
1227 108
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1228 128
            }
1229
1230
            // Objects without operators or with DBRef fields can be converted immediately
1231 128
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1232 109
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1233 7
            }
1234
1235
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1236 102
        }
1237 88
1238
        /* The property path may include a third field segment, excluding the
1239
         * collection item pointer. If present, this next object property must
1240
         * be processed recursively.
1241 16
         */
1242 6
        if ($nextObjectProperty) {
1243
            // Respect the targetDocument's class metadata when recursing
1244
            $nextTargetClass = isset($targetMapping['targetDocument'])
1245 16
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1246
                : null;
1247
1248
            if (empty($targetMapping['reference'])) {
1249
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1250
            } else {
1251
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1252 19
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1253
                    $nextObjectProperty = '$' . $nextObjectProperty;
1254 16
                }
1255 10
                $fieldNames = [[$nextObjectProperty, $value]];
1256 16
            }
1257
1258 16
            return array_map(static function ($preparedTuple) use ($fieldName) {
1259 14
                [$key, $value] = $preparedTuple;
0 ignored issues
show
Bug introduced by
The variable $key does not exist. Did you forget to declare it?

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

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

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

Loading history...
1260
1261
                return [$fieldName . '.' . $key, $value];
1262 4
            }, $fieldNames);
1263 1
        }
1264
1265 4
        return [[$fieldName, $value]];
1266
    }
1267
1268
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1269 16
    {
1270
        foreach ($expression as $k => $v) {
1271 16
            // Ignore query operators whose arguments need no type conversion
1272 16
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1273
                continue;
1274
            }
1275 5
1276
            // Process query operators whose argument arrays need type conversion
1277
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1278 82
                foreach ($v as $k2 => $v2) {
1279
                    if ($v2 instanceof $class->name) {
1280 82
                        // If a value in a query is a target document, e.g. ['referenceField' => $targetDocument],
1281
                        // retreive id from target document and convert this id using it's type
1282 82
                        $expression[$k][$k2] = $class->getDatabaseIdentifierValue($class->getIdentifierValue($v2));
1283 16
1284
                        continue;
1285
                    }
1286
                    // Otherwise if a value in a query is already id, e.g. ['referenceField' => $targetDocumentId],
1287 82
                    // just convert id to it's database representation using it's type
1288 78
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1289 78
                }
1290
                continue;
1291
            }
1292 1
1293
            // Recursively process expressions within a $not operator
1294 1
            if ($k === '$not' && is_array($v)) {
1295
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1296
                continue;
1297
            }
1298 77
1299
            if ($v instanceof $class->name) {
1300 78
                $expression[$k] = $class->getDatabaseIdentifierValue($class->getIdentifierValue($v));
1301
            } else {
1302
                $expression[$k] = $class->getDatabaseIdentifierValue($v);
1303
            }
1304 20
        }
1305 15
1306 15
        return $expression;
1307
    }
1308
1309 20
    /**
1310 1
     * Checks whether the value has DBRef fields.
1311
     *
1312 19
     * This method doesn't check if the the value is a complete DBRef object,
1313
     * although it should return true for a DBRef. Rather, we're checking that
1314
     * the value has one or more fields for a DBref. In practice, this could be
1315
     * $elemMatch criteria for matching a DBRef.
1316 82
     *
1317
     * @param mixed $value
1318
     */
1319
    private function hasDBRefFields($value) : bool
1320
    {
1321
        if (! is_array($value) && ! is_object($value)) {
1322
            return false;
1323
        }
1324
1325
        if (is_object($value)) {
1326
            $value = get_object_vars($value);
1327
        }
1328
1329 83
        foreach ($value as $key => $_) {
1330
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1331 83
                return true;
1332
            }
1333
        }
1334
1335 83
        return false;
1336
    }
1337
1338
    /**
1339 83
     * Checks whether the value has query operators.
1340 83
     *
1341 4
     * @param mixed $value
1342
     */
1343
    private function hasQueryOperators($value) : bool
1344
    {
1345 82
        if (! is_array($value) && ! is_object($value)) {
1346
            return false;
1347
        }
1348
1349
        if (is_object($value)) {
1350
            $value = get_object_vars($value);
1351
        }
1352
1353 87
        foreach ($value as $key => $_) {
1354
            if (isset($key[0]) && $key[0] === '$') {
1355 87
                return true;
1356
            }
1357
        }
1358
1359 87
        return false;
1360
    }
1361
1362
    /**
1363 87
     * Returns the list of discriminator values for the given ClassMetadata
1364 87
     */
1365 83
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1366
    {
1367
        $discriminatorValues = [];
1368
1369 11
        if ($metadata->discriminatorValue !== null) {
1370
            $discriminatorValues[] = $metadata->discriminatorValue;
1371
        }
1372
1373
        foreach ($metadata->subClasses as $className) {
1374
            $key = array_search($className, $metadata->discriminatorMap);
1375 32
            if (! $key) {
1376
                continue;
1377 32
            }
1378
1379 32
            $discriminatorValues[] = $key;
1380 29
        }
1381
1382
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1383 32
        if ($metadata->defaultDiscriminatorValue && in_array($metadata->defaultDiscriminatorValue, $discriminatorValues)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->defaultDiscriminatorValue of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1384 12
            $discriminatorValues[] = null;
1385 12
        }
1386
1387
        return $discriminatorValues;
1388
    }
1389 12
1390
    private function handleCollections(object $document, array $options) : void
1391
    {
1392
        // Collection deletions (deletions of complete collections)
1393 32
        $collections = [];
1394 3
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1395
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1396
                continue;
1397 32
            }
1398
1399
            $collections[] = $coll;
1400 604
        }
1401
        if (! empty($collections)) {
1402
            $this->cp->delete($document, $collections, $options);
1403 604
        }
1404 604
        // Collection updates (deleteRows, updateRows, insertRows)
1405 114
        $collections = [];
1406 103
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1407
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1408
                continue;
1409 33
            }
1410
1411 604
            $collections[] = $coll;
1412 33
        }
1413
        if (! empty($collections)) {
1414
            $this->cp->update($document, $collections, $options);
1415 604
        }
1416 604
        // Take new snapshots from visited collections
1417 114
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1418 29
            $coll->takeSnapshot();
1419
        }
1420
    }
1421 106
1422
    /**
1423 604
     * If the document is new, ignore shard key field value, otherwise throw an
1424 106
     * exception. Also, shard key field should be present in actual document
1425
     * data.
1426
     *
1427 604
     * @throws MongoDBException
1428 255
     */
1429
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1430 604
    {
1431
        $dcs      = $this->uow->getDocumentChangeSet($document);
1432
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1433
1434
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1435
        $fieldName    = $fieldMapping['fieldName'];
1436
1437
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1438
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1439 10
        }
1440
1441 10
        if (! isset($actualDocumentData[$fieldName])) {
1442 10
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1443
        }
1444 10
    }
1445 10
1446
    /**
1447 10
     * Get shard key aware query for single document.
1448 2
     */
1449
    private function getQueryForDocument(object $document) : array
1450
    {
1451 8
        $id = $this->uow->getDocumentIdentifier($document);
1452
        $id = $this->class->getDatabaseIdentifierValue($id);
1453
1454 8
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1455
1456
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1457
    }
1458
1459 314
    private function getWriteOptions(array $options = []) : array
1460
    {
1461 314
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1462 314
        $documentOptions = [];
1463
        if ($this->class->hasWriteConcern()) {
1464 314
            $documentOptions['w'] = $this->class->getWriteConcern();
1465
        }
1466 312
1467
        return array_merge($defaultOptions, $documentOptions, $options);
1468
    }
1469 615
1470
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1471 615
    {
1472 615
        $reference = $this->dm->createReference($value, $mapping);
1473 615
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1474 9
            return [[$fieldName, $reference]];
1475
        }
1476
1477 615
        switch ($mapping['storeAs']) {
1478
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1479
                $keys = ['id' => true];
1480 16
                break;
1481
1482 16
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1483 15
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1484 9
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1485
1486
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1487 6
                    unset($keys['$db']);
1488
                }
1489
1490
                if (isset($mapping['targetDocument'])) {
1491
                    unset($keys['$ref'], $keys['$db']);
1492
                }
1493
                break;
1494 6
1495
            default:
1496 6
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1497 5
        }
1498
1499
        if ($mapping['type'] === 'many') {
1500 6
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1501 4
        }
1502
1503 6
        return array_map(
1504
            static function ($key) use ($reference, $fieldName) {
1505
                return [$fieldName . '.' . $key, $reference[$key]];
1506
            },
1507
            array_keys($keys)
1508
        );
1509 6
    }
1510
}
1511