Completed
Pull Request — master (#1803)
by Maciej
15:54
created

DocumentPersister::load()   B

Complexity

Conditions 10
Paths 40

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10.017

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 17
cts 18
cp 0.9444
rs 7.6666
c 0
b 0
f 0
cc 10
nc 40
nop 5
crap 10.017

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Query\CriteriaMerger;
22
use Doctrine\ODM\MongoDB\Query\Query;
23
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
24
use Doctrine\ODM\MongoDB\Types\Type;
25
use Doctrine\ODM\MongoDB\UnitOfWork;
26
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
27
use InvalidArgumentException;
28
use MongoDB\BSON\ObjectId;
29
use MongoDB\Collection;
30
use MongoDB\Driver\Cursor;
31
use MongoDB\Driver\Exception\Exception as DriverException;
32
use MongoDB\Driver\Exception\WriteException;
33
use MongoDB\GridFS\Bucket;
34
use ProxyManager\Proxy\GhostObjectInterface;
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 1119
    public function __construct(
107
        PersistenceBuilder $pb,
108
        DocumentManager $dm,
109
        UnitOfWork $uow,
110
        HydratorFactory $hydratorFactory,
111
        ClassMetadata $class,
112
        ?CriteriaMerger $cm = null
113
    ) {
114 1119
        $this->pb              = $pb;
115 1119
        $this->dm              = $dm;
116 1119
        $this->cm              = $cm ?: new CriteriaMerger();
117 1119
        $this->uow             = $uow;
118 1119
        $this->hydratorFactory = $hydratorFactory;
119 1119
        $this->class           = $class;
120 1119
        $this->collection      = $dm->getDocumentCollection($class->name);
121 1119
        $this->cp              = $this->uow->getCollectionPersister();
122
123 1119
        if (! $class->isFile) {
124 1111
            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 510
    public function addInsert(object $document) : void
145
    {
146 510
        $this->queuedInserts[spl_object_hash($document)] = $document;
147 510
    }
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 510
    public function executeInserts(array $options = []) : void
187
    {
188 510
        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 510
        $inserts = [];
193 510
        $options = $this->getWriteOptions($options);
194 510
        foreach ($this->queuedInserts as $oid => $document) {
195 510
            $data = $this->pb->prepareInsertData($document);
196
197
            // Set the initial version for each insert
198 509
            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 509
            $inserts[] = $data;
213
        }
214
215 509
        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 509
                $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 509
        foreach ($this->queuedInserts as $document) {
229 509
            $this->handleCollections($document, $options);
230
        }
231
232 509
        $this->queuedInserts = [];
233 509
    }
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
        if ($data === null) {
434
            throw MongoDBException::cannotRefreshDocument();
435
        }
436 23
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
437 23
        $this->uow->setOriginalDocumentData($document, $data);
438 23
    }
439
440
    /**
441
     * Finds a document by a set of criteria.
442
     *
443
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
444
     * be used to match an _id value.
445
     *
446
     * @param mixed $criteria Query criteria
447
     *
448
     * @throws LockException
449
     *
450
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
451
     */
452 371
    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...
453
    {
454
        // TODO: remove this
455 371
        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...
456
            $criteria = ['_id' => $criteria];
457
        }
458
459 371
        $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...
460 371
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
461 371
        $criteria = $this->addFilterToPreparedQuery($criteria);
462
463 371
        $options = [];
464 371
        if ($sort !== null) {
465 95
            $options['sort'] = $this->prepareSort($sort);
466
        }
467 371
        $result = $this->collection->findOne($criteria, $options);
468 371
        $result = $result !== null ? (array) $result : null;
469
470 371
        if ($this->class->isLockable) {
471 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
472 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
473 1
                throw LockException::lockFailed($document);
474
            }
475
        }
476
477 370
        if ($result === null) {
478 116
            return null;
479
        }
480
481 326
        return $this->createDocument($result, $document, $hints);
482
    }
483
484
    /**
485
     * Finds documents by a set of criteria.
486
     */
487 22
    public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = null, ?int $skip = null) : Iterator
488
    {
489 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
490 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
491 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
492
493 22
        $options = [];
494 22
        if ($sort !== null) {
495 11
            $options['sort'] = $this->prepareSort($sort);
496
        }
497
498 22
        if ($limit !== null) {
499 10
            $options['limit'] = $limit;
500
        }
501
502 22
        if ($skip !== null) {
503 1
            $options['skip'] = $skip;
504
        }
505
506 22
        $baseCursor = $this->collection->find($criteria, $options);
507 22
        return $this->wrapCursor($baseCursor);
508
    }
