Completed
Push — master ( 62ca36...7f0e11 )
by Maciej
28:45 queued 07:56
created

DocumentPersister::loadCollection()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

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

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

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

Loading history...
647 75
            return;
648
        }
649
650 97
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
651 97
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
652 97
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
653 97
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
654
655 97
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
656
657 97
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
658 97
            $id   = $data[$embeddedMetadata->identifier] ?? null;
659
660 97
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
661 96
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
662
            }
663 97
            if (CollectionHelper::isHash($mapping['strategy'])) {
664 25
                $collection->set($key, $embeddedDocumentObject);
665
            } else {
666 80
                $collection->add($embeddedDocumentObject);
667
            }
668
        }
669 97
    }
670
671 59
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
672
    {
673 59
        $hints      = $collection->getHints();
674 59
        $mapping    = $collection->getMapping();
675 59
        $groupedIds = [];
676
677 59
        $sorted = isset($mapping['sort']) && $mapping['sort'];
678
679 59
        foreach ($collection->getMongoData() as $key => $reference) {
680 53
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
681 53
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
682 53
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
683
684
            // create a reference to the class and id
685 53
            $reference = $this->dm->getReference($className, $id);
686
687
            // no custom sort so add the references right now in the order they are embedded
688 53
            if (! $sorted) {
689 52
                if (CollectionHelper::isHash($mapping['strategy'])) {
690 2
                    $collection->set($key, $reference);
691
                } else {
692 50
                    $collection->add($reference);
693
                }
694
            }
695
696
            // only query for the referenced object if it is not already initialized or the collection is sorted
697 53
            if (! (($reference instanceof GhostObjectInterface && ! $reference->isProxyInitialized())) && ! $sorted) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
698 22
                continue;
699
            }
700
701 38
            $groupedIds[$className][] = $identifier;
702
        }
703 59
        foreach ($groupedIds as $className => $ids) {
704 38
            $class           = $this->dm->getClassMetadata($className);
705 38
            $mongoCollection = $this->dm->getDocumentCollection($className);
706 38
            $criteria        = $this->cm->merge(
707 38
                ['_id' => ['$in' => array_values($ids)]],
708 38
                $this->dm->getFilterCollection()->getFilterCriteria($class),
709 38
                $mapping['criteria'] ?? []
710
            );
711 38
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
712
713 38
            $options = [];
714 38
            if (isset($mapping['sort'])) {
715 38
                $options['sort'] = $this->prepareSort($mapping['sort']);
716
            }
717 38
            if (isset($mapping['limit'])) {
718
                $options['limit'] = $mapping['limit'];
719
            }
720 38
            if (isset($mapping['skip'])) {
721
                $options['skip'] = $mapping['skip'];
722
            }
723 38
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
724
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
725
            }
726
727 38
            $cursor    = $mongoCollection->find($criteria, $options);
728 38
            $documents = $cursor->toArray();
729 38
            foreach ($documents as $documentData) {
730 37
                $document = $this->uow->getById($documentData['_id'], $class);
731 37
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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