Completed
Push — master ( 8fcee2...1aab9c )
by Andreas
37:27 queued 15:21
created

DocumentPersister::hasDBRefFields()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 9

Importance

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