509
510
    /**
511
     * @throws MongoDBException
512
     */
513 299
    private function getShardKeyQuery(object $document) : array
514
    {
515 299
        if (! $this->class->isSharded()) {
516 289
            return [];
517
        }
518
519 10
        $shardKey = $this->class->getShardKey();
520 10
        $keys     = array_keys($shardKey['keys']);
521 10
        $data     = $this->uow->getDocumentActualData($document);
522
523 10
        $shardKeyQueryPart = [];
524 10
        foreach ($keys as $key) {
525 10
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
526 10
            $this->guardMissingShardKey($document, $key, $data);
527
528 8
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
529 1
                $reference = $this->prepareReference(
530 1
                    $key,
531 1
                    $data[$mapping['fieldName']],
532 1
                    $mapping,
533 1
                    false
534
                );
535 1
                foreach ($reference as $keyValue) {
536 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
537
                }
538
            } else {
539 7
                $value                   = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
540 8
                $shardKeyQueryPart[$key] = $value;
541
            }
542
        }
543
544 8
        return $shardKeyQueryPart;
545
    }
546
547
    /**
548
     * Wraps the supplied base cursor in the corresponding ODM class.
549
     */
550 22
    private function wrapCursor(Cursor $baseCursor) : Iterator
551
    {
552 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
553
    }
554
555
    /**
556
     * Checks whether the given managed document exists in the database.
557
     */
558 3
    public function exists(object $document) : bool
559
    {
560 3
        $id = $this->class->getIdentifierObject($document);
561 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
562
    }
563
564
    /**
565
     * Locks document by storing the lock mode on the mapped lock field.
566
     */
567 5
    public function lock(object $document, int $lockMode) : void
568
    {
569 5
        $id          = $this->uow->getDocumentIdentifier($document);
570 5
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
571 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
572 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
573 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
574 5
    }
575
576
    /**
577
     * Releases any lock that exists on this document.
578
     */
579 1
    public function unlock(object $document) : void
580
    {
581 1
        $id          = $this->uow->getDocumentIdentifier($document);
582 1
        $criteria    = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
583 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
584 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
585 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
586 1
    }
587
588
    /**
589
     * Creates or fills a single document object from an query result.
590
     *
591
     * @param array      $result   The query result.
592
     * @param object     $document The document object to fill, if any.
593
     * @param array      $hints    Hints for document creation.
594
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
595
     */
596 326
    private function createDocument(array $result, ?object $document = null, array $hints = []) : ?object
597
    {
598 326
        if ($document !== null) {
599 29
            $hints[Query::HINT_REFRESH] = true;
600 29
            $id                         = $this->class->getPHPIdentifierValue($result['_id']);
601 29
            $this->uow->registerManaged($document, $id, $result);
602
        }
603
604 326
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
605
    }
606
607
    /**
608
     * Loads a PersistentCollection data. Used in the initialize() method.
609
     */
610 184
    public function loadCollection(PersistentCollectionInterface $collection) : void
611
    {
612 184
        $mapping = $collection->getMapping();
613 184
        switch ($mapping['association']) {
614
            case ClassMetadata::EMBED_MANY:
615 129
                $this->loadEmbedManyCollection($collection);
616 129
                break;
617
618
            case ClassMetadata::REFERENCE_MANY:
619 78
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
620 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
621
                } else {
622 74
                    if ($mapping['isOwningSide']) {
623 62
                        $this->loadReferenceManyCollectionOwningSide($collection);
624
                    } else {
625 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
626
                    }
627
                }
628 78
                break;
629
        }
630 184
    }
631
632 129
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection) : void
633
    {
634 129
        $embeddedDocuments = $collection->getMongoData();
635 129
        $mapping           = $collection->getMapping();
636 129
        $owner             = $collection->getOwner();
637 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...
638 75
            return;
639
        }
640
641 100
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
642 100
            $className              = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
643 100
            $embeddedMetadata       = $this->dm->getClassMetadata($className);
644 100
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
645
646 100
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
647
648 100
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
649 100
            $id   = $data[$embeddedMetadata->identifier] ?? null;
650
651 100
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
652 99
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
653
            }
