Completed
Push — master ( 95608f...d78a07 )
by Andreas
14s queued 10s
created

loadReferenceManyWithRepositoryMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
421 1
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
422 1
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
423
424 1
            $this->bucket->delete($databaseIdentifier);
425
426 1
            return;
427
        }
428
429 35
        $query = $this->getQueryForDocument($document);
430
431 35
        if ($this->class->isLockable) {
432 2
            $query[$this->class->lockField] = ['$exists' => false];
433
        }
434
435 35
        $options = $this->getWriteOptions($options);
436
437 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...
438 35
        $result = $this->collection->deleteOne($query, $options);
439
440 35
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
441 2
            throw LockException::lockFailed($document);
442
        }
443 33
    }
444
445
    /**
446
     * Refreshes a managed document.
447
     */
448 23
    public function refresh(object $document) : void
449
    {
450 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...
451 23
        $query = $this->getQueryForDocument($document);
452 23
        $data  = $this->collection->findOne($query);
453 23
        if ($data === null) {
454
            throw MongoDBException::cannotRefreshDocument();
455
        }
456 23
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
457 23
        $this->uow->setOriginalDocumentData($document, $data);
458 23
    }
459
460
    /**
461
     * Finds a document by a set of criteria.
462
     *
463
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
464
     * be used to match an _id value.
465
     *
466
     * @param mixed $criteria Query criteria
467
     *
468
     * @throws LockException
469
     *
470
     * @todo Check identity map? loadById method? Try to guess whether
471
     *     $criteria is the id?
472
     */
473 366
    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...
474
    {
475
        // TODO: remove this
476 366
        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...
477
            $criteria = ['_id' => $criteria];
478
        }
479
480 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...
481 366
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
482 366
        $criteria = $this->addFilterToPreparedQuery($criteria);
483
484 366
        $options = [];
485 366
        if ($sort !== null) {
486 95
            $options['sort'] = $this->prepareSort($sort);
487
        }
488 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...
489 366
        $result = $this->collection->findOne($criteria, $options);
490 366
        $result = $result !== null ? (array) $result : null;
491
492 366
        if ($this->class->isLockable) {
493 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
494 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
495 1
                throw LockException::lockFailed($document);
496
            }
497
        }
498
499 365
        if ($result === null) {
500 115
            return null;
501
        }
502
503 321
        return $this->createDocument($result, $document, $hints);
504
    }
505
506
    /**
507
     * Finds documents by a set of criteria.
508
     */
509 24
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
510
    {
511 24
        $criteria = $this->prepareQueryOrNewObj($criteria);
512 24
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
513 24
        $criteria = $this->addFilterToPreparedQuery($criteria);
514
515 24
        $options = [];
516 24
        if ($sort !== null) {
517 11
            $options['sort'] = $this->prepareSort($sort);
518
        }
519
520 24
        if ($limit !== null) {
521 10
            $options['limit'] = $limit;
522
        }
523
524 24
        if ($skip !== null) {
525 1
            $options['skip'] = $skip;
526
        }
527
528 24
        assert($this->collection instanceof Collection);
0 ignored issues
show
Bug introduced by
The class MongoDB\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
529 24
        $baseCursor = $this->collection->find($criteria, $options);
530
531 24
        return $this->wrapCursor($baseCursor);
532
    }
533
534
    /**
535
     * @throws MongoDBException
536
     */
537 314
    private function getShardKeyQuery(object $document) : array
538
    {
539 314
        if (! $this->class->isSharded()) {
540 304
            return [];
541
        }
542
543 10
        $shardKey = $this->class->getShardKey();
544 10
        $keys     = array_keys($shardKey['keys']);
545 10
        $data     = $this->uow->getDocumentActualData($document);
546
547 10
        $shardKeyQueryPart = [];
548 10
        foreach ($keys as $key) {
549 10
            assert(is_string($key));
550 10
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
551 10
            $this->guardMissingShardKey($document, $key, $data);
552
553 8
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
554 1
                $reference = $this->prepareReference(
555 1
                    $key,
556 1
                    $data[$mapping['fieldName']],
557 1
                    $mapping,
558 1
                    false
559
                );
560 1
                foreach ($reference as $keyValue) {
561 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
562
                }
563
            } else {
564 7
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
565 7
                $shardKeyQueryPart[$key] = $value;
566
            }
567
        }
