Completed
Pull Request — master (#1757)
by Maciej
15:59
created

DocumentPersister::getQueryForDocument()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
757 21
                continue;
758
            }
759
760 39
            $groupedIds[$className][] = $identifier;
761
        }
762 60
        foreach ($groupedIds as $className => $ids) {
763 39
            $class = $this->dm->getClassMetadata($className);
764 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
765 39
            $criteria = $this->cm->merge(
766 39
                ['_id' => ['$in' => array_values($ids)]],
767 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
768 39
                $mapping['criteria'] ?? []
769
            );
770 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
771
772 39
            $options = [];
773 39
            if (isset($mapping['sort'])) {
774 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
775
            }
776 39
            if (isset($mapping['limit'])) {
777
                $options['limit'] = $mapping['limit'];
778
            }
779 39
            if (isset($mapping['skip'])) {
780
                $options['skip'] = $mapping['skip'];
781
            }
782 39
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
783
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
784
            }
785
786 39
            $cursor = $mongoCollection->find($criteria, $options);
787 39
            $documents = $cursor->toArray();
788 39
            foreach ($documents as $documentData) {
789 38
                $document = $this->uow->getById($documentData['_id'], $class);
790 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
791 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
792 38
                    $this->uow->setOriginalDocumentData($document, $data);
793 38
                    $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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