654 100
            if (CollectionHelper::isHash($mapping['strategy'])) {
655 25
                $collection->set($key, $embeddedDocumentObject);
656
            } else {
657 100
                $collection->add($embeddedDocumentObject);
658
            }
659
        }
660 100
    }
661
662 62
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) : void
663
    {
664 62
        $hints      = $collection->getHints();
665 62
        $mapping    = $collection->getMapping();
666 62
        $groupedIds = [];
667
668 62
        $sorted = isset($mapping['sort']) && $mapping['sort'];
669
670 62
        foreach ($collection->getMongoData() as $key => $reference) {
671 56
            $className  = $this->uow->getClassNameForAssociation($mapping, $reference);
672 56
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
673 56
            $id         = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
674
675
            // create a reference to the class and id
676 56
            $reference = $this->dm->getReference($className, $id);
677
678
            // no custom sort so add the references right now in the order they are embedded
679 56
            if (! $sorted) {
680 55
                if (CollectionHelper::isHash($mapping['strategy'])) {
681 2
                    $collection->set($key, $reference);
682
                } else {
683 53
                    $collection->add($reference);
684
                }
685
            }
686
687
            // only query for the referenced object if it is not already initialized or the collection is sorted
688 56
            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...
689 22
                continue;
690
            }
691
692 41
            $groupedIds[$className][] = $identifier;
693
        }
694 62
        foreach ($groupedIds as $className => $ids) {
695 41
            $class           = $this->dm->getClassMetadata($className);
696 41
            $mongoCollection = $this->dm->getDocumentCollection($className);
697 41
            $criteria        = $this->cm->merge(
698 41
                ['_id' => ['$in' => array_values($ids)]],
699 41
                $this->dm->getFilterCollection()->getFilterCriteria($class),
700 41
                $mapping['criteria'] ?? []
701
            );
702 41
            $criteria        = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
703
704 41
            $options = [];
705 41
            if (isset($mapping['sort'])) {
706 41
                $options['sort'] = $this->prepareSort($mapping['sort']);
707
            }
708 41
            if (isset($mapping['limit'])) {
709
                $options['limit'] = $mapping['limit'];
710
            }
711 41
            if (isset($mapping['skip'])) {
712
                $options['skip'] = $mapping['skip'];
713
            }
714 41
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
715
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
716
            }
717
718 41
            $cursor    = $mongoCollection->find($criteria, $options);
719 41
            $documents = $cursor->toArray();
720 41
            foreach ($documents as $documentData) {
721 40
                $document = $this->uow->getById($documentData['_id'], $class);
722 40
                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...
723 40
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
724 40
                    $this->uow->setOriginalDocumentData($document, $data);
725
                }
726
727 40
                if (! $sorted) {
728 39
                    continue;
729
                }
730
731 41
                $collection->add($document);
732
            }
733
        }
734 62
    }
735
736 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) : void
737
    {
738 17
        $query     = $this->createReferenceManyInverseSideQuery($collection);
739 17
        $iterator = $query->execute();
740 17
        assert($iterator instanceof Iterator);
741 17
        $documents = $iterator->toArray();
742 17
        foreach ($documents as $key => $document) {
743 16
            $collection->add($document);
744
        }
745 17
    }
746
747 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) : Query
748
    {
749 17
        $hints             = $collection->getHints();
750 17
        $mapping           = $collection->getMapping();
751 17
        $owner             = $collection->getOwner();
752 17
        $ownerClass        = $this->dm->getClassMetadata(get_class($owner));
753 17
        $targetClass       = $this->dm->getClassMetadata($mapping['targetDocument']);
754 17
        $mappedByMapping   = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
755 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
756
757 17
        $criteria = $this->cm->merge(
758 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
759 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
760 17
            $mapping['criteria'] ?? []
761
        );
762 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
763 17
        $qb       = $this->dm->createQueryBuilder($mapping['targetDocument'])
764 17
            ->setQueryArray($criteria);
765
766 17
        if (isset($mapping['sort'])) {
767 17
            $qb->sort($mapping['sort']);
768
        }
769 17
        if (isset($mapping['limit'])) {
770 2
            $qb->limit($mapping['limit']);
771
        }
772 17
        if (isset($mapping['skip'])) {
773
            $qb->skip($mapping['skip']);
774
        }
775
776 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
777
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
778
        }