568
569 8
        return $shardKeyQueryPart;
570
    }
571
572
    /**
573
     * Wraps the supplied base cursor in the corresponding ODM class.
574
     */
575 24
    private function wrapCursor(Cursor $baseCursor) : Iterator
576
    {
577 24
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
578
    }
579
580
    /**
581
     * Checks whether the given managed document exists in the database.
582
     */
583 3
    public function exists(object $document) : bool
584
    {
585 3
        $id = $this->class->getIdentifierObject($document);
586 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...
587
588 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
589
    }
590
591
    /**
592
     * Locks document by storing the lock mode on the mapped lock field.
593
     */
594 5
    public function lock(object $document, int $lockMode) : void
595
    {
596 5
        $id          = $this->uow->getDocumentIdentifier($document);
597 5
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
598 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
599 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...
600 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
601 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
602 5
    }
603
604
    /**
605
     * Releases any lock that exists on this document.
606
     */
607 1
    public function unlock(object $document) : void
608
    {
609 1
        $id          = $this->uow->getDocumentIdentifier($document);
610 1
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
611 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
612 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...
613 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
614 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
615 1
    }
616
617
    /**
618
     * Creates or fills a single document object from an query result.
619
     *
620
     * @param array  $result   The query result.
621
     * @param object $document The document object to fill, if any.
622
     * @param array  $hints    Hints for document creation.
623
     *
624
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
625
     */
626 321
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
627
    {
628 321
        if ($document !== null) {
629 26
            $hints[Query::HINT_REFRESH] = true;
630 26
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
631 26
            $this->uow->registerManaged($document, $id, $result);
632
        }
633
634 321
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
635
    }
636
637
    /**
638
     * Loads a PersistentCollection data. Used in the initialize() method.
639
     */
640 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
641
    {
642 178
        $mapping = $collection->getMapping();
643 178
        switch ($mapping['association']) {
644
            case ClassMetadata::EMBED_MANY:
645 126
                $this->loadEmbedManyCollection($collection);
646 126
                break;
647
648
            case ClassMetadata::REFERENCE_MANY:
649 75
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
650 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
651
                } else {
652 71
                    if ($mapping['isOwningSide']) {
653 59
                        $this->loadReferenceManyCollectionOwningSide($collection);
654
                    } else {
655 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
656
                    }
657
                }
658 75
                break;
659
        }
660 178
    }
661
662 126
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
663
    {
664 126
        $embeddedDocuments = $collection->getMongoData();
665 126
        $mapping           = $collection->getMapping();
666 126
        $owner             = $collection->getOwner();
667 126
        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...
668 75
            return;
669
        }
670
671 97
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
672 97
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
673 97
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
674 97
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
675
676 97
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
677
678 97
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
679 97
            $id   = $data[$embeddedMetadata->identifier] ?? null;
680
681 97
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
682 96
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
683
            }
684 97
            if (CollectionHelper::isHash($mapping['strategy'])) {
685 25
                $collection->set($key, $embeddedDocumentObject);
686
            } else {
687 80
                $collection->add($embeddedDocumentObject);
688
            }
689
        }
690 97
    }
691
692 59
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
693
    {
694 59
        $hints      = $collection->getHints();
695 59
        $mapping    = $collection->getMapping();
696 59
        $groupedIds = [];
697
698 59
        $sorted = isset($mapping['sort']) && $mapping['sort'];
699
700 59
        foreach ($collection->getMongoData() as $key => $reference) {
701 53
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
702 53
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
703 53
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
704
705
            // create a reference to the class and id
706 53
            $reference = $this->dm->getReference($className, $id);
707
708
            // no custom sort so add the references right now in the order they are embedded
709 53
            if (! $sorted) {
710 52
                if (CollectionHelper::isHash($mapping['strategy'])) {
711 2
                    $collection->set($key, $reference);
712
                } else {
713 50
                    $collection->add($reference);
714
                }
715
            }
716
717
            // only query for the referenced object if it is not already initialized or the collection is sorted
718 53
            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...
719 22
                continue;
720
            }
721
722 38
            $groupedIds[$className][] = $identifier;
723
        }
724 59
        foreach ($groupedIds as $className => $ids) {
725 38
            $class           = $this->dm->getClassMetadata($className);
726 38
            $mongoCollection = $this->dm->getDocumentCollection($className);
727 38
            $criteria        = $this->cm->merge(
728 38
                ['_id' => ['$in' => array_values($ids)]],
729 38
                $this->dm->getFilterCollection()->getFilterCriteria($class),
730 38
                $mapping['criteria'] ?? []
731
            );
732 38
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
733
734 38
            $options = [];
735 38
            if (isset($mapping['sort'])) {
736 38
                $options['sort'] = $this->prepareSort($mapping['sort']);
737
            }
738 38
            if (isset($mapping['limit'])) {
739
                $options['limit'] = $mapping['limit'];
740
            }
741 38
            if (isset($mapping['skip'])) {
742
                $options['skip'] = $mapping['skip'];
743
            }
744 38
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
745
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
746
            }
747
748 38
            $cursor    = $mongoCollection->find($criteria, $options);
749 38
            $documents = $cursor->toArray();
750 38
            foreach ($documents as $documentData) {
751 37
                $document = $this->uow->getById($documentData['_id'], $class);
752 37
                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...
753 37
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
754 37
                    $this->uow->setOriginalDocumentData($document, $data);
755
                }
756
757 37
                if (! $sorted) {
758 36
                    continue;
759
                }
760
761 1
                $collection->add($document);
762
            }
763
        }
764 59
    }
765
766 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
767
    {
768 17
        $query    = $this->createReferenceManyInverseSideQuery($collection);
769 17
        $iterator = $query->execute();
770 17
        assert($iterator instanceof Iterator);
771 17
        $documents = $iterator->toArray();
772 17
        foreach ($documents as $key => $document) {
773 16
            $collection->add($document);
774
        }
775 17
    }
776
777 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
778
    {
779 17
        $hints   = $collection->getHints();
780 17
        $mapping = $collection->getMapping();
781 17
        $owner   = $collection->getOwner();
782
783 17
        if ($owner === null) {
784
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
785
        }
786
787 17
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
788 17
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
789 17
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
790 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
791
792 17
        $criteria = $this->cm->merge(
793 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
794 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
795 17
            $mapping['criteria'] ?? []
796
        );
797 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
798 17
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
799 17
            ->setQueryArray($criteria);
800
801 17
        if (isset($mapping['sort'])) {
802 17
            $qb->sort($mapping['sort']);
803
        }
804 17
        if (isset($mapping['limit'])) {
805 2
            $qb->limit($mapping['limit']);
806
        }
807 17
        if (isset($mapping['skip'])) {
808
            $qb->skip($mapping['skip']);
809
        }
810
811 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
812
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
813
        }
814
815 17
        foreach ($mapping['prime'] as $field) {
816 4
            $qb->field($field)->prime(true);
817
        }
818
819 17
        return $qb->getQuery();
820
    }
821
822 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
823
    {
824 5
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
825 5
        $mapping   = $collection->getMapping();
826 5
        $documents = $cursor->toArray();
827 5
        foreach ($documents as $key => $obj) {
828 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
829 1
                $collection->set($key, $obj);
830
            } else {
831 4
                $collection->add($obj);
832
            }
833
        }
834 5
    }
835
836 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
837
    {
838 5
        $mapping          = $collection->getMapping();
839 5
        $repositoryMethod = $mapping['repositoryMethod'];
840 5
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
841 5
            ->$repositoryMethod($collection->getOwner());
842
843 5
        if (! $cursor instanceof Iterator) {
844
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
845
        }
846
847 5
        if (! empty($mapping['prime'])) {
848 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
849 1
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
850 1
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
851
852 1
            assert(is_array($primers));
853
854 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
855
        }
856
857 5
        return $cursor;
858
    }
859
860
    /**
861
     * Prepare a projection array by converting keys, which are PHP property
862
     * names, to MongoDB field names.
863
     */
864 15
    public function prepareProjection(array $fields) : array
865
    {
866 15
        $preparedFields = [];
867
868 15
        foreach ($fields as $key => $value) {
869 15
            $preparedFields[$this->prepareFieldName($key)] = $value;
870
        }
871
872 15
        return $preparedFields;
873
    }
874
875
    /**
876
     * @param int|string $sort
877
     *
878
     * @return int|string|null
879
     */
880 26
    private function getSortDirection($sort)
881
    {
882 26
        switch (strtolower((string) $sort)) {
883 26
            case 'desc':
884 15
                return -1;
885 23
            case 'asc':
886 13
                return 1;
887
        }
888
889 13
        return $sort;
890
    }
891
892
    /**
893
     * Prepare a sort specification array by converting keys to MongoDB field
894
     * names and changing direction strings to int.
895
     */
896 142
    public function prepareSort(array $fields) : array
897
    {
898 142
        $sortFields = [];
899
900 142
        foreach ($fields as $key => $value) {
901 26
            if (is_array($value)) {
902 1
                $sortFields[$this->prepareFieldName($key)] = $value;
903
            } else {
904 26
                $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
905
            }
906
        }
907
908 142
        return $sortFields;
909
    }
910
911
    /**
912
     * Prepare a mongodb field name and convert the PHP property names to
913
     * MongoDB field names.
914
     */
915 462
    public function prepareFieldName(string $fieldName) : string
916
    {
917 462
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
918
919 462
        return $fieldNames[0][0];
920
    }
921
922
    /**
923
     * Adds discriminator criteria to an already-prepared query.
924
     *
925
     * If the class we're querying has a discriminator field set, we add all
926
     * possible discriminator values to the query. The list of possible
927
     * discriminator values is based on the discriminatorValue of the class
928
     * itself as well as those of all its subclasses.
929
     *
930
     * This method should be used once for query criteria and not be used for
931
     * nested expressions. It should be called before
932
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
933
     */
934 523
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
935
    {
936 523
        if (isset($preparedQuery[$this->class->discriminatorField]) || $this->class->discriminatorField === null) {
937 500
            return $preparedQuery;
938
        }
939
940 32
        $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
941
942 32
        if ($discriminatorValues === []) {
943 1
            return $preparedQuery;
944
        }
945
946 32
        if (count($discriminatorValues) === 1) {
947 21
            $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
948
        } else {
949 14
            $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
950
        }
951
952 32
        return $preparedQuery;
953
    }
954
955
    /**
956
     * Adds filter criteria to an already-prepared query.
957
     *
958
     * This method should be used once for query criteria and not be used for
959
     * nested expressions. It should be called after
960
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
961
     */
962 524
    public function addFilterToPreparedQuery(array $preparedQuery) : array
963
    {
964
        /* If filter criteria exists for this class, prepare it and merge
965
         * over the existing query.
966
         *
967
         * @todo Consider recursive merging in case the filter criteria and
968
         * prepared query both contain top-level $and/$or operators.
969
         */
970 524
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
971 524
        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...
972 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
973
        }
974
975 524
        return $preparedQuery;
976
    }
977
978
    /**
979
     * Prepares the query criteria or new document object.
980
     *
981
     * PHP field names and types will be converted to those used by MongoDB.
982
     */
983 599
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
984
    {
985 599
        $preparedQuery = [];
986
987 599
        foreach ($query as $key => $value) {
988
            // Recursively prepare logical query clauses
989 555
            if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) {
990 20
                foreach ($value as $k2 => $v2) {
991 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
992
                }
993 20
                continue;
994
            }
995
996 555
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
997 74
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
998 74
                continue;
999
            }
1000
1001 555
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1002 555
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1003 555
                $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...
1004 153
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1005 504
                    : Type::convertPHPToDatabaseValue($preparedValue);
1006
            }
1007
        }
1008
1009 599
        return $preparedQuery;
1010
    }
1011
1012
    /**
1013
     * Prepares a query value and converts the PHP value to the database value
1014
     * if it is an identifier.
1015
     *
1016
     * It also handles converting $fieldName to the database name if they are
1017
     * different.
1018
     *
1019
     * @param mixed $value
1020
     */
1021 977
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1022
    {
1023 977
        $class = $class ?? $this->class;
1024
1025
        // @todo Consider inlining calls to ClassMetadata methods
1026
1027
        // Process all non-identifier fields by translating field names
1028 977
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1029 281
            $mapping   = $class->fieldMappings[$fieldName];
1030 281
            $fieldName = $mapping['name'];
1031
1032 281
            if (! $prepareValue) {
1033 77
                return [[$fieldName, $value]];
1034
            }
1035
1036
            // Prepare mapped, embedded objects
1037 214
            if (! empty($mapping['embedded']) && is_object($value) &&
1038 214
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1039 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1040
            }
1041
1042 212
            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...
1043
                try {
1044 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1045 1
                } 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...
1046
                    // do nothing in case passed object is not mapped document
1047
                }
1048
            }
1049
1050
            // No further preparation unless we're dealing with a simple reference
1051 199
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) {
1052 132
                return [[$fieldName, $value]];
1053
            }
1054
1055
            // Additional preparation for one or more simple reference values
1056 94
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1057
1058 94
            if (! is_array($value)) {
1059 90
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1060
            }
1061
1062
            // Objects without operators or with DBRef fields can be converted immediately
1063 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1064 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1065
            }
1066
1067 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1068
        }
1069
1070
        // Process identifier fields
1071 867
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1072 357
            $fieldName = '_id';
1073
1074 357
            if (! $prepareValue) {
1075 42
                return [[$fieldName, $value]];
1076
            }
1077
1078 318
            if (! is_array($value)) {
1079 292
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1080
            }
1081
1082
            // Objects without operators or with DBRef fields can be converted immediately
1083 60
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1084 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1085
            }
1086
1087 55
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1088
        }
1089
1090
        // No processing for unmapped, non-identifier, non-dotted field names
1091 611
        if (strpos($fieldName, '.') === false) {
1092 463
            return [[$fieldName, $value]];
1093
        }
1094
1095
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1096
         *
1097
         * We can limit parsing here, since at most three segments are
1098
         * significant: "fieldName.objectProperty" with an optional index or key
1099
         * for collections stored as either BSON arrays or objects.
1100
         */
1101 160
        $e = explode('.', $fieldName, 4);
1102
1103
        // No further processing for unmapped fields
1104 160
        if (! isset($class->fieldMappings[$e[0]])) {
1105 6
            return [[$fieldName, $value]];
1106
        }
1107
1108 155
        $mapping = $class->fieldMappings[$e[0]];
1109 155
        $e[0]    = $mapping['name'];
1110
1111
        // Hash and raw fields will not be prepared beyond the field name
1112 155
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1113 1
            $fieldName = implode('.', $e);
1114
1115 1
            return [[$fieldName, $value]];
1116
        }
1117
1118 154
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1119 154
                && isset($e[2])) {
1120 1
            $objectProperty       = $e[2];
1121 1
            $objectPropertyPrefix = $e[1] . '.';
1122 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1123 153
        } elseif ($e[1] !== '$') {
1124 152
            $fieldName            = $e[0] . '.' . $e[1];
1125 152
            $objectProperty       = $e[1];
1126 152
            $objectPropertyPrefix = '';
1127 152
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1128 1
        } elseif (isset($e[2])) {
1129 1
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1130 1
            $objectProperty       = $e[2];
1131 1
            $objectPropertyPrefix = $e[1] . '.';
1132 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1133
        } else {
1134 1
            $fieldName = $e[0] . '.' . $e[1];
1135
1136 1
            return [[$fieldName, $value]];
1137
        }
1138
1139
        // No further processing for fields without a targetDocument mapping
1140 154
        if (! isset($mapping['targetDocument'])) {
1141 5
            if ($nextObjectProperty) {
1142
                $fieldName .= '.' . $nextObjectProperty;
1143
            }
1144
1145 5
            return [[$fieldName, $value]];
1146
        }
1147
1148 149
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1149
1150
        // No further processing for unmapped targetDocument fields
1151 149
        if (! $targetClass->hasField($objectProperty)) {
1152 26
            if ($nextObjectProperty) {
1153
                $fieldName .= '.' . $nextObjectProperty;
1154
            }
1155
1156 26
            return [[$fieldName, $value]];
1157
        }
1158
1159 128
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1160 128
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1161
1162
        // Prepare DBRef identifiers or the mapped field's property path
1163 128
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1164 108
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1165 128
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1166
1167
        // Process targetDocument identifier fields
1168 128
        if ($objectPropertyIsId) {
1169 109
            if (! $prepareValue) {
1170 7
                return [[$fieldName, $value]];
1171
            }
1172
1173 102
            if (! is_array($value)) {
1174 88
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1175
            }
1176
1177
            // Objects without operators or with DBRef fields can be converted immediately
1178 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1179 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1180
            }
1181
1182 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1183
        }
1184
1185
        /* The property path may include a third field segment, excluding the
1186
         * collection item pointer. If present, this next object property must
1187
         * be processed recursively.
1188
         */
1189 19
        if ($nextObjectProperty) {
1190
            // Respect the targetDocument's class metadata when recursing
1191 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1192 10
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1193 16
                : null;
1194
1195 16
            if (empty($targetMapping['reference'])) {
1196 14
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1197
            } else {
1198
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1199 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1200 1
                    $nextObjectProperty = '$' . $nextObjectProperty;
1201
                }
1202 4
                $fieldNames = [[$nextObjectProperty, $value]];
1203
            }
1204
1205
            return array_map(static function ($preparedTuple) use ($fieldName) {
1206 16
                [$key, $value] = $preparedTuple;
0 ignored issues
show
Bug introduced by
The variable $key does not exist. Did you forget to declare it?

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

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

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

Loading history...
1207
1208 16
                return [$fieldName . '.' . $key, $value];
1209 16
            }, $fieldNames);
1210
        }
1211
1212 5
        return [[$fieldName, $value]];
1213
    }
1214
1215 77
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1216
    {
1217 77
        foreach ($expression as $k => $v) {
1218
            // Ignore query operators whose arguments need no type conversion
1219 77
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1220 16
                continue;
1221
            }
1222
1223
            // Process query operators whose argument arrays need type conversion
1224 77
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1225 75
                foreach ($v as $k2 => $v2) {
1226 75
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1227
                }
1228 75
                continue;
1229
            }
1230
1231
            // Recursively process expressions within a $not operator
1232 18
            if ($k === '$not' && is_array($v)) {
1233 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1234 15
                continue;
1235
            }
1236
1237 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1238
        }
1239
1240 77
        return $expression;
1241
    }
1242
1243
    /**
1244
     * Checks whether the value has DBRef fields.
1245
     *
1246
     * This method doesn't check if the the value is a complete DBRef object,
1247
     * although it should return true for a DBRef. Rather, we're checking that
1248
     * the value has one or more fields for a DBref. In practice, this could be
1249
     * $elemMatch criteria for matching a DBRef.
1250
     *
1251
     * @param mixed $value
1252
     */
1253 78
    private function hasDBRefFields($value) : bool
1254
    {
1255 78
        if (! is_array($value) && ! is_object($value)) {
1256
            return false;
1257
        }
1258
1259 78
        if (is_object($value)) {
1260
            $value = get_object_vars($value);
1261
        }
1262
1263 78
        foreach ($value as $key => $_) {
1264 78
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1265 4
                return true;
1266
            }
1267
        }
1268
1269 77
        return false;
1270
    }
1271
1272
    /**
1273
     * Checks whether the value has query operators.
1274
     *
1275
     * @param mixed $value
1276
     */
1277 82
    private function hasQueryOperators($value) : bool
1278
    {
1279 82
        if (! is_array($value) && ! is_object($value)) {
1280
            return false;
1281
        }
1282
1283 82
        if (is_object($value)) {
1284
            $value = get_object_vars($value);
1285
        }
1286
1287 82
        foreach ($value as $key => $_) {
1288 82
            if (isset($key[0]) && $key[0] === '$') {
1289 78
                return true;
1290
            }
1291
        }
1292
1293 11
        return false;
1294
    }
1295
1296
    /**
1297
     * Returns the list of discriminator values for the given ClassMetadata
1298
     */
1299 32
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1300
    {
1301 32
        $discriminatorValues = [];
1302
1303 32
        if ($metadata->discriminatorValue !== null) {
1304 29
            $discriminatorValues[] = $metadata->discriminatorValue;
1305
        }
1306
1307 32
        foreach ($metadata->subClasses as $className) {
1308 12
            $key = array_search($className, $metadata->discriminatorMap);
1309 12
            if (! $key) {
1310
                continue;
1311
            }
1312
1313 12
            $discriminatorValues[] = $key;
1314
        }
1315
1316
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1317 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...
1318 3
            $discriminatorValues[] = null;
1319
        }
1320
1321 32
        return $discriminatorValues;
1322
    }
1323
1324 595
    private function handleCollections(object $document, array $options) : void
1325
    {
1326
        // Collection deletions (deletions of complete collections)
1327 595
        $collections = [];
1328 595
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1329 113
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1330 102
                continue;
1331
            }
1332
1333 33
            $collections[] = $coll;
1334
        }
1335 595
        if (! empty($collections)) {
1336 33
            $this->cp->delete($document, $collections, $options);
1337
        }
1338
        // Collection updates (deleteRows, updateRows, insertRows)
1339 595
        $collections = [];
1340 595
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1341 113
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1342 29
                continue;
1343
            }
1344
1345 105
            $collections[] = $coll;
1346
        }
1347 595
        if (! empty($collections)) {
1348 105
            $this->cp->update($document, $collections, $options);
1349
        }
1350
        // Take new snapshots from visited collections
1351 595
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1352 252
            $coll->takeSnapshot();
1353
        }
1354 595
    }
1355
1356
    /**
1357
     * If the document is new, ignore shard key field value, otherwise throw an
1358
     * exception. Also, shard key field should be present in actual document
1359
     * data.
1360
     *
1361
     * @throws MongoDBException
1362
     */
1363 10
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1364
    {
1365 10
        $dcs      = $this->uow->getDocumentChangeSet($document);
1366 10
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1367
1368 10
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1369 10
        $fieldName    = $fieldMapping['fieldName'];
1370
1371 10
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1372 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1373
        }
1374
1375 8
        if (! isset($actualDocumentData[$fieldName])) {
1376
            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...
1377
        }
1378 8
    }
1379
1380
    /**
1381
     * Get shard key aware query for single document.
1382
     */
1383 310
    private function getQueryForDocument(object $document) : array
1384
    {
1385 310
        $id = $this->uow->getDocumentIdentifier($document);
1386 310
        $id = $this->class->getDatabaseIdentifierValue($id);
1387
1388 310
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1389
1390 308
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1391
    }
1392
1393 606
    private function getWriteOptions(array $options = []) : array
1394
    {
1395 606
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1396 606
        $documentOptions = [];
1397 606
        if ($this->class->hasWriteConcern()) {
1398 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1399
        }
1400
1401 606
        return array_merge($defaultOptions, $documentOptions, $options);
1402
    }
1403
1404 15
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1405
    {
1406 15
        $reference = $this->dm->createReference($value, $mapping);
1407 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1408 8
            return [[$fieldName, $reference]];
1409
        }
1410
1411 6
        switch ($mapping['storeAs']) {
1412
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1413
                $keys = ['id' => true];
1414
                break;
1415
1416
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1417
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1418 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1419
1420 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1421 5
                    unset($keys['$db']);
1422
                }
1423
1424 6
                if (isset($mapping['targetDocument'])) {
1425 4
                    unset($keys['$ref'], $keys['$db']);
1426
                }
1427 6
                break;
1428
1429
            default:
1430
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1431
        }
1432
1433 6
        if ($mapping['type'] === 'many') {
1434 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1435
        }
1436
1437 4
        return array_map(
1438
            static function ($key) use ($reference, $fieldName) {
1439 4
                return [$fieldName . '.' . $key, $reference[$key]];
1440 4
            },
1441 4
            array_keys($keys)
1442
        );
1443
    }
1444
}
1445