Completed
Pull Request — master (#1870)
by Olivier
14:29
created

createReferenceManyInverseSideQuery()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 6.0163

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 24
cts 26
cp 0.9231
rs 8.6737
c 0
b 0
f 0
cc 6
nc 32
nop 1
crap 6.0163
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\PersistentCollectionInterface;
21
use Doctrine\ODM\MongoDB\Proxy\Proxy;
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 stdClass;
36
use function array_combine;
37
use function array_fill;
38
use function array_intersect_key;
39
use function array_keys;
40
use function array_map;
41
use function array_merge;
42
use function array_search;
43
use function array_slice;
44
use function array_values;
45
use function count;
46
use function explode;
47
use function get_class;
48
use function get_object_vars;
49
use function implode;
50
use function in_array;
51
use function is_array;
52
use function is_object;
53
use function is_scalar;
54
use function max;
55
use function spl_object_hash;
56
use function sprintf;
57
use function strpos;
58
use function strtolower;
59
60
/**
61
 * The DocumentPersister is responsible for persisting documents.
62
 */
63
class DocumentPersister
64
{
65
    /** @var PersistenceBuilder */
66
    private $pb;
67
68
    /** @var DocumentManager */
69
    private $dm;
70
71
    /** @var UnitOfWork */
72
    private $uow;
73
74
    /** @var ClassMetadata */
75
    private $class;
76
77
    /** @var Collection */
78
    private $collection;
79
80
    /** @var Bucket|null */
81
    private $bucket;
82
83
    /**
84
     * Array of queued inserts for the persister to insert.
85
     *
86
     * @var array
87
     */
88
    private $queuedInserts = [];
89
90
    /**
91
     * Array of queued inserts for the persister to insert.
92
     *
93
     * @var array
94
     */
95
    private $queuedUpserts = [];
96
97
    /** @var CriteriaMerger */
98
    private $cm;
99
100
    /** @var CollectionPersister */
101
    private $cp;
102
103
    /** @var HydratorFactory */
104
    private $hydratorFactory;
105
106 1121
    public function __construct(
107
        PersistenceBuilder $pb,
108
        DocumentManager $dm,
109
        UnitOfWork $uow,
110
        HydratorFactory $hydratorFactory,
111
        ClassMetadata $class,
112
        ?CriteriaMerger $cm = null
113
    ) {
114 1121
        $this->pb              = $pb;
115 1121
        $this->dm              = $dm;
116 1121
        $this->cm              = $cm ?: new CriteriaMerger();
117 1121
        $this->uow             = $uow;
118 1121
        $this->hydratorFactory = $hydratorFactory;
119 1121
        $this->class           = $class;
120 1121
        $this->collection      = $dm->getDocumentCollection($class->name);
121 1121
        $this->cp              = $this->uow->getCollectionPersister();
122
123 1121
        if (! $class->isFile) {
124 1113
            return;
125
        }
126
127 10
        $this->bucket = $dm->getDocumentBucket($class->name);
128 10
    }
129
130
    public function getInserts() : array
131
    {
132
        return $this->queuedInserts;
133
    }
134
135
    public function isQueuedForInsert(object $document) : bool
136
    {
137
        return isset($this->queuedInserts[spl_object_hash($document)]);
138
    }
139
140
    /**
141
     * Adds a document to the queued insertions.
142
     * The document remains queued until {@link executeInserts} is invoked.
143
     */
144 511
    public function addInsert(object $document) : void
145
    {
146 511
        $this->queuedInserts[spl_object_hash($document)] = $document;
147 511
    }
148
149
    public function getUpserts() : array
150
    {
151
        return $this->queuedUpserts;
152
    }
153
154
    public function isQueuedForUpsert(object $document) : bool
155
    {
156
        return isset($this->queuedUpserts[spl_object_hash($document)]);
157
    }
158
159
    /**
160
     * Adds a document to the queued upserts.
161
     * The document remains queued until {@link executeUpserts} is invoked.
162
     */
163 85
    public function addUpsert(object $document) : void
164
    {
165 85
        $this->queuedUpserts[spl_object_hash($document)] = $document;
166 85
    }
167
168
    /**
169
     * Gets the ClassMetadata instance of the document class this persister is used for.
170
     */
171
    public function getClassMetadata() : ClassMetadata
172
    {
173
        return $this->class;
174
    }
175
176
    /**
177
     * Executes all queued document insertions.
178
     *
179
     * Queued documents without an ID will inserted in a batch and queued
180
     * documents with an ID will be upserted individually.
181
     *
182
     * If no inserts are queued, invoking this method is a NOOP.
183
     *
184
     * @throws DriverException
185
     */
186 511
    public function executeInserts(array $options = []) : void
187
    {
188 511
        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...
189
            return;
190
        }
191
192 511
        $inserts = [];
193 511
        $options = $this->getWriteOptions($options);
194 511
        foreach ($this->queuedInserts as $oid => $document) {
195 511
            $data = $this->pb->prepareInsertData($document);
196
197
            // Set the initial version for each insert
198 510
            if ($this->class->isVersioned) {
199 38
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
200 38
                $nextVersion    = null;
201 38
                if ($versionMapping['type'] === 'int') {
202 36
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
203 36
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
204 2
                } elseif ($versionMapping['type'] === 'date') {
205 2
                    $nextVersionDateTime = new DateTime();
206 2
                    $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
207 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
208
                }
209 38
                $data[$versionMapping['name']] = $nextVersion;
210
            }
211
212 510
            $inserts[] = $data;
213
        }
214
215 510
        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...
216
            try {
217 510
                $this->collection->insertMany($inserts, $options);
218 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...
219 6
                $this->queuedInserts = [];
220 6
                throw $e;
221
            }
222
        }
223
224
        /* All collections except for ones using addToSet have already been
225
         * saved. We have left these to be handled separately to avoid checking
226
         * collection for uniqueness on PHP side.
227
         */
228 510
        foreach ($this->queuedInserts as $document) {
229 510
            $this->handleCollections($document, $options);
230
        }
231
232 510
        $this->queuedInserts = [];
233 510
    }