779
780 17
        foreach ($mapping['prime'] as $field) {
781 4
            $qb->field($field)->prime(true);
782
        }
783
784 17
        return $qb->getQuery();
785
    }
786
787 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) : void
788
    {
789 5
        $cursor    = $this->createReferenceManyWithRepositoryMethodCursor($collection);
790 5
        $mapping   = $collection->getMapping();
791 5
        $documents = $cursor->toArray();
792 5
        foreach ($documents as $key => $obj) {
793 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
794 1
                $collection->set($key, $obj);
795
            } else {
796 5
                $collection->add($obj);
797
            }
798
        }
799 5
    }
800
801 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) : Iterator
802
    {
803 5
        $mapping          = $collection->getMapping();
804 5
        $repositoryMethod = $mapping['repositoryMethod'];
805 5
        $cursor           = $this->dm->getRepository($mapping['targetDocument'])
806 5
            ->$repositoryMethod($collection->getOwner());
807
808 5
        if (! $cursor instanceof Iterator) {
809
            throw new BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
810
        }
811
812 5
        if (! empty($mapping['prime'])) {
813 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
814 1
            $primers         = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
815 1
            $class           = $this->dm->getClassMetadata($mapping['targetDocument']);
816
817 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
818
        }
819
820 5
        return $cursor;
821
    }
822
823
    /**
824
     * Prepare a projection array by converting keys, which are PHP property
825
     * names, to MongoDB field names.
826
     */
827 14
    public function prepareProjection(array $fields) : array
828
    {
829 14
        $preparedFields = [];
830
831 14
        foreach ($fields as $key => $value) {
832 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
833
        }
834
835 14
        return $preparedFields;
836
    }
837
838
    /**
839
     * @param int|string $sort
840
     *
841
     * @return int|string|null
842
     */
843 25
    private function getSortDirection($sort)
844
    {
845 25
        switch (strtolower((string) $sort)) {
846 25
            case 'desc':
847 15
                return -1;
848
849 22
            case 'asc':
850 13
                return 1;
851
        }
852
853 12
        return $sort;
854
    }
855
856
    /**
857
     * Prepare a sort specification array by converting keys to MongoDB field
858
     * names and changing direction strings to int.
859
     */
860 144
    public function prepareSort(array $fields) : array
861
    {
862 144
        $sortFields = [];
863
864 144
        foreach ($fields as $key => $value) {
865 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
866
        }
867
868 144
        return $sortFields;
869
    }
870
871
    /**
872
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
873
     */
874 436
    public function prepareFieldName(string $fieldName) : string
875
    {
876 436
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
877
878 436
        return $fieldNames[0][0];
879
    }
880
881
    /**
882
     * Adds discriminator criteria to an already-prepared query.
883
     *
884
     * This method should be used once for query criteria and not be used for
885
     * nested expressions. It should be called before
886
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
887
     */
888 525
    public function addDiscriminatorToPreparedQuery(array $preparedQuery) : array
889
    {
890
        /* If the class has a discriminator field, which is not already in the
891
         * criteria, inject it now. The field/values need no preparation.
892
         */
893 525
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
894 27
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
895 27
            if (count($discriminatorValues) === 1) {
896 19
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
897
            } else {
898 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
899
            }
900
        }
901
902 525
        return $preparedQuery;
903
    }
904
905
    /**
906
     * Adds filter criteria to an already-prepared query.
907
     *
908
     * This method should be used once for query criteria and not be used for
909
     * nested expressions. It should be called after
910
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
911
     */
912 526
    public function addFilterToPreparedQuery(array $preparedQuery) : array
913
    {
914
        /* If filter criteria exists for this class, prepare it and merge
915
         * over the existing query.
916
         *
917
         * @todo Consider recursive merging in case the filter criteria and
918
         * prepared query both contain top-level $and/$or operators.
919
         */
920 526
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
921 526
        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...
922 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
923
        }
924
925 526
        return $preparedQuery;
926
    }
927
928
    /**
929
     * Prepares the query criteria or new document object.
930
     *
931
     * PHP field names and types will be converted to those used by MongoDB.
932
     */
933 558
    public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : array
934
    {
935 558
        $preparedQuery = [];
936
937 558
        foreach ($query as $key => $value) {
938
            // Recursively prepare logical query clauses
939 517
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
940 20
                foreach ($value as $k2 => $v2) {
941 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
942
                }
943 20
                continue;
944
            }
945
946 517
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
947 40
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
948 40
                continue;
949
            }
950
951 517
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
952 517
            foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
953 517
                $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...
954 135
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
955 517
                    : Type::convertPHPToDatabaseValue($preparedValue);
956
            }
957
        }
958
959 558
        return $preparedQuery;
960
    }
961
962
    /**
963
     * Prepares a query value and converts the PHP value to the database value
964
     * if it is an identifier.
965
     *
966
     * It also handles converting $fieldName to the database name if they are different.
967
     *
968
     * @param mixed $value
969
     */
970 913
    private function prepareQueryElement(string $fieldName, $value = null, ?ClassMetadata $class = null, bool $prepareValue = true, bool $inNewObj = false) : array
971
    {
972 913
        $class = $class ?? $this->class;
973
974
        // @todo Consider inlining calls to ClassMetadata methods
975
976
        // Process all non-identifier fields by translating field names
977 913
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
978 257
            $mapping   = $class->fieldMappings[$fieldName];
979 257
            $fieldName = $mapping['name'];
980
981 257
            if (! $prepareValue) {
982 52
                return [[$fieldName, $value]];
983
            }
984
985
            // Prepare mapped, embedded objects
986 215
            if (! empty($mapping['embedded']) && is_object($value) &&
987 215
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
988 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
989
            }
990
991 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...
992
                try {
993 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
994 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...
995
                    // do nothing in case passed object is not mapped document
996
                }
997
            }
998
999
            // No further preparation unless we're dealing with a simple reference
1000
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1001 200
            $arrayValue = (array) $value;
1002 200
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1003 133
                return [[$fieldName, $value]];
1004
            }
1005
1006
            // Additional preparation for one or more simple reference values
1007 94
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1008
1009 94
            if (! is_array($value)) {
1010 90
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1011
            }
1012
1013
            // Objects without operators or with DBRef fields can be converted immediately
1014 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1015 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1016
            }
1017
1018 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1019
        }
1020
1021
        // Process identifier fields
1022 822
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1023 361
            $fieldName = '_id';
1024
1025 361
            if (! $prepareValue) {
1026 42
                return [[$fieldName, $value]];
1027
            }
1028
1029 322
            if (! is_array($value)) {
1030 296
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1031
            }
1032
1033
            // Objects without operators or with DBRef fields can be converted immediately
1034 63
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1035 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1036
            }
1037
1038 58
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1039
        }
1040
1041
        // No processing for unmapped, non-identifier, non-dotted field names
1042 561
        if (strpos($fieldName, '.') === false) {
1043 416
            return [[$fieldName, $value]];
1044
        }
1045
1046
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1047
         *
1048
         * We can limit parsing here, since at most three segments are
1049
         * significant: "fieldName.objectProperty" with an optional index or key
1050
         * for collections stored as either BSON arrays or objects.
1051
         */
1052 157
        $e = explode('.', $fieldName, 4);
1053
1054
        // No further processing for unmapped fields
1055 157
        if (! isset($class->fieldMappings[$e[0]])) {
1056 6
            return [[$fieldName, $value]];
1057
        }
1058
1059 152
        $mapping = $class->fieldMappings[$e[0]];
1060 152
        $e[0]    = $mapping['name'];
1061
1062
        // Hash and raw fields will not be prepared beyond the field name
1063 152
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1064 1
            $fieldName = implode('.', $e);
1065
1066 1
            return [[$fieldName, $value]];
1067
        }
1068
1069 151
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1070 151
                && isset($e[2])) {
1071 1
            $objectProperty       = $e[2];
1072 1
            $objectPropertyPrefix = $e[1] . '.';
1073 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1074 150
        } elseif ($e[1] !== '$') {
1075 149
            $fieldName            = $e[0] . '.' . $e[1];
1076 149
            $objectProperty       = $e[1];
1077 149
            $objectPropertyPrefix = '';
1078 149
            $nextObjectProperty   = implode('.', array_slice($e, 2));
1079 1
        } elseif (isset($e[2])) {
1080 1
            $fieldName            = $e[0] . '.' . $e[1] . '.' . $e[2];
1081 1
            $objectProperty       = $e[2];
1082 1
            $objectPropertyPrefix = $e[1] . '.';
1083 1
            $nextObjectProperty   = implode('.', array_slice($e, 3));
1084
        } else {
1085 1
            $fieldName = $e[0] . '.' . $e[1];
1086
1087 1
            return [[$fieldName, $value]];
1088
        }
1089
1090
        // No further processing for fields without a targetDocument mapping
1091 151
        if (! isset($mapping['targetDocument'])) {
1092 3
            if ($nextObjectProperty) {
1093
                $fieldName .= '.' . $nextObjectProperty;
1094
            }
1095
1096 3
            return [[$fieldName, $value]];
1097
        }
1098
1099 148
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1100
1101
        // No further processing for unmapped targetDocument fields
1102 148
        if (! $targetClass->hasField($objectProperty)) {
1103 25
            if ($nextObjectProperty) {
1104
                $fieldName .= '.' . $nextObjectProperty;
1105
            }
1106
1107 25
            return [[$fieldName, $value]];
1108
        }
1109
1110 128
        $targetMapping      = $targetClass->getFieldMapping($objectProperty);
1111 128
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1112
1113
        // Prepare DBRef identifiers or the mapped field's property path
1114 128
        $fieldName = $objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID
1115 108
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1116 128
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1117
1118
        // Process targetDocument identifier fields
1119 128
        if ($objectPropertyIsId) {
1120 109
            if (! $prepareValue) {
1121 7
                return [[$fieldName, $value]];
1122
            }
1123
1124 102
            if (! is_array($value)) {
1125 88
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1126
            }
1127
1128
            // Objects without operators or with DBRef fields can be converted immediately
1129 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1130 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1131
            }
1132
1133 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1134
        }
1135
1136
        /* The property path may include a third field segment, excluding the
1137
         * collection item pointer. If present, this next object property must
1138
         * be processed recursively.
1139
         */
1140 19
        if ($nextObjectProperty) {
1141
            // Respect the targetDocument's class metadata when recursing
1142 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1143 10
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1144 16
                : null;
1145
1146 16
            if (empty($targetMapping['reference'])) {
1147 14
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1148
            } else {
1149
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1150 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1151 1
                    $nextObjectProperty = '$' . $nextObjectProperty;
1152
                }
1153 4
                $fieldNames = [[$nextObjectProperty, $value]];
1154
            }
1155
1156
            return array_map(static function ($preparedTuple) use ($fieldName) {
1157 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...
1158
1159 16
                return [$fieldName . '.' . $key, $value];
1160 16
            }, $fieldNames);
1161
        }
1162
1163 5
        return [[$fieldName, $value]];
1164
    }
1165
1166
    /**
1167
     * Prepares a query expression.
1168
     *
1169
     * @param array|object $expression
1170
     */
1171 80
    private function prepareQueryExpression($expression, ClassMetadata $class) : array
1172
    {
1173 80
        foreach ($expression as $k => $v) {
1174
            // Ignore query operators whose arguments need no type conversion
1175 80
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1176 16
                continue;
1177
            }
1178
1179
            // Process query operators whose argument arrays need type conversion
1180 80
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1181 78
                foreach ($v as $k2 => $v2) {
1182 78
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1183
                }
1184 78
                continue;
1185
            }
1186
1187
            // Recursively process expressions within a $not operator
1188 18
            if ($k === '$not' && is_array($v)) {
1189 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1190 15
                continue;
1191
            }
1192
1193 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1194
        }
1195
1196 80
        return $expression;
1197
    }
1198
1199
    /**
1200
     * Checks whether the value has DBRef fields.
1201
     *
1202
     * This method doesn't check if the the value is a complete DBRef object,
1203
     * although it should return true for a DBRef. Rather, we're checking that
1204
     * the value has one or more fields for a DBref. In practice, this could be
1205
     * $elemMatch criteria for matching a DBRef.
1206
     *
1207
     * @param mixed $value
1208
     */
1209 81
    private function hasDBRefFields($value) : bool
1210
    {
1211 81
        if (! is_array($value) && ! is_object($value)) {
1212
            return false;
1213
        }
1214
1215 81
        if (is_object($value)) {
1216
            $value = get_object_vars($value);
1217
        }
1218
1219 81
        foreach ($value as $key => $_) {
1220 81
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1221 81
                return true;
1222
            }
1223
        }
1224
1225 80
        return false;
1226
    }
1227
1228
    /**
1229
     * Checks whether the value has query operators.
1230
     *
1231
     * @param mixed $value
1232
     */
1233 85
    private function hasQueryOperators($value) : bool
1234
    {
1235 85
        if (! is_array($value) && ! is_object($value)) {
1236
            return false;
1237
        }
1238
1239 85
        if (is_object($value)) {
1240
            $value = get_object_vars($value);
1241
        }
1242
1243 85
        foreach ($value as $key => $_) {
1244 85
            if (isset($key[0]) && $key[0] === '$') {
1245 85
                return true;
1246
            }
1247
        }
1248
1249 11
        return false;
1250
    }
1251
1252
    /**
1253
     * Gets the array of discriminator values for the given ClassMetadata
1254
     */
1255 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata) : array
1256
    {
1257 27
        $discriminatorValues = [$metadata->discriminatorValue];
1258 27
        foreach ($metadata->subClasses as $className) {
1259 8
            $key = array_search($className, $metadata->discriminatorMap);
1260 8
            if (! $key) {
1261
                continue;
1262
            }
1263
1264 8
            $discriminatorValues[] = $key;
1265
        }
1266
1267
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1268 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...
1269 2
            $discriminatorValues[] = null;
1270
        }
1271
1272 27
        return $discriminatorValues;
1273
    }
1274
1275 582
    private function handleCollections(object $document, array $options) : void
1276
    {
1277
        // Collection deletions (deletions of complete collections)
1278 582
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1279 107
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1280 96
                continue;
1281
            }
1282
1283 31
            $this->cp->delete($coll, $options);
1284
        }
1285
        // Collection updates (deleteRows, updateRows, insertRows)
1286 582
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1287 107
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1288 29
                continue;
1289
            }
1290
1291 99
            $this->cp->update($coll, $options);
1292
        }
1293
        // Take new snapshots from visited collections
1294 582
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1295 250
            $coll->takeSnapshot();
1296
        }
1297 582
    }
1298
1299
    /**
1300
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1301
     * Also, shard key field should be present in actual document data.
1302
     *
1303
     * @throws MongoDBException
1304
     */
1305 10
    private function guardMissingShardKey(object $document, string $shardKeyField, array $actualDocumentData) : void
1306
    {
1307 10
        $dcs      = $this->uow->getDocumentChangeSet($document);
1308 10
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1309
1310 10
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1311 10
        $fieldName    = $fieldMapping['fieldName'];
1312
1313 10
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1314 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...
1315
        }
1316
1317 8
        if (! isset($actualDocumentData[$fieldName])) {
1318
            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...
1319
        }
1320 8
    }
1321
1322
    /**
1323
     * Get shard key aware query for single document.
1324
     */
1325 295
    private function getQueryForDocument(object $document) : array
1326
    {
1327 295
        $id = $this->uow->getDocumentIdentifier($document);
1328 295
        $id = $this->class->getDatabaseIdentifierValue($id);
1329
1330 295
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1331 293
        return array_merge(['_id' => $id], $shardKeyQueryPart);
1332
    }
1333
1334 583
    private function getWriteOptions(array $options = []) : array
1335
    {
1336 583
        $defaultOptions  = $this->dm->getConfiguration()->getDefaultCommitOptions();
1337 583
        $documentOptions = [];
1338 583
        if ($this->class->hasWriteConcern()) {
1339 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1340
        }
1341
1342 583
        return array_merge($defaultOptions, $documentOptions, $options);
1343
    }
1344
1345 15
    private function prepareReference(string $fieldName, $value, array $mapping, bool $inNewObj) : array
1346
    {
1347 15
        $reference = $this->dm->createReference($value, $mapping);
1348 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1349 8
            return [[$fieldName, $reference]];
1350
        }
1351
1352 6
        switch ($mapping['storeAs']) {
1353
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1354
                $keys = ['id' => true];
1355
                break;
1356
1357
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1358
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1359 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1360
1361 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1362 5
                    unset($keys['$db']);
1363
                }
1364
1365 6
                if (isset($mapping['targetDocument'])) {
1366 4
                    unset($keys['$ref'], $keys['$db']);
1367
                }
1368 6
                break;
1369
1370
            default:
1371
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1372
        }
1373
1374 6
        if ($mapping['type'] === 'many') {
1375 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1376
        }
1377
1378 4
        return array_map(
1379
            static function ($key) use ($reference, $fieldName) {
1380 4
                return [$fieldName . '.' . $key, $reference[$key]];
1381 4
            },
1382 4
            array_keys($keys)
1383
        );
1384
    }
1385
}
1386