Completed
Push — master ( b90edc...95608f )
by Andreas
14:15 queued 11s
created

DocumentPersister::getWriteOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0185
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 562
    public function __construct(
112
        PersistenceBuilder $pb,
113
        DocumentManager $dm,
114
        UnitOfWork $uow,
115
        HydratorFactory $hydratorFactory,
116
        ClassMetadata $class,
117
        ?CriteriaMerger $cm = null
118
    ) {
119 562
        $this->pb              = $pb;
120 562
        $this->dm              = $dm;
121 562
        $this->cm              = $cm ?: new CriteriaMerger();
122 562
        $this->uow             = $uow;
123 562
        $this->hydratorFactory = $hydratorFactory;
124 562
        $this->class           = $class;
125 562
        $this->cp              = $this->uow->getCollectionPersister();
126
127 562
        if ($class->isEmbeddedDocument || $class->isQueryResultDocument) {
128 18
            return;
129
        }
130
131 559
        $this->collection = $dm->getDocumentCollection($class->name);
132
133 559
        if (! $class->isFile) {
134 554
            return;
135
        }
136
137 10
        $this->bucket = $dm->getDocumentBucket($class->name);
138 10
    }
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 22
    public function addInsert(object $document) : void
155
    {
156 22
        $this->queuedInserts[spl_object_hash($document)] = $document;
157 22
    }
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 9
    public function addUpsert(object $document) : void
174
    {
175 9
        $this->queuedUpserts[spl_object_hash($document)] = $document;
176 9
    }
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 22
    public function executeInserts(array $options = []) : void
198
    {
199 22
        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 22
        $inserts = [];
204 22
        $options = $this->getWriteOptions($options);
205 22
        foreach ($this->queuedInserts as $oid => $document) {
206 22
            $data = $this->pb->prepareInsertData($document);
207
208
            // Set the initial version for each insert
209 11
            if ($this->class->isVersioned) {
210 3
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
211 3
                $nextVersion    = null;
212 3
                if ($versionMapping['type'] === 'int') {
213 2
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
214 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
215 1
                } elseif ($versionMapping['type'] === 'date') {
216 1
                    $nextVersionDateTime = new DateTime();
217 1
                    $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
218 1
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
219
                }
220 3
                $data[$versionMapping['name']] = $nextVersion;
221
            }
222
223 11
            $inserts[] = $data;
224
        }
225
226 11
        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 11
                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 11
                $this->collection->insertMany($inserts, $options);
230 11
            } 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 11
                $this->queuedInserts = [];
232 11
                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
        foreach ($this->queuedInserts as $document) {
241
            $this->handleCollections($document, $options);
242
        }
243
244
        $this->queuedInserts = [];
245
    }
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 9
    public function executeUpserts(array $options = []) : void
255
    {
256 9
        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 9
        $options = $this->getWriteOptions($options);
261 9
        foreach ($this->queuedUpserts as $oid => $document) {
262
            try {
263 9
                $this->executeUpsert($document, $options);
264
                $this->handleCollections($document, $options);
265
                unset($this->queuedUpserts[$oid]);
266 9
            } 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
    }
272
273
    /**
274
     * Executes a single upsert in {@link executeUpserts}
275
     */
276 9
    private function executeUpsert(object $document, array $options) : void
277
    {
278 9
        $options['upsert'] = true;
279 9
        $criteria          = $this->getQueryForDocument($document);
280
281 9
        $data = $this->pb->prepareUpsertData($document);
282
283
        // Set the initial version for each upsert
284 9
        if ($this->class->isVersioned) {
285 1
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
286 1
            $nextVersion    = null;
287 1
            if ($versionMapping['type'] === 'int') {
288
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
289
                $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 1
            $data['$set'][$versionMapping['name']] = $nextVersion;
296
        }
297
298 9
        foreach (array_keys($criteria) as $field) {
299 9
            unset($data['$set'][$field]);
300 9
            unset($data['$inc'][$field]);
301 9
            unset($data['$setOnInsert'][$field]);
302
        }
303
304
        // Do not send empty update operators
305 9
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
306 9
            if (! empty($data[$operator])) {
307 6
                continue;
308
            }
309
310 9
            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 9
        if (empty($data)) {
327 3
            $retry = true;
328 3
            $data  = ['$set' => ['_id' => $criteria['_id']]];
329
        }
330
331
        try {
332 9
            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 9
            $this->collection->updateOne($criteria, $data, $options);
334
335
            return;
336 9
        } 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
    public function update(object $document, array $options = []) : void
352
    {
353
        $update = $this->pb->prepareUpdateData($document);
354
355
        $query = $this->getQueryForDocument($document);
356
357
        foreach (array_keys($query) as $field) {
358
            unset($update['$set'][$field]);
359
        }
360
361
        if (empty($update['$set'])) {
362
            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
        $nextVersion = null;
369
        if ($this->class->isVersioned) {
370
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
371
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
372
            if ($versionMapping['type'] === 'int') {
373
                $nextVersion                             = $currentVersion + 1;
374
                $update['$inc'][$versionMapping['name']] = 1;
375
                $query[$versionMapping['name']]          = $currentVersion;
376
            } elseif ($versionMapping['type'] === 'date') {
377
                $nextVersion                             = new DateTime();
378
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
379
                $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
380
            }
381
        }
382
383
        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
            if ($this->class->isLockable) {
387
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
388
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
389
                if ($isLocked) {
390
                    $update['$unset'] = [$lockMapping['name'] => true];
391
                } else {
392
                    $query[$lockMapping['name']] = ['$exists' => false];
393
                }
394
            }
395
396
            $options = $this->getWriteOptions($options);
397
398
            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
            $result = $this->collection->updateOne($query, $update, $options);
400
401
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
402
                throw LockException::lockFailed($document);
403
            }
404
405
            if ($this->class->isVersioned) {
406
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
407
            }
408
        }
409
410
        $this->handleCollections($document, $options);
411
    }
412
413
    /**
414
     * Removes document from mongo
415
     *
416
     * @throws LockException
417
     */
418
    public function delete(object $document, array $options = []) : void
419
    {
420
        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
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
422
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
423
424
            $this->bucket->delete($databaseIdentifier);
425
426
            return;
427
        }
428
429
        $query = $this->getQueryForDocument($document);
430
431
        if ($this->class->isLockable) {
432
            $query[$this->class->lockField] = ['$exists' => false];
433
        }
434
435
        $options = $this->getWriteOptions($options);
436
437
        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
        $result = $this->collection->deleteOne($query, $options);
439
440
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
441
            throw LockException::lockFailed($document);
442
        }
443
    }
444
445
    /**
446
     * Refreshes a managed document.
447
     */
448
    public function refresh(object $document) : void
449
    {
450
        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
        $query = $this->getQueryForDocument($document);
452
        $data  = $this->collection->findOne($query);
453
        if ($data === null) {
454
            throw MongoDBException::cannotRefreshDocument();
455
        }
456
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
457
        $this->uow->setOriginalDocumentData($document, $data);
458
    }
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 2
    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 2
        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 2
        $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 2
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
482 2
        $criteria = $this->addFilterToPreparedQuery($criteria);
483
484 2
        $options = [];
485 2
        if ($sort !== null) {
486 1
            $options['sort'] = $this->prepareSort($sort);
487
        }
488 2
        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 2
        $result = $this->collection->findOne($criteria, $options);
490
        $result = $result !== null ? (array) $result : null;
491
492
        if ($this->class->isLockable) {
493
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
494
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
495
                throw LockException::lockFailed($document);
496
            }
497
        }
498
499
        if ($result === null) {
500
            return null;
501
        }
502
503
        return $this->createDocument($result, $document, $hints);
504
    }
505
506
    /**
507
     * Finds documents by a set of criteria.
508
     */
509
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
510
    {
511
        $criteria = $this->prepareQueryOrNewObj($criteria);
512
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
513
        $criteria = $this->addFilterToPreparedQuery($criteria);
514
515
        $options = [];
516
        if ($sort !== null) {
517
            $options['sort'] = $this->prepareSort($sort);
518
        }
519
520
        if ($limit !== null) {
521
            $options['limit'] = $limit;
522
        }
523
524
        if ($skip !== null) {
525
            $options['skip'] = $skip;
526
        }
527
528
        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
        $baseCursor = $this->collection->find($criteria, $options);
530
531
        return $this->wrapCursor($baseCursor);
532
    }
533
534
    /**
535
     * @throws MongoDBException
536
     */
537 13
    private function getShardKeyQuery(object $document) : array
538
    {
539 13
        if (! $this->class->isSharded()) {
540 9
            return [];
541
        }
542
543 4
        $shardKey = $this->class->getShardKey();
544 4
        $keys     = array_keys($shardKey['keys']);
545 4
        $data     = $this->uow->getDocumentActualData($document);
546
547 4
        $shardKeyQueryPart = [];
548 4
        foreach ($keys as $key) {
549 4
            assert(is_string($key));
550 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
551 4
            $this->guardMissingShardKey($document, $key, $data);
552
553 4
            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 3
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
565 3
                $shardKeyQueryPart[$key] = $value;
566
            }
567
        }
568
569 4
        return $shardKeyQueryPart;
570
    }
571
572
    /**
573
     * Wraps the supplied base cursor in the corresponding ODM class.
574
     */
575
    private function wrapCursor(Cursor $baseCursor) : Iterator
576
    {
577
        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
    public function exists(object $document) : bool
584
    {
585
        $id = $this->class->getIdentifierObject($document);
586
        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
        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
    public function lock(object $document, int $lockMode) : void
595
    {
596
        $id          = $this->uow->getDocumentIdentifier($document);
597
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
598
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
599
        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
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
601
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
602
    }
603
604
    /**
605
     * Releases any lock that exists on this document.
606
     */
607
    public function unlock(object $document) : void
608
    {
609
        $id          = $this->uow->getDocumentIdentifier($document);
610
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
611
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
612
        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
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
614
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
615
    }
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
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
627
    {
628
        if ($document !== null) {
629
            $hints[Query::HINT_REFRESH] = true;
630
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
631
            $this->uow->registerManaged($document, $id, $result);
632
        }
633
634
        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 3
    public function loadCollection(PersistentCollectionInterface $collection) : void
641
    {
642 3
        $mapping = $collection->getMapping();
643 3
        switch ($mapping['association']) {
644
            case ClassMetadata::EMBED_MANY:
645 2
                $this->loadEmbedManyCollection($collection);
646 2
                break;
647
648
            case ClassMetadata::REFERENCE_MANY:
649 1
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
650
                    $this->loadReferenceManyWithRepositoryMethod($collection);
651
                } else {
652 1
                    if ($mapping['isOwningSide']) {
653 1
                        $this->loadReferenceManyCollectionOwningSide($collection);
654
                    } else {
655
                        $this->loadReferenceManyCollectionInverseSide($collection);
656
                    }
657
                }
658
                break;
659
        }
660 2
    }
661
662 2
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
663
    {
664 2
        $embeddedDocuments = $collection->getMongoData();
665 2
        $mapping           = $collection->getMapping();
666 2
        $owner             = $collection->getOwner();
667 2
        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
            return;
669
        }
670
671 2
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
672 2
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
673 2
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
674 2
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
675
676 2
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
677
678 2
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
679 2
            $id   = $data[$embeddedMetadata->identifier] ?? null;
680
681 2
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
682
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
683
            }
684 2
            if (CollectionHelper::isHash($mapping['strategy'])) {
685
                $collection->set($key, $embeddedDocumentObject);
686
            } else {
687 2
                $collection->add($embeddedDocumentObject);
688
            }
689
        }
690 2
    }
691
692 1
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
693
    {
694 1
        $hints      = $collection->getHints();
695 1
        $mapping    = $collection->getMapping();
696 1
        $groupedIds = [];
697
698 1
        $sorted = isset($mapping['sort']) && $mapping['sort'];
699
700 1
        foreach ($collection->getMongoData() as $key => $reference) {
701 1
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
702 1
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
703 1
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
704
705
            // create a reference to the class and id
706 1
            $reference = $this->dm->getReference($className, $id);
707
708
            // no custom sort so add the references right now in the order they are embedded
709 1
            if (! $sorted) {
710 1
                if (CollectionHelper::isHash($mapping['strategy'])) {
711
                    $collection->set($key, $reference);
712
                } else {
713 1
                    $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 1
            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
                continue;
720
            }
721
722 1
            $groupedIds[$className][] = $identifier;
723
        }
724 1
        foreach ($groupedIds as $className => $ids) {
725 1
            $class           = $this->dm->getClassMetadata($className);
726 1
            $mongoCollection = $this->dm->getDocumentCollection($className);
727 1
            $criteria        = $this->cm->merge(
728 1
                ['_id' => ['$in' => array_values($ids)]],
729 1
                $this->dm->getFilterCollection()->getFilterCriteria($class),
730 1
                $mapping['criteria'] ?? []
731
            );
732 1
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
733
734 1
            $options = [];
735 1
            if (isset($mapping['sort'])) {
736 1
                $options['sort'] = $this->prepareSort($mapping['sort']);
737
            }
738 1
            if (isset($mapping['limit'])) {
739
                $options['limit'] = $mapping['limit'];
740
            }
741 1
            if (isset($mapping['skip'])) {
742
                $options['skip'] = $mapping['skip'];
743
            }
744 1
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
745
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
746
            }
747
748 1
            $cursor    = $mongoCollection->find($criteria, $options);
749
            $documents = $cursor->toArray();
750
            foreach ($documents as $documentData) {
751
                $document = $this->uow->getById($documentData['_id'], $class);
752
                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
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
754
                    $this->uow->setOriginalDocumentData($document, $data);
755
                }
756
757
                if (! $sorted) {
758
                    continue;
759
                }
760
761
                $collection->add($document);
762
            }
763
        }
764
    }
765
766
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
767
    {
768
        $query    = $this->createReferenceManyInverseSideQuery($collection);
769
        $iterator = $query->execute();
770
        assert($iterator instanceof Iterator);
771
        $documents = $iterator->toArray();
772
        foreach ($documents as $key => $document) {
773
            $collection->add($document);
774
        }
775
    }
776
777
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
778
    {
779
        $hints   = $collection->getHints();
780
        $mapping = $collection->getMapping();
781
        $owner   = $collection->getOwner();
782
783
        if ($owner === null) {
784
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
785
        }
786
787
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
788
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
789
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
790
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
791
792
        $criteria = $this->cm->merge(
793
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
794
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
795
            $mapping['criteria'] ?? []
796
        );
797
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
798
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
799
            ->setQueryArray($criteria);
800
801
        if (isset($mapping['sort'])) {
802
            $qb->sort($mapping['sort']);
803
        }
804
        if (isset($mapping['limit'])) {
805
            $qb->limit($mapping['limit']);
806
        }
807
        if (isset($mapping['skip'])) {
808
            $qb->skip($mapping['skip']);
809
        }
810
811
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
812
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
813
        }
814
815
        foreach ($mapping['prime'] as $field) {
816
            $qb->field($field)->prime(true);
817
        }
818
819
        return $qb->getQuery();
820
    }
821
822
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
823
    {
824
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
825
        $mapping   = $collection->getMapping();
826
        $documents = $cursor->toArray();
827
        foreach ($documents as $key => $obj) {
828
            if (CollectionHelper::isHash($mapping['strategy'])) {
829
                $collection->set($key, $obj);
830
            } else {
831
                $collection->add($obj);
832
            }
833
        }
834
    }
835
836
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
837
    {
838
        $mapping          = $collection->getMapping();
839
        $repositoryMethod = $mapping['repositoryMethod'];
840
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
841
            ->$repositoryMethod($collection->getOwner());
842
843
        if (! $cursor instanceof Iterator) {
844
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
845
        }
846
847
        if (! empty($mapping['prime'])) {
848
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
849
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
850
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
851
852
            assert(is_array($primers));
853
854
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
855
        }
856
857
        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 10
    public function prepareProjection(array $fields) : array
865
    {
866 10
        $preparedFields = [];
867
868 10
        foreach ($fields as $key => $value) {
869 10
            $preparedFields[$this->prepareFieldName($key)] = $value;
870
        }
871
872 10
        return $preparedFields;
873
    }
874
875
    /**
876
     * @param int|string $sort
877
     *
878
     * @return int|string|null
879
     */
880 8
    private function getSortDirection($sort)
881
    {
882 8
        switch (strtolower((string) $sort)) {
883 8
            case 'desc':
884 4
                return -1;
885 6
            case 'asc':
886 3
                return 1;
887
        }
888
889 4
        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 10
    public function prepareSort(array $fields) : array
897
    {
898 10
        $sortFields = [];
899
900 10
        foreach ($fields as $key => $value) {
901 8
            if (is_array($value)) {
902 1
                $sortFields[$this->prepareFieldName($key)] = $value;
903
            } else {
904 8
                $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
905
            }
906
        }
907
908 10
        return $sortFields;
909
    }
910
911
    /**
912
     * Prepare a mongodb field name and convert the PHP property names to
913
     * MongoDB field names.
914
     */
915 398
    public function prepareFieldName(string $fieldName) : string
916
    {
917 398
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
918
919 398
        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 67
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
935
    {
936 67
        if (isset($preparedQuery[$this->class->discriminatorField]) || $this->class->discriminatorField === null) {
937 56
            return $preparedQuery;
938
        }
939
940 11
        $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
941
942 11
        if ($discriminatorValues === []) {
943
            return $preparedQuery;
944
        }
945
946 11
        if (count($discriminatorValues) === 1) {
947 8
            $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
948
        } else {
949 3
            $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
950
        }
951
952 11
        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 68
    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 68
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
971 68
        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 4
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
973
        }
974
975 68
        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 128
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
984
    {
985 128
        $preparedQuery = [];
986
987 128
        foreach ($query as $key => $value) {
988
            // Recursively prepare logical query clauses
989 115
            if (in_array($key, ['$and', '$or', '$nor'], true) && is_array($value)) {
990 8
                foreach ($value as $k2 => $v2) {
991 8
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
992
                }
993 8
                continue;
994
            }
995
996 115
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
997 56
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
998 56
                continue;
999
            }
1000
1001 115
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1002 115
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
1003 115
                $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 58
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1005 86
                    : Type::convertPHPToDatabaseValue($preparedValue);
1006
            }
1007
        }
1008
1009 128
        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 503
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
1022
    {
1023 503
        $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 503
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1029 78
            $mapping   = $class->fieldMappings[$fieldName];
1030 78
            $fieldName = $mapping['name'];
1031
1032 78
            if (! $prepareValue) {
1033 46
                return [[$fieldName, $value]];
1034
            }
1035
1036
            // Prepare mapped, embedded objects
1037 33
            if (! empty($mapping['embedded']) && is_object($value) &&
1038 33
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1039
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1040
            }
1041
1042 33
            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 6
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1045
                } 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 27
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty((array) $value)) {
1052 26
                return [[$fieldName, $value]];
1053
            }
1054
1055
            // Additional preparation for one or more simple reference values
1056 1
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1057
1058 1
            if (! is_array($value)) {
1059 1
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1060
            }
1061
1062
            // Objects without operators or with DBRef fields can be converted immediately
1063
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1064
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1065
            }
1066
1067
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1068
        }
1069
1070
        // Process identifier fields
1071 454
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1072 20
            $fieldName = '_id';
1073
1074 20
            if (! $prepareValue) {
1075 17
                return [[$fieldName, $value]];
1076
            }
1077
1078 3
            if (! is_array($value)) {
1079 2
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1080
            }
1081
1082
            // Objects without operators or with DBRef fields can be converted immediately
1083 1
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1084
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1085
            }
1086
1087 1
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1088
        }
1089
1090
        // No processing for unmapped, non-identifier, non-dotted field names
1091 451
        if (strpos($fieldName, '.') === false) {
1092 430
            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 25
        $e = explode('.', $fieldName, 4);
1102
1103
        // No further processing for unmapped fields
1104 25
        if (! isset($class->fieldMappings[$e[0]])) {
1105 5
            return [[$fieldName, $value]];
1106
        }
1107
1108 20
        $mapping = $class->fieldMappings[$e[0]];
1109 20
        $e[0]    = $mapping['name'];
1110
1111
        // Hash and raw fields will not be prepared beyond the field name
1112 20
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1113 1
            $fieldName = implode('.', $e);
1114
1115 1
            return [[$fieldName, $value]];
1116
        }
1117
1118 19
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1119 19
                && isset($e[2])) {
1120
            $objectProperty       = $e[2];
1121
            $objectPropertyPrefix = $e[1] . '.';
1122
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1123 19
        } elseif ($e[1] !== '$') {
1124 18
            $fieldName            = $e[0] . '.' . $e[1];
1125 18
            $objectProperty       = $e[1];
1126 18
            $objectPropertyPrefix = '';
1127 18
            $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 19
        if (! isset($mapping['targetDocument'])) {
1141 2
            if ($nextObjectProperty) {
1142
                $fieldName .= '.' . $nextObjectProperty;
1143
            }
1144
1145 2
            return [[$fieldName, $value]];
1146
        }
1147
1148 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1149
1150
        // No further processing for unmapped targetDocument fields
1151 17
        if (! $targetClass->hasField($objectProperty)) {
1152 3
            if ($nextObjectProperty) {
1153
                $fieldName .= '.' . $nextObjectProperty;
1154
            }
1155
1156 3
            return [[$fieldName, $value]];
1157
        }
1158
1159 16
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1160 16
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1161
1162
        // Prepare DBRef identifiers or the mapped field's property path
1163 16
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1164 8
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1165 16
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1166
1167
        // Process targetDocument identifier fields
1168 16
        if ($objectPropertyIsId) {
1169 8
            if (! $prepareValue) {
1170
                return [[$fieldName, $value]];
1171
            }
1172
1173 8
            if (! is_array($value)) {
1174 1
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1175
            }
1176
1177
            // Objects without operators or with DBRef fields can be converted immediately
1178 7
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1179
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1180
            }
1181
1182 7
            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 8
        if ($nextObjectProperty) {
1190
            // Respect the targetDocument's class metadata when recursing
1191 7
            $nextTargetClass = isset($targetMapping['targetDocument'])
1192 7
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1193 7
                : null;
1194
1195 7
            if (empty($targetMapping['reference'])) {
1196 7
                $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 2
                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 2
                $fieldNames = [[$nextObjectProperty, $value]];
1203
            }
1204
1205
            return array_map(static function ($preparedTuple) use ($fieldName) {
1206 7
                [$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 7
                return [$fieldName . '.' . $key, $value];
1209 7
            }, $fieldNames);
1210
        }
1211
1212 1
        return [[$fieldName, $value]];
1213
    }
1214
1215 8
    private function prepareQueryExpression(array $expression, ClassMetadata $class) : array
1216
    {
1217 8
        foreach ($expression as $k => $v) {
1218
            // Ignore query operators whose arguments need no type conversion
1219 8
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1220
                continue;
1221
            }
1222
1223
            // Process query operators whose argument arrays need type conversion
1224 8
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1225 7
                foreach ($v as $k2 => $v2) {
1226 7
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1227
                }
1228 7
                continue;
1229
            }
1230
1231
            // Recursively process expressions within a $not operator
1232 2
            if ($k === '$not' && is_array($v)) {
1233
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1234
                continue;
1235
            }
1236
1237 2
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1238
        }
1239
1240 8
        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 8
    private function hasDBRefFields($value) : bool
1254
    {
1255 8
        if (! is_array($value) && ! is_object($value)) {
1256
            return false;
1257
        }
1258
1259 8
        if (is_object($value)) {
1260
            $value = get_object_vars($value);
1261
        }
1262
1263 8
        foreach ($value as $key => $_) {
1264 8
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1265
                return true;
1266
            }
1267
        }
1268
1269 8
        return false;
1270
    }
1271
1272
    /**
1273
     * Checks whether the value has query operators.
1274
     *
1275
     * @param mixed $value
1276
     */
1277 8
    private function hasQueryOperators($value) : bool
1278
    {
1279 8
        if (! is_array($value) && ! is_object($value)) {
1280
            return false;
1281
        }
1282
1283 8
        if (is_object($value)) {
1284
            $value = get_object_vars($value);
1285
        }
1286
1287 8
        foreach ($value as $key => $_) {
1288 8
            if (isset($key[0]) && $key[0] === '$') {
1289 8
                return true;
1290
            }
1291
        }
1292
1293
        return false;
1294
    }
1295
1296
    /**
1297
     * Returns the list of discriminator values for the given ClassMetadata
1298
     */
1299 11
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1300
    {
1301 11
        $discriminatorValues = [];
1302
1303 11
        if ($metadata->discriminatorValue !== null) {
1304 9
            $discriminatorValues[] = $metadata->discriminatorValue;
1305
        }
1306
1307 11
        foreach ($metadata->subClasses as $className) {
1308 3
            $key = array_search($className, $metadata->discriminatorMap);
1309 3
            if (! $key) {
1310
                continue;
1311
            }
1312
1313 3
            $discriminatorValues[] = $key;
1314
        }
1315
1316
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1317 11
        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
            $discriminatorValues[] = null;
1319
        }
1320
1321 11
        return $discriminatorValues;
1322
    }
1323
1324
    private function handleCollections(object $document, array $options) : void
1325
    {
1326
        // Collection deletions (deletions of complete collections)
1327
        $collections = [];
1328
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1329
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1330
                continue;
1331
            }
1332
1333
            $collections[] = $coll;
1334
        }
1335
        if (! empty($collections)) {
1336
            $this->cp->delete($document, $collections, $options);
1337
        }
1338
        // Collection updates (deleteRows, updateRows, insertRows)
1339
        $collections = [];
1340
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1341
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1342
                continue;
1343
            }
1344
1345
            $collections[] = $coll;
1346
        }
1347
        if (! empty($collections)) {
1348
            $this->cp->update($document, $collections, $options);
1349
        }
1350
        // Take new snapshots from visited collections
1351
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1352
            $coll->takeSnapshot();
1353
        }
1354
    }
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 4
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1364
    {
1365 4
        $dcs      = $this->uow->getDocumentChangeSet($document);
1366 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1367
1368 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1369 4
        $fieldName    = $fieldMapping['fieldName'];
1370
1371 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1372
            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 4
        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 4
    }
1379
1380
    /**
1381
     * Get shard key aware query for single document.
1382
     */
1383 9
    private function getQueryForDocument(object $document) : array
1384
    {
1385 9
        $id = $this->uow->getDocumentIdentifier($document);
1386 9
        $id = $this->class->getDatabaseIdentifierValue($id);
1387
1388 9
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1389
1390 9
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1391
    }
1392
1393 31
    private function getWriteOptions(array $options = []) : array
1394
    {
1395 31
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1396 31
        $documentOptions = [];
1397 31
        if ($this->class->hasWriteConcern()) {
1398
            $documentOptions['w'] = $this->class->getWriteConcern();
1399
        }
1400
1401 31
        return array_merge($defaultOptions, $documentOptions, $options);
1402
    }
1403
1404 7
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1405
    {
1406 7
        $reference = $this->dm->createReference($value, $mapping);
1407 7
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1408 6
            return [[$fieldName, $reference]];
1409
        }
1410
1411 1
        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 1
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1419
1420 1
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1421 1
                    unset($keys['$db']);
1422
                }
1423
1424 1
                if (isset($mapping['targetDocument'])) {
1425 1
                    unset($keys['$ref'], $keys['$db']);
1426
                }
1427 1
                break;
1428
1429
            default:
1430
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1431
        }
1432
1433 1
        if ($mapping['type'] === 'many') {
1434
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1435
        }
1436
1437 1
        return array_map(
1438
            static function ($key) use ($reference, $fieldName) {
1439 1
                return [$fieldName . '.' . $key, $reference[$key]];
1440 1
            },
1441 1
            array_keys($keys)
1442
        );
1443
    }
1444
}
1445