234
235
    /**
236
     * Executes all queued document upserts.
237
     *
238
     * Queued documents with an ID are upserted individually.
239
     *
240
     * If no upserts are queued, invoking this method is a NOOP.
241
     */
242 85
    public function executeUpserts(array $options = []) : void
243
    {
244 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...
245
            return;
246
        }
247
248 85
        $options = $this->getWriteOptions($options);
249 85
        foreach ($this->queuedUpserts as $oid => $document) {
250
            try {
251 85
                $this->executeUpsert($document, $options);
252 85
                $this->handleCollections($document, $options);
253 85
                unset($this->queuedUpserts[$oid]);
254
            } 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...
255
                unset($this->queuedUpserts[$oid]);
256 85
                throw $e;
257
            }
258
        }
259 85
    }
260
261
    /**
262
     * Executes a single upsert in {@link executeUpserts}
263
     */
264 85
    private function executeUpsert(object $document, array $options) : void
265
    {
266 85
        $options['upsert'] = true;
267 85
        $criteria          = $this->getQueryForDocument($document);
268
269 85
        $data = $this->pb->prepareUpsertData($document);
270
271
        // Set the initial version for each upsert
272 85
        if ($this->class->isVersioned) {
273 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
274 3
            $nextVersion    = null;
275 3
            if ($versionMapping['type'] === 'int') {
276 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
277 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
278 1
            } elseif ($versionMapping['type'] === 'date') {
279 1
                $nextVersionDateTime = new DateTime();
280 1
                $nextVersion         = Type::convertPHPToDatabaseValue($nextVersionDateTime);
281 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
282
            }
283 3
            $data['$set'][$versionMapping['name']] = $nextVersion;
284
        }
285
286 85
        foreach (array_keys($criteria) as $field) {
287 85
            unset($data['$set'][$field]);
288 85
            unset($data['$inc'][$field]);
289 85
            unset($data['$setOnInsert'][$field]);
290
        }
291
292
        // Do not send empty update operators
293 85
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
294 85
            if (! empty($data[$operator])) {
295 70
                continue;
296
            }
297
298 85
            unset($data[$operator]);
299
        }
300
301
        /* If there are no modifiers remaining, we're upserting a document with
302
         * an identifier as its only field. Since a document with the identifier
303
         * may already exist, the desired behavior is "insert if not exists" and
304
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
305
         * the identifier to the same value in our criteria.
306
         *
307
         * This will fail for versions before MongoDB 2.6, which require an
308
         * empty $set modifier. The best we can do (without attempting to check
309
         * server versions in advance) is attempt the 2.6+ behavior and retry
310
         * after the relevant exception.
311
         *
312
         * See: https://jira.mongodb.org/browse/SERVER-12266
313
         */
314 85
        if (empty($data)) {
315 16
            $retry = true;
316 16
            $data  = ['$set' => ['_id' => $criteria['_id']]];
317
        }
318
319
        try {
320 85
            $this->collection->updateOne($criteria, $data, $options);
321 85
            return;
322
        } 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...
323
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
324
                throw $e;
325
            }
326
        }
327
328
        $this->collection->updateOne($criteria, ['$set' => new stdClass()], $options);
329
    }
330
331
    /**
332
     * Updates the already persisted document if it has any new changesets.
333
     *
334
     * @throws LockException
335
     */
336 219
    public function update(object $document, array $options = []) : void
337
    {
338 219
        $update = $this->pb->prepareUpdateData($document);
339
340 219
        $query = $this->getQueryForDocument($document);
341
342 217
        foreach (array_keys($query) as $field) {
343 217
            unset($update['$set'][$field]);
344
        }
345
346 217
        if (empty($update['$set'])) {
347 94
            unset($update['$set']);
348
        }
349
350
        // Include versioning logic to set the new version value in the database
351
        // and to ensure the version has not changed since this document object instance
352
        // was fetched from the database
353 217
        $nextVersion = null;
354 217
        if ($this->class->isVersioned) {
355 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
356 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
357 31
            if ($versionMapping['type'] === 'int') {
358 28
                $nextVersion                             = $currentVersion + 1;
359 28
                $update['$inc'][$versionMapping['name']] = 1;
360 28
                $query[$versionMapping['name']]          = $currentVersion;
361 3
            } elseif ($versionMapping['type'] === 'date') {
362 3
                $nextVersion                             = new DateTime();
363 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
364 3
                $query[$versionMapping['name']]          = Type::convertPHPToDatabaseValue($currentVersion);
365
            }
366
        }
367
368 217
        if (! empty($update)) {
369
            // Include locking logic so that if the document object in memory is currently
370
            // locked then it will remove it, otherwise it ensures the document is not locked.
371 149
            if ($this->class->isLockable) {
372 11
                $isLocked    = $this->class->reflFields[$this->class->lockField]->getValue($document);
373 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
374 11
                if ($isLocked) {
375 2
                    $update['$unset'] = [$lockMapping['name'] => true];
376
                } else {
377 9
                    $query[$lockMapping['name']] = ['$exists' => false];
378
                }
379
            }
380
381 149
            $options = $this->getWriteOptions($options);
382
383 149
            $result = $this->collection->updateOne($query, $update, $options);
384
385 149
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
386 5
                throw LockException::lockFailed($document);
387 145
            } elseif ($this->class->isVersioned) {
388 27
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
389
            }
390
        }
391
392 213
        $this->handleCollections($document, $options);
393 213
    }
394
395
    /**
396
     * Removes document from mongo
397
     *
398
     * @throws LockException
399
     */
400 36
    public function delete(object $document, array $options = []) : void
401
    {
402 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...
403 1
            $documentIdentifier = $this->uow->getDocumentIdentifier($document);
404 1
            $databaseIdentifier = $this->class->getDatabaseIdentifierValue($documentIdentifier);
405
406 1
            $this->bucket->delete($databaseIdentifier);
407
408 1
            return;
409
        }
410
411 35
        $query = $this->getQueryForDocument($document);
412
413 35
        if ($this->class->isLockable) {
414 2
            $query[$this->class->lockField] = ['$exists' => false];
415
        }
416
417 35
        $options = $this->getWriteOptions($options);
418
419 35
        $result = $this->collection->deleteOne($query, $options);
420
421 35
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
422 2
            throw LockException::lockFailed($document);
423
        }
424 33
    }
425
426
    /**
427
     * Refreshes a managed document.
428
     */
429 23
    public function refresh(object $document) : void
430
    {
431 23
        $query = $this->getQueryForDocument($document);
432 23
        $data  = $this->collection->findOne($query);
433 23
        $data  = $this->hydratorFactory->hydrate($document, $data);
434 23
        $this->uow->setOriginalDocumentData($document, $data);
435 23
    }
436
437
    /**
438
     * Finds a document by a set of criteria.
439
     *
440
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
441
     * be used to match an _id value.
442
     *
443
     * @param mixed $criteria Query criteria
444
     *
445
     * @throws LockException
446
     *
447
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
448
     */
449 372
    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...
450
    {
451
        // TODO: remove this
452 372
        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...
453
            $criteria = ['_id' => $criteria];
454
        }
455
456 372
        $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...
457 372
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
458 372
        $criteria = $this->addFilterToPreparedQuery($criteria);
459
460 372
        $options = [];
461 372
        if ($sort !== null) {
462 95
            $options['sort'] = $this->prepareSort($sort);
463
        }
464 372
        $result = $this->collection->findOne($criteria, $options);
465
466 372
        if ($this->class->isLockable) {
467 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
468 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
469 1
                throw LockException::lockFailed($document);
470
            }
471
        }
472
473 371
        if ($result === null) {
474 116
            return null;
475
        }
476
477 327
        return $this->createDocument($result, $document, $hints);
478
    }
479
480
    /**
481
     * Finds documents by a set of criteria.
482
     */
483 22
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
484
    {
485 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
486 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
487 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
488
489 22
        $options = [];
490 22
        if ($sort !== null) {
491 11
            $options['sort'] = $this->prepareSort($sort);
492
        }
493
494 22
        if ($limit !== null) {
495 10
            $options['limit'] = $limit;
496
        }
497
498 22
        if ($skip !== null) {
499 1
            $options['skip'] = $skip;
500
        }
501
502 22
        $baseCursor = $this->collection->find($criteria, $options);
503 22
        return $this->wrapCursor($baseCursor);
504
    }
505
506
    /**
507
     * @throws MongoDBException
508
     */
509 299
    private function getShardKeyQuery(object $document) : array
510
    {
511 299
        if (! $this->class->isSharded()) {
512 289
            return [];
513
        }
514
515 10
        $shardKey = $this->class->getShardKey();
516 10
        $keys     = array_keys($shardKey['keys']);
517 10
        $data     = $this->uow->getDocumentActualData($document);
518
519 10
        $shardKeyQueryPart = [];
520 10
        foreach ($keys as $key) {
521 10
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
522 10
            $this->guardMissingShardKey($document, $key, $data);
523
524 8
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
525 1
                $reference = $this->prepareReference(
526 1
                    $key,
527 1
                    $data[$mapping['fieldName']],
528 1
                    $mapping,
529 1
                    false
530
                );
531 1
                foreach ($reference as $keyValue) {
532 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
533
                }
534
            } else {
535 7
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
536 8
                $shardKeyQueryPart[$key] = $value;
537
            }
538
        }
539
540 8
        return $shardKeyQueryPart;
541
    }
542
543
    /**
544
     * Wraps the supplied base cursor in the corresponding ODM class.
545
     */
546 22
    private function wrapCursor(Cursor $baseCursor) : Iterator
547
    {
548 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
549
    }
550
551
    /**
552
     * Checks whether the given managed document exists in the database.
553
     */
554 3
    public function exists(object $document) : bool
555
    {
556 3
        $id = $this->class->getIdentifierObject($document);
557 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
558
    }
559
560
    /**
561
     * Locks document by storing the lock mode on the mapped lock field.
562
     */
563 5
    public function lock(object $document, int $lockMode) : void
564
    {
565 5
        $id          = $this->uow->getDocumentIdentifier($document);
566 5
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
567 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
568 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
569 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
570 5
    }
571
572
    /**
573
     * Releases any lock that exists on this document.
574
     */
575 1
    public function unlock(object $document) : void
576
    {
577 1
        $id          = $this->uow->getDocumentIdentifier($document);
578 1
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
579 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
580 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
581 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
582 1
    }
583
584
    /**
585
     * Creates or fills a single document object from an query result.
586
     *
587
     * @param array $result    The query result.
588
     * @param object $document The document object to fill, if any.
589
     * @param array  $hints    Hints for document creation.
590
     *
591
     * @return object The filled and managed document object or NULL, if the query result is empty.
592
     */
593 327
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
594
    {
595 327
        if ($document !== null) {
596 28
            $hints[Query::HINT_REFRESH] = true;
597 28
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
598 28
            $this->uow->registerManaged($document, $id, $result);
599
        }
600
601 327
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
602
    }
603
604
    /**
605
     * Loads a PersistentCollection data. Used in the initialize() method.
606
     */
607 184
    public function loadCollection(PersistentCollectionInterface $collection) : void
608
    {
609 184
        $mapping = $collection->getMapping();
610 184
        switch ($mapping['association']) {
611
            case ClassMetadata::EMBED_MANY:
612 129
                $this->loadEmbedManyCollection($collection);
613 129
                break;
614
615
            case ClassMetadata::REFERENCE_MANY:
616 78
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
617 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
618
                } else {
619 74
                    if ($mapping['isOwningSide']) {
620 62
                        $this->loadReferenceManyCollectionOwningSide($collection);
621
                    } else {
622 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
623
                    }
624
                }
625 78
                break;
626
        }
627 184
    }
628
629 129
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
630
    {
631 129
        $embeddedDocuments = $collection->getMongoData();
632 129
        $mapping           = $collection->getMapping();
633 129
        $owner             = $collection->getOwner();
634 129
        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...
635 75
            return;
636
        }
637
638 100
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
639 100
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
640 100
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
641 100
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
642
643 100
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
644
645 100
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
646 100
            $id   = $data[$embeddedMetadata->identifier] ?? null;
647
648 100
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
649 99
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
650
            }
651 100
            if (CollectionHelper::isHash($mapping['strategy'])) {
652 25
                $collection->set($key, $embeddedDocumentObject);
653
            } else {
654 100
                $collection->add($embeddedDocumentObject);
655
            }
656
        }
657 100
    }
658
659 62
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
660
    {
661 62
        $hints      = $collection->getHints();
662 62
        $mapping    = $collection->getMapping();
663 62
        $groupedIds = [];
664
665 62
        $sorted = isset($mapping['sort']) && $mapping['sort'];
666
667 62
        foreach ($collection->getMongoData() as $key => $reference) {
668 56
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
669 56
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
670 56
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
671
672
            // create a reference to the class and id
673 56
            $reference = $this->dm->getReference($className, $id);
674
675
            // no custom sort so add the references right now in the order they are embedded
676 56
            if (! $sorted) {
677 55
                if (CollectionHelper::isHash($mapping['strategy'])) {
678 2
                    $collection->set($key, $reference);
679
                } else {
680 53
                    $collection->add($reference);
681
                }
682
            }
683
684
            // only query for the referenced object if it is not already initialized or the collection is sorted
685 56
            if (! (($reference instanceof Proxy && ! $reference->__isInitialized__)) && ! $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
686 22
                continue;
687
            }
688
689 41
            $groupedIds[$className][] = $identifier;
690
        }
691 62
        foreach ($groupedIds as $className => $ids) {
692 41
            $class           = $this->dm->getClassMetadata($className);
693 41
            $mongoCollection = $this->dm->getDocumentCollection($className);
694 41
            $criteria        = $this->cm->merge(
695 41
                ['_id' => ['$in' => array_values($ids)]],
696 41
                $this->dm->getFilterCollection()->getFilterCriteria($class),
697 41
                $mapping['criteria'] ?? []
698
            );
699 41
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
700
701 41
            $options = [];
702 41
            if (isset($mapping['sort'])) {
703 41
                $options['sort'] = $this->prepareSort($mapping['sort']);
704
            }
705 41
            if (isset($mapping['limit'])) {
706
                $options['limit'] = $mapping['limit'];
707
            }
708 41
            if (isset($mapping['skip'])) {
709
                $options['skip'] = $mapping['skip'];
710
            }
711 41
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
712
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
713
            }
714
715 41
            $cursor    = $mongoCollection->find($criteria, $options);
716 41
            $documents = $cursor->toArray();
717 41
            foreach ($documents as $documentData) {
718 40
                $document = $this->uow->getById($documentData['_id'], $class);
719 40
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
720 40
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
721 40
                    $this->uow->setOriginalDocumentData($document, $data);
722 40
                    $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
723
                }
724 40
                if (! $sorted) {
725 39
                    continue;
726
                }
727
728 41
                $collection->add($document);
729
            }
730
        }
731 62
    }
732
733 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
734
    {
735 17
        $query     = $this->createReferenceManyInverseSideQuery($collection);
736 17
        $documents = $query->execute()->toArray();
737 17
        foreach ($documents as $key => $document) {
738 16
            $collection->add($document);
739
        }
740 17
    }
741
742 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
743
    {
744 17
        $hints             = $collection->getHints();
745 17
        $mapping           = $collection->getMapping();
746 17
        $owner             = $collection->getOwner();
747 17
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
748 17
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
749 17
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
750 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
751
752 17
        $criteria = $this->cm->merge(
753 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
754 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
755 17
            $mapping['criteria'] ?? []
756
        );
757 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
758 17
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
759 17
            ->setQueryArray($criteria);
760
761 17
        if (isset($mapping['sort'])) {
762 17
            $qb->sort($mapping['sort']);
763
        }
764 17
        if (isset($mapping['limit'])) {
765 2
            $qb->limit($mapping['limit']);
766
        }
767 17
        if (isset($mapping['skip'])) {
768
            $qb->skip($mapping['skip']);
769
        }
770
771 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
772
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
773
        }
774
775 17
        foreach ($mapping['prime'] as $field) {
776 4
            $qb->field($field)->prime(true);
777
        }
778
779 17
        return $qb->getQuery();
780
    }
781
782 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
783
    {
784 5
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
785 5
        $mapping   = $collection->getMapping();
786 5
        $documents = $cursor->toArray();
787 5
        foreach ($documents as $key => $obj) {
788 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
789 1
                $collection->set($key, $obj);
790
            } else {
791 5
                $collection->add($obj);
792
            }
793
        }
794 5
    }
795
796 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
797
    {
798 5
        $mapping          = $collection->getMapping();
799 5
        $repositoryMethod = $mapping['repositoryMethod'];
800 5
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
801 5
            ->$repositoryMethod($collection->getOwner());
802
803 5
        if (! $cursor instanceof Iterator) {
804
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
805
        }
806
807 5
        if (! empty($mapping['prime'])) {
808 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
809 1
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
810 1
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
811
812 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
813
        }
814
815 5
        return $cursor;
816
    }
817
818
    /**
819
     * Prepare a projection array by converting keys, which are PHP property
820
     * names, to MongoDB field names.
821
     */
822 14
    public function prepareProjection(array $fields) : array
823
    {
824 14
        $preparedFields = [];
825
826 14
        foreach ($fields as $key => $value) {
827 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
828
        }
829
830 14
        return $preparedFields;
831
    }
832
833
    /**
834
     * @param int|string|null $sort
835
     *
836
     * @return int|string|null
837
     */
838 25
    private function getSortDirection($sort)
839
    {
840 25
        switch (strtolower((string) $sort)) {
841 25
            case 'desc':
842 15
                return -1;
843
844 22
            case 'asc':
845 13
                return 1;
846
        }
847
848 12
        return $sort;
849
    }
850
851
    /**
852
     * Prepare a sort specification array by converting keys to MongoDB field
853
     * names and changing direction strings to int.
854
     */
855 146
    public function prepareSort(array $fields) : array
856
    {
857 146
        $sortFields = [];
858
859 146
        foreach ($fields as $key => $value) {
860 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
861
        }
862
863 146
        return $sortFields;
864
    }
865
866
    /**
867
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
868
     */
869 436
    public function prepareFieldName(string $fieldName) : string
870
    {
871 436
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
872
873 436
        return $fieldNames[0][0];
874
    }
875
876
    /**
877
     * Adds discriminator criteria to an already-prepared query.
878
     *
879
     * This method should be used once for query criteria and not be used for
880
     * nested expressions. It should be called before
881
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
882
     */
883 527
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
884
    {
885
        /* If the class has a discriminator field, which is not already in the
886
         * criteria, inject it now. The field/values need no preparation.
887
         */
888 527
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
889 27
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
890 27
            if (count($discriminatorValues) === 1) {
891 19
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
892
            } else {
893 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
894
            }
895
        }
896
897 527
        return $preparedQuery;
898
    }
899
900
    /**
901
     * Adds filter criteria to an already-prepared query.
902
     *
903
     * This method should be used once for query criteria and not be used for
904
     * nested expressions. It should be called after
905
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
906
     */
907 528
    public function addFilterToPreparedQuery(array $preparedQuery) : array
908
    {
909
        /* If filter criteria exists for this class, prepare it and merge
910
         * over the existing query.
911
         *
912
         * @todo Consider recursive merging in case the filter criteria and
913
         * prepared query both contain top-level $and/$or operators.
914
         */
915 528
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
916 528
        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...
917 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
918
        }
919
920 528
        return $preparedQuery;
921
    }
922
923
    /**
924
     * Prepares the query criteria or new document object.
925
     *
926
     * PHP field names and types will be converted to those used by MongoDB.
927
     */
928 560
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
929
    {
930 560
        $preparedQuery = [];
931
932 560
        foreach ($query as $key => $value) {
933
            // Recursively prepare logical query clauses
934 518
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
935 20
                foreach ($value as $k2 => $v2) {
936 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
937
                }
938 20
                continue;
939
            }
940
941 518
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
942 40
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
943 40
                continue;
944
            }
945
946 518
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
947 518
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
948 518
                $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...
949 135
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
950 518
                    : Type::convertPHPToDatabaseValue($preparedValue);
951
            }
952
        }
953
954 560
        return $preparedQuery;
955
    }
956
957
    /**
958
     * Prepares a query value and converts the PHP value to the database value
959
     * if it is an identifier.
960
     *
961
     * It also handles converting $fieldName to the database name if they are different.
962
     *
963
     * @param mixed $value
964
     */
965 914
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
966
    {
967 914
        $class = $class ?? $this->class;
968
969
        // @todo Consider inlining calls to ClassMetadata methods
970
971
        // Process all non-identifier fields by translating field names
972 914
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
973 257
            $mapping   = $class->fieldMappings[$fieldName];
974 257
            $fieldName = $mapping['name'];
975
976 257
            if (! $prepareValue) {
977 52
                return [[$fieldName, $value]];
978
            }
979
980
            // Prepare mapped, embedded objects
981 215
            if (! empty($mapping['embedded']) && is_object($value) &&
982 215
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
983 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
984
            }
985
986 213
            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...
987
                try {
988 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
989 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...
990
                    // do nothing in case passed object is not mapped document
991
                }
992
            }
993
994
            // No further preparation unless we're dealing with a simple reference
995
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
996 200
            $arrayValue = (array) $value;
997 200
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
998 133
                return [[$fieldName, $value]];
999
            }
1000
1001
            // Additional preparation for one or more simple reference values
1002 94
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1003
1004 94
            if (! is_array($value)) {
1005 90
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1006
            }
1007
1008
            // Objects without operators or with DBRef fields can be converted immediately
1009 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1010 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1011
            }
1012
1013 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1014
        }
1015
1016
        // Process identifier fields
1017 823
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1018 362
            $fieldName = '_id';
1019
1020 362
            if (! $prepareValue) {
1021 42
                return [[$fieldName, $value]];
1022
            }
1023
1024 323
            if (! is_array($value)) {
1025 297
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1026
            }
1027
1028
            // Objects without operators or with DBRef fields can be converted immediately
1029 63
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1030 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1031
            }
1032
1033 58
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1034
        }
1035
1036
        // No processing for unmapped, non-identifier, non-dotted field names
1037 561
        if (strpos($fieldName, '.') === false) {
1038 416
            return [[$fieldName, $value]];
1039
        }
1040
1041
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1042
         *
1043
         * We can limit parsing here, since at most three segments are
1044
         * significant: "fieldName.objectProperty" with an optional index or key
1045
         * for collections stored as either BSON arrays or objects.
1046
         */
1047 157
        $e = explode('.', $fieldName, 4);
1048
1049
        // No further processing for unmapped fields
1050 157
        if (! isset($class->fieldMappings[$e[0]])) {
1051 6
            return [[$fieldName, $value]];
1052
        }
1053
1054 152
        $mapping = $class->fieldMappings[$e[0]];
1055 152
        $e[0]    = $mapping['name'];
1056
1057
        // Hash and raw fields will not be prepared beyond the field name
1058 152
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1059 1
            $fieldName = implode('.', $e);
1060
1061 1
            return [[$fieldName, $value]];
1062
        }
1063
1064 151
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1065 151
                && isset($e[2])) {
1066 1
            $objectProperty       = $e[2];
1067 1
            $objectPropertyPrefix = $e[1] . '.';
1068 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1069 150
        } elseif ($e[1] !== '$') {
1070 149
            $fieldName            = $e[0] . '.' . $e[1];
1071 149
            $objectProperty       = $e[1];
1072 149
            $objectPropertyPrefix = '';
1073 149
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1074 1
        } elseif (isset($e[2])) {
1075 1
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1076 1
            $objectProperty       = $e[2];
1077 1
            $objectPropertyPrefix = $e[1] . '.';
1078 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1079
        } else {
1080 1
            $fieldName = $e[0] . '.' . $e[1];
1081
1082 1
            return [[$fieldName, $value]];
1083
        }
1084
1085
        // No further processing for fields without a targetDocument mapping
1086 151
        if (! isset($mapping['targetDocument'])) {
1087 3
            if ($nextObjectProperty) {
1088
                $fieldName .= '.' . $nextObjectProperty;
1089
            }
1090
1091 3
            return [[$fieldName, $value]];
1092
        }
1093
1094 148
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1095
1096
        // No further processing for unmapped targetDocument fields
1097 148
        if (! $targetClass->hasField($objectProperty)) {
1098 25
            if ($nextObjectProperty) {
1099
                $fieldName .= '.' . $nextObjectProperty;
1100
            }
1101
1102 25
            return [[$fieldName, $value]];
1103
        }
1104
1105 128
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1106 128
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1107
1108
        // Prepare DBRef identifiers or the mapped field's property path
1109 128
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1110 108
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1111 128
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1112
1113
        // Process targetDocument identifier fields
1114 128
        if ($objectPropertyIsId) {
1115 109
            if (! $prepareValue) {
1116 7
                return [[$fieldName, $value]];
1117
            }
1118
1119 102
            if (! is_array($value)) {
1120 88
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1121
            }
1122
1123
            // Objects without operators or with DBRef fields can be converted immediately
1124 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1125 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1126
            }
1127
1128 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1129
        }
1130
1131
        /* The property path may include a third field segment, excluding the
1132
         * collection item pointer. If present, this next object property must
1133
         * be processed recursively.
1134
         */
1135 19
        if ($nextObjectProperty) {
1136
            // Respect the targetDocument's class metadata when recursing
1137 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1138 10
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1139 16
                : null;
1140
1141 16
            if (empty($targetMapping['reference'])) {
1142 14
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1143
            } else {
1144
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1145 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1146 1
                    $nextObjectProperty = '$' . $nextObjectProperty;
1147
                }
1148 4
                $fieldNames = [[$nextObjectProperty, $value]];
1149
            }
1150
1151
            return array_map(static function ($preparedTuple) use ($fieldName) {
1152 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...
1153
1154 16
                return [$fieldName . '.' . $key, $value];
1155 16
            }, $fieldNames);
1156
        }
1157
1158 5
        return [[$fieldName, $value]];
1159
    }
1160
1161
    /**
1162
     * Prepares a query expression.
1163
     *
1164
     * @param array|object $expression
1165
     */
1166 80
    private function prepareQueryExpression($expression, ClassMetadata $class) : array
1167
    {
1168 80
        foreach ($expression as $k => $v) {
1169
            // Ignore query operators whose arguments need no type conversion
1170 80
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1171 16
                continue;
1172
            }
1173
1174
            // Process query operators whose argument arrays need type conversion
1175 80
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1176 78
                foreach ($v as $k2 => $v2) {
1177 78
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1178
                }
1179 78
                continue;
1180
            }
1181
1182
            // Recursively process expressions within a $not operator
1183 18
            if ($k === '$not' && is_array($v)) {
1184 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1185 15
                continue;
1186
            }
1187
1188 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1189
        }
1190
1191 80
        return $expression;
1192
    }
1193
1194
    /**
1195
     * Checks whether the value has DBRef fields.
1196
     *
1197
     * This method doesn't check if the the value is a complete DBRef object,
1198
     * although it should return true for a DBRef. Rather, we're checking that
1199
     * the value has one or more fields for a DBref. In practice, this could be
1200
     * $elemMatch criteria for matching a DBRef.
1201
     *
1202
     * @param mixed $value
1203
     */
1204 81
    private function hasDBRefFields($value) : bool
1205
    {
1206 81
        if (! is_array($value) && ! is_object($value)) {
1207
            return false;
1208
        }
1209
1210 81
        if (is_object($value)) {
1211
            $value = get_object_vars($value);
1212
        }
1213
1214 81
        foreach ($value as $key => $_) {
1215 81
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1216 81
                return true;
1217
            }
1218
        }
1219
1220 80
        return false;
1221
    }
1222
1223
    /**
1224
     * Checks whether the value has query operators.
1225
     *
1226
     * @param mixed $value
1227
     */
1228 85
    private function hasQueryOperators($value) : bool
1229
    {
1230 85
        if (! is_array($value) && ! is_object($value)) {
1231
            return false;
1232
        }
1233
1234 85
        if (is_object($value)) {
1235
            $value = get_object_vars($value);
1236
        }
1237
1238 85
        foreach ($value as $key => $_) {
1239 85
            if (isset($key[0]) && $key[0] === '$') {
1240 85
                return true;
1241
            }
1242
        }
1243
1244 11
        return false;
1245
    }
1246
1247
    /**
1248
     * Gets the array of discriminator values for the given ClassMetadata
1249
     */
1250 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1251
    {
1252 27
        $discriminatorValues = [$metadata->discriminatorValue];
1253 27
        foreach ($metadata->subClasses as $className) {
1254 8
            $key = array_search($className, $metadata->discriminatorMap);
1255 8
            if (! $key) {
1256
                continue;
1257
            }
1258
1259 8
            $discriminatorValues[] = $key;
1260
        }
1261
1262
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1263 27
        if ($metadata->defaultDiscriminatorValue && in_array($metadata->defaultDiscriminatorValue, $discriminatorValues)) {
1264 2
            $discriminatorValues[] = null;
1265
        }
1266
1267 27
        return $discriminatorValues;
1268
    }
1269
1270 583
    private function handleCollections(object $document, array $options) : void
1271
    {
1272
        // Collection deletions (deletions of complete collections)
1273 583
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1274 107
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1275 96
                continue;
1276
            }
1277
1278 31
            $this->cp->delete($coll, $options);
1279
        }
1280
        // Collection updates (deleteRows, updateRows, insertRows)
1281 583
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1282 107
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1283 29
                continue;
1284
            }
1285
1286 99
            $this->cp->update($coll, $options);
1287
        }
1288
        // Take new snapshots from visited collections
1289 583
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1290 250
            $coll->takeSnapshot();
1291
        }
1292 583
    }
1293
1294
    /**
1295
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1296
     * Also, shard key field should be present in actual document data.
1297
     *
1298
     * @throws MongoDBException
1299
     */
1300 10
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1301
    {
1302 10
        $dcs      = $this->uow->getDocumentChangeSet($document);
1303 10
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1304
1305 10
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1306 10
        $fieldName    = $fieldMapping['fieldName'];
1307
1308 10
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1309 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...
1310
        }
1311
1312 8
        if (! isset($actualDocumentData[$fieldName])) {
1313
            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...
1314
        }
1315 8
    }
1316
1317
    /**
1318
     * Get shard key aware query for single document.
1319
     */
1320 295
    private function getQueryForDocument(object $document) : array
1321
    {
1322 295
        $id = $this->uow->getDocumentIdentifier($document);
1323 295
        $id = $this->class->getDatabaseIdentifierValue($id);
1324
1325 295
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1326 293
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1327
    }
1328
1329 584
    private function getWriteOptions(array $options = []) : array
1330
    {
1331 584
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1332 584
        $documentOptions = [];
1333 584
        if ($this->class->hasWriteConcern()) {
1334 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1335
        }
1336
1337 584
        return array_merge($defaultOptions, $documentOptions, $options);
1338
    }
1339
1340 15
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1341
    {
1342 15
        $reference = $this->dm->createReference($value, $mapping);
1343 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1344 8
            return [[$fieldName, $reference]];
1345
        }
1346
1347 6
        switch ($mapping['storeAs']) {
1348
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1349
                $keys = ['id' => true];
1350
                break;
1351
1352
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1353
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1354 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1355
1356 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1357 5
                    unset($keys['$db']);
1358
                }
1359
1360 6
                if (isset($mapping['targetDocument'])) {
1361 4
                    unset($keys['$ref'], $keys['$db']);
1362
                }
1363 6
                break;
1364
1365
            default:
1366
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1367
        }
1368
1369 6
        if ($mapping['type'] === 'many') {
1370 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1371
        }
1372
1373 4
        return array_map(
1374
            static function ($key) use ($reference, $fieldName) {
1375 4
                return [$fieldName . '.' . $key, $reference[$key]];
1376 4
            },
1377 4
            array_keys($keys)
1378
        );
1379
    }
1380
}
1381