Completed
Pull Request — master (#1767)
by Andreas
09:40
created

DocumentPersister::prepareReference()   D

Complexity

Conditions 9
Paths 20

Size

Total Lines 40
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9.2363

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 18
cts 21
cp 0.8571
rs 4.909
c 0
b 0
f 0
cc 9
eloc 24
nc 20
nop 4
crap 9.2363
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Doctrine\Common\Persistence\Mapping\MappingException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
10
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
11
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
12
use Doctrine\ODM\MongoDB\Iterator\Iterator;
13
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
14
use Doctrine\ODM\MongoDB\LockException;
15
use Doctrine\ODM\MongoDB\LockMode;
16
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
17
use Doctrine\ODM\MongoDB\MongoDBException;
18
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
19
use Doctrine\ODM\MongoDB\Proxy\Proxy;
20
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
21
use Doctrine\ODM\MongoDB\Query\Query;
22
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
23
use Doctrine\ODM\MongoDB\Types\Type;
24
use Doctrine\ODM\MongoDB\UnitOfWork;
25
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
26
use MongoDB\BSON\ObjectId;
27
use MongoDB\Collection;
28
use MongoDB\Driver\Cursor;
29
use MongoDB\Driver\Exception\Exception as DriverException;
30
use MongoDB\Driver\Exception\WriteException;
31
use function array_combine;
32
use function array_fill;
33
use function array_intersect_key;
34
use function array_keys;
35
use function array_map;
36
use function array_merge;
37
use function array_search;
38
use function array_slice;
39
use function array_values;
40
use function count;
41
use function explode;
42
use function get_class;
43
use function get_object_vars;
44
use function implode;
45
use function in_array;
46
use function is_array;
47
use function is_object;
48
use function is_scalar;
49
use function max;
50
use function spl_object_hash;
51
use function sprintf;
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 UnitOfWork instance.
77
     *
78
     * @var UnitOfWork
79
     */
80
    private $uow;
81
82
    /**
83
     * The ClassMetadata instance for the document type being persisted.
84
     *
85
     * @var ClassMetadata
86
     */
87
    private $class;
88
89
    /**
90
     * The MongoCollection instance for this document.
91
     *
92
     * @var Collection
93
     */
94
    private $collection;
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedInserts = [];
102
103
    /**
104
     * Array of queued inserts for the persister to insert.
105
     *
106
     * @var array
107
     */
108
    private $queuedUpserts = [];
109
110
    /**
111
     * The CriteriaMerger instance.
112
     *
113
     * @var CriteriaMerger
114
     */
115
    private $cm;
116
117
    /**
118
     * The CollectionPersister instance.
119
     *
120
     * @var CollectionPersister
121
     */
122
    private $cp;
123
124
    /**
125
     * The HydratorFactory instance.
126
     *
127
     * @var HydratorFactory
128
     */
129
    private $hydratorFactory;
130
131
    /**
132
     * Initializes this instance.
133
     *
134
     */
135 1073
    public function __construct(
136
        PersistenceBuilder $pb,
137
        DocumentManager $dm,
138
        UnitOfWork $uow,
139
        HydratorFactory $hydratorFactory,
140
        ClassMetadata $class,
141
        ?CriteriaMerger $cm = null
142
    ) {
143 1073
        $this->pb = $pb;
144 1073
        $this->dm = $dm;
145 1073
        $this->cm = $cm ?: new CriteriaMerger();
146 1073
        $this->uow = $uow;
147 1073
        $this->hydratorFactory = $hydratorFactory;
148 1073
        $this->class = $class;
149 1073
        $this->collection = $dm->getDocumentCollection($class->name);
150 1073
        $this->cp = $this->uow->getCollectionPersister();
151 1073
    }
152
153
    /**
154
     * @return array
155
     */
156
    public function getInserts()
157
    {
158
        return $this->queuedInserts;
159
    }
160
161
    /**
162
     * @param object $document
163
     * @return bool
164
     */
165
    public function isQueuedForInsert($document)
166
    {
167
        return isset($this->queuedInserts[spl_object_hash($document)]);
168
    }
169
170
    /**
171
     * Adds a document to the queued insertions.
172
     * The document remains queued until {@link executeInserts} is invoked.
173
     *
174
     * @param object $document The document to queue for insertion.
175
     */
176 476
    public function addInsert($document)
177
    {
178 476
        $this->queuedInserts[spl_object_hash($document)] = $document;
179 476
    }
180
181
    /**
182
     * @return array
183
     */
184
    public function getUpserts()
185
    {
186
        return $this->queuedUpserts;
187
    }
188
189
    /**
190
     * @param object $document
191
     * @return bool
192
     */
193
    public function isQueuedForUpsert($document)
194
    {
195
        return isset($this->queuedUpserts[spl_object_hash($document)]);
196
    }
197
198
    /**
199
     * Adds a document to the queued upserts.
200
     * The document remains queued until {@link executeUpserts} is invoked.
201
     *
202
     * @param object $document The document to queue for insertion.
203
     */
204 83
    public function addUpsert($document)
205
    {
206 83
        $this->queuedUpserts[spl_object_hash($document)] = $document;
207 83
    }
208
209
    /**
210
     * Gets the ClassMetadata instance of the document class this persister is used for.
211
     *
212
     * @return ClassMetadata
213
     */
214
    public function getClassMetadata()
215
    {
216
        return $this->class;
217
    }
218
219
    /**
220
     * Executes all queued document insertions.
221
     *
222
     * Queued documents without an ID will inserted in a batch and queued
223
     * documents with an ID will be upserted individually.
224
     *
225
     * If no inserts are queued, invoking this method is a NOOP.
226
     *
227
     * @param array $options Options for batchInsert() and update() driver methods
228
     */
229 476
    public function executeInserts(array $options = [])
230
    {
231 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...
232
            return;
233
        }
234
235 476
        $inserts = [];
236 476
        $options = $this->getWriteOptions($options);
237 476
        foreach ($this->queuedInserts as $oid => $document) {
238 476
            $data = $this->pb->prepareInsertData($document);
239
240
            // Set the initial version for each insert
241 475
            if ($this->class->isVersioned) {
242 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
243 20
                $nextVersion = null;
244 20
                if ($versionMapping['type'] === 'int') {
245 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
246 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
247 2
                } elseif ($versionMapping['type'] === 'date') {
248 2
                    $nextVersionDateTime = new \DateTime();
249 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
250 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
251
                }
252 20
                $data[$versionMapping['name']] = $nextVersion;
253
            }
254
255 475
            $inserts[] = $data;
256
        }
257
258 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...
259
            try {
260 475
                $this->collection->insertMany($inserts, $options);
261 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...
262 6
                $this->queuedInserts = [];
263 6
                throw $e;
264
            }
265
        }
266
267
        /* All collections except for ones using addToSet have already been
268
         * saved. We have left these to be handled separately to avoid checking
269
         * collection for uniqueness on PHP side.
270
         */
271 475
        foreach ($this->queuedInserts as $document) {
272 475
            $this->handleCollections($document, $options);
273
        }
274
275 475
        $this->queuedInserts = [];
276 475
    }
277
278
    /**
279
     * Executes all queued document upserts.
280
     *
281
     * Queued documents with an ID are upserted individually.
282
     *
283
     * If no upserts are queued, invoking this method is a NOOP.
284
     *
285
     * @param array $options Options for batchInsert() and update() driver methods
286
     */
287 83
    public function executeUpserts(array $options = [])
288
    {
289 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...
290
            return;
291
        }
292
293 83
        $options = $this->getWriteOptions($options);
294 83
        foreach ($this->queuedUpserts as $oid => $document) {
295
            try {
296 83
                $this->executeUpsert($document, $options);
297 83
                $this->handleCollections($document, $options);
298 83
                unset($this->queuedUpserts[$oid]);
299
            } 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...
300
                unset($this->queuedUpserts[$oid]);
301 83
                throw $e;
302
            }
303
        }
304 83
    }
305
306
    /**
307
     * Executes a single upsert in {@link executeUpserts}
308
     *
309
     * @param object $document
310
     * @param array  $options
311
     */
312 83
    private function executeUpsert($document, array $options)
313
    {
314 83
        $options['upsert'] = true;
315 83
        $criteria = $this->getQueryForDocument($document);
316
317 83
        $data = $this->pb->prepareUpsertData($document);
318
319
        // Set the initial version for each upsert
320 83
        if ($this->class->isVersioned) {
321 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
322 2
            $nextVersion = null;
323 2
            if ($versionMapping['type'] === 'int') {
324 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
325 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
326 1
            } elseif ($versionMapping['type'] === 'date') {
327 1
                $nextVersionDateTime = new \DateTime();
328 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
329 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
330
            }
331 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
332
        }
333
334 83
        foreach (array_keys($criteria) as $field) {
335 83
            unset($data['$set'][$field]);
336 83
            unset($data['$inc'][$field]);
337 83
            unset($data['$setOnInsert'][$field]);
338
        }
339
340
        // Do not send empty update operators
341 83
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
342 83
            if (empty($data[$operator])) {
343 83
                unset($data[$operator]);
344
            }
345
        }
346
347
        /* If there are no modifiers remaining, we're upserting a document with
348
         * an identifier as its only field. Since a document with the identifier
349
         * may already exist, the desired behavior is "insert if not exists" and
350
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
351
         * the identifier to the same value in our criteria.
352
         *
353
         * This will fail for versions before MongoDB 2.6, which require an
354
         * empty $set modifier. The best we can do (without attempting to check
355
         * server versions in advance) is attempt the 2.6+ behavior and retry
356
         * after the relevant exception.
357
         *
358
         * See: https://jira.mongodb.org/browse/SERVER-12266
359
         */
360 83
        if (empty($data)) {
361 16
            $retry = true;
362 16
            $data = ['$set' => ['_id' => $criteria['_id']]];
363
        }
364
365
        try {
366 83
            $this->collection->updateOne($criteria, $data, $options);
367 83
            return;
368
        } 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...
369
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
370
                throw $e;
371
            }
372
        }
373
374
        $this->collection->updateOne($criteria, ['$set' => new \stdClass()], $options);
375
    }
376
377
    /**
378
     * Updates the already persisted document if it has any new changesets.
379
     *
380
     * @param object $document
381
     * @param array  $options  Array of options to be used with update()
382
     * @throws LockException
383
     */
384 195
    public function update($document, array $options = [])
385
    {
386 195
        $update = $this->pb->prepareUpdateData($document);
387
388 195
        $query = $this->getQueryForDocument($document);
389
390 195
        foreach (array_keys($query) as $field) {
391 195
            unset($update['$set'][$field]);
392
        }
393
394 195
        if (empty($update['$set'])) {
395 89
            unset($update['$set']);
396
        }
397
398
        // Include versioning logic to set the new version value in the database
399
        // and to ensure the version has not changed since this document object instance
400
        // was fetched from the database
401 195
        $nextVersion = null;
402 195
        if ($this->class->isVersioned) {
403 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
404 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
405 13
            if ($versionMapping['type'] === 'int') {
406 10
                $nextVersion = $currentVersion + 1;
407 10
                $update['$inc'][$versionMapping['name']] = 1;
408 10
                $query[$versionMapping['name']] = $currentVersion;
409 3
            } elseif ($versionMapping['type'] === 'date') {
410 3
                $nextVersion = new \DateTime();
411 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
412 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
413
            }
414
        }
415
416 195
        if (! empty($update)) {
417
            // Include locking logic so that if the document object in memory is currently
418
            // locked then it will remove it, otherwise it ensures the document is not locked.
419 129
            if ($this->class->isLockable) {
420 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
421 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
422 11
                if ($isLocked) {
423 2
                    $update['$unset'] = [$lockMapping['name'] => true];
424
                } else {
425 9
                    $query[$lockMapping['name']] = ['$exists' => false];
426
                }
427
            }
428
429 129
            $options = $this->getWriteOptions($options);
430
431 129
            $result = $this->collection->updateOne($query, $update, $options);
432
433 129
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
434 4
                throw LockException::lockFailed($document);
435 125
            } elseif ($this->class->isVersioned) {
436 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
437
            }
438
        }
439
440 191
        $this->handleCollections($document, $options);
441 191
    }
442
443
    /**
444
     * Removes document from mongo
445
     *
446
     * @param mixed $document
447
     * @param array $options  Array of options to be used with remove()
448
     * @throws LockException
449
     */
450 32
    public function delete($document, array $options = [])
451
    {
452 32
        $query = $this->getQueryForDocument($document);
453
454 32
        if ($this->class->isLockable) {
455 2
            $query[$this->class->lockField] = ['$exists' => false];
456
        }
457
458 32
        $options = $this->getWriteOptions($options);
459
460 32
        $result = $this->collection->deleteOne($query, $options);
461
462 32
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
463 2
            throw LockException::lockFailed($document);
464
        }
465 30
    }
466
467
    /**
468
     * Refreshes a managed document.
469
     *
470
     * @param object $document The document to refresh.
471
     */
472 20
    public function refresh($document)
473
    {
474 20
        $query = $this->getQueryForDocument($document);
475 20
        $data = $this->collection->findOne($query);
476 20
        $data = $this->hydratorFactory->hydrate($document, $data);
477 20
        $this->uow->setOriginalDocumentData($document, $data);
478 20
    }
479
480
    /**
481
     * Finds a document by a set of criteria.
482
     *
483
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
484
     * be used to match an _id value.
485
     *
486
     * @param mixed  $criteria Query criteria
487
     * @param object $document Document to load the data into. If not specified, a new document is created.
488
     * @param array  $hints    Hints for document creation
489
     * @param int    $lockMode
490
     * @param array  $sort     Sort array for Cursor::sort()
491
     * @throws LockException
492
     * @return object|null The loaded and managed document instance or null if no document was found
493
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
494
     */
495 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...
496
    {
497
        // TODO: remove this
498 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...
499
            $criteria = ['_id' => $criteria];
500
        }
501
502 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...
503 342
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
504 342
        $criteria = $this->addFilterToPreparedQuery($criteria);
505
506 342
        $options = [];
507 342
        if ($sort !== null) {
508 92
            $options['sort'] = $this->prepareSort($sort);
509
        }
510 342
        $result = $this->collection->findOne($criteria, $options);
511
512 342
        if ($this->class->isLockable) {
513 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
514 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
515 1
                throw LockException::lockFailed($result);
516
            }
517
        }
518
519 341
        return $this->createDocument($result, $document, $hints);
520
    }
521
522
    /**
523
     * Finds documents by a set of criteria.
524
     *
525
     * @param array    $criteria Query criteria
526
     * @param array    $sort     Sort array for Cursor::sort()
527
     * @param int|null $limit    Limit for Cursor::limit()
528
     * @param int|null $skip     Skip for Cursor::skip()
529
     * @return Iterator
530
     */
531 22
    public function loadAll(array $criteria = [], ?array $sort = null, $limit = null, $skip = null)
532
    {
533 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
534 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
535 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
536
537 22
        $options = [];
538 22
        if ($sort !== null) {
539 11
            $options['sort'] = $this->prepareSort($sort);
540
        }
541
542 22
        if ($limit !== null) {
543 10
            $options['limit'] = $limit;
544
        }
545
546 22
        if ($skip !== null) {
547 1
            $options['skip'] = $skip;
548
        }
549
550 22
        $baseCursor = $this->collection->find($criteria, $options);
551 22
        $cursor = $this->wrapCursor($baseCursor);
552
553 22
        return $cursor;
554
    }
555
556
    /**
557
     * @param object $document
558
     *
559
     * @return array
560
     * @throws MongoDBException
561
     */
562 267
    private function getShardKeyQuery($document)
563
    {
564 267
        if (! $this->class->isSharded()) {
565 263
            return [];
566
        }
567
568 4
        $shardKey = $this->class->getShardKey();
569 4
        $keys = array_keys($shardKey['keys']);
570 4
        $data = $this->uow->getDocumentActualData($document);
571
572 4
        $shardKeyQueryPart = [];
573 4
        foreach ($keys as $key) {
574 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
575 4
            $this->guardMissingShardKey($document, $key, $data);
576
577 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
578 1
                $reference = $this->prepareReference(
579 1
                    $key,
580 1
                    $data[$mapping['fieldName']],
581 1
                    $mapping,
582 1
                    false
583
                );
584 1
                foreach ($reference as $keyValue) {
585 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
586
                }
587
            } else {
588 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
589 4
                $shardKeyQueryPart[$key] = $value;
590
            }
591
        }
592
593 4
        return $shardKeyQueryPart;
594
    }
595
596
    /**
597
     * Wraps the supplied base cursor in the corresponding ODM class.
598
     *
599
     */
600 22
    private function wrapCursor(Cursor $baseCursor): Iterator
601
    {
602 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
603
    }
604
605
    /**
606
     * Checks whether the given managed document exists in the database.
607
     *
608
     * @param object $document
609
     * @return bool TRUE if the document exists in the database, FALSE otherwise.
610
     */
611 3
    public function exists($document)
612
    {
613 3
        $id = $this->class->getIdentifierObject($document);
614 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
615
    }
616
617
    /**
618
     * Locks document by storing the lock mode on the mapped lock field.
619
     *
620
     * @param object $document
621
     * @param int    $lockMode
622
     */
623 5
    public function lock($document, $lockMode)
624
    {
625 5
        $id = $this->uow->getDocumentIdentifier($document);
626 5
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
627 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
628 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
629 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
630 5
    }
631
632
    /**
633
     * Releases any lock that exists on this document.
634
     *
635
     * @param object $document
636
     */
637 1
    public function unlock($document)
638
    {
639 1
        $id = $this->uow->getDocumentIdentifier($document);
640 1
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
641 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
642 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
643 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
644 1
    }
645
646
    /**
647
     * Creates or fills a single document object from an query result.
648
     *
649
     * @param object $result   The query result.
650
     * @param object $document The document object to fill, if any.
651
     * @param array  $hints    Hints for document creation.
652
     * @return object The filled and managed document object or NULL, if the query result is empty.
653
     */
654 341
    private function createDocument($result, $document = null, array $hints = [])
655
    {
656 341
        if ($result === null) {
657 111
            return null;
658
        }
659
660 300
        if ($document !== null) {
661 38
            $hints[Query::HINT_REFRESH] = true;
662 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
663 38
            $this->uow->registerManaged($document, $id, $result);
664
        }
665
666 300
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
667
    }
668
669
    /**
670
     * Loads a PersistentCollection data. Used in the initialize() method.
671
     *
672
     */
673 163
    public function loadCollection(PersistentCollectionInterface $collection)
674
    {
675 163
        $mapping = $collection->getMapping();
676 163
        switch ($mapping['association']) {
677
            case ClassMetadata::EMBED_MANY:
678 109
                $this->loadEmbedManyCollection($collection);
679 109
                break;
680
681
            case ClassMetadata::REFERENCE_MANY:
682 76
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
683 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
684
                } else {
685 72
                    if ($mapping['isOwningSide']) {
686 60
                        $this->loadReferenceManyCollectionOwningSide($collection);
687
                    } else {
688 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
689
                    }
690
                }
691 76
                break;
692
        }
693 163
    }
694
695 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
696
    {
697 109
        $embeddedDocuments = $collection->getMongoData();
698 109
        $mapping = $collection->getMapping();
699 109
        $owner = $collection->getOwner();
700 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...
701 58
            return;
702
        }
703
704 82
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
705 82
            $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
706 82
            $embeddedMetadata = $this->dm->getClassMetadata($className);
707 82
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
708
709 82
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
710
711 82
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
712 82
            $id = $data[$embeddedMetadata->identifier] ?? null;
713
714 82
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
715 81
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
716
            }
717 82
            if (CollectionHelper::isHash($mapping['strategy'])) {
718 10
                $collection->set($key, $embeddedDocumentObject);
719
            } else {
720 82
                $collection->add($embeddedDocumentObject);
721
            }
722
        }
723 82
    }
724
725 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
726
    {
727 60
        $hints = $collection->getHints();
728 60
        $mapping = $collection->getMapping();
729 60
        $groupedIds = [];
730
731 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
732
733 60
        foreach ($collection->getMongoData() as $key => $reference) {
734 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
735 54
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
736 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
737
738
            // create a reference to the class and id
739 54
            $reference = $this->dm->getReference($className, $id);
740
741
            // no custom sort so add the references right now in the order they are embedded
742 54
            if (! $sorted) {
743 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
744 2
                    $collection->set($key, $reference);
745
                } else {
746 51
                    $collection->add($reference);
747
                }
748
            }
749
750
            // only query for the referenced object if it is not already initialized or the collection is sorted
751 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...
752 21
                continue;
753
            }
754
755 39
            $groupedIds[$className][] = $identifier;
756
        }
757 60
        foreach ($groupedIds as $className => $ids) {
758 39
            $class = $this->dm->getClassMetadata($className);
759 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
760 39
            $criteria = $this->cm->merge(
761 39
                ['_id' => ['$in' => array_values($ids)]],
762 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
763 39
                $mapping['criteria'] ?? []
764
            );
765 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
766
767 39
            $options = [];
768 39
            if (isset($mapping['sort'])) {
769 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
770
            }
771 39
            if (isset($mapping['limit'])) {
772
                $options['limit'] = $mapping['limit'];
773
            }
774 39
            if (isset($mapping['skip'])) {
775
                $options['skip'] = $mapping['skip'];
776
            }
777 39
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
778
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
779
            }
780
781 39
            $cursor = $mongoCollection->find($criteria, $options);
782 39
            $documents = $cursor->toArray();
783 39
            foreach ($documents as $documentData) {
784 38
                $document = $this->uow->getById($documentData['_id'], $class);
785 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
786 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
787 38
                    $this->uow->setOriginalDocumentData($document, $data);
788 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...
789
                }
790 38
                if (! $sorted) {
791 37
                    continue;
792
                }
793
794 39
                $collection->add($document);
795
            }
796
        }
797 60
    }
798
799 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
800
    {
801 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
802 17
        $documents = $query->execute()->toArray();
803 17
        foreach ($documents as $key => $document) {
804 16
            $collection->add($document);
805
        }
806 17
    }
807
808
    /**
809
     *
810
     * @return Query
811
     */
812 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
813
    {
814 17
        $hints = $collection->getHints();
815 17
        $mapping = $collection->getMapping();
816 17
        $owner = $collection->getOwner();
817 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
818 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
819 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
820 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
821
822 17
        $criteria = $this->cm->merge(
823 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
824 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
825 17
            $mapping['criteria'] ?? []
826
        );
827 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
828 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
829 17
            ->setQueryArray($criteria);
830
831 17
        if (isset($mapping['sort'])) {
832 17
            $qb->sort($mapping['sort']);
833
        }
834 17
        if (isset($mapping['limit'])) {
835 2
            $qb->limit($mapping['limit']);
836
        }
837 17
        if (isset($mapping['skip'])) {
838
            $qb->skip($mapping['skip']);
839
        }
840
841 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
842
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
843
        }
844
845 17
        foreach ($mapping['prime'] as $field) {
846 4
            $qb->field($field)->prime(true);
847
        }
848
849 17
        return $qb->getQuery();
850
    }
851
852 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
853
    {
854 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
855 5
        $mapping = $collection->getMapping();
856 5
        $documents = $cursor->toArray();
857 5
        foreach ($documents as $key => $obj) {
858 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
859 1
                $collection->set($key, $obj);
860
            } else {
861 5
                $collection->add($obj);
862
            }
863
        }
864 5
    }
865
866
    /**
867
     *
868
     * @return \Iterator
869
     */
870 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
871
    {
872 5
        $mapping = $collection->getMapping();
873 5
        $repositoryMethod = $mapping['repositoryMethod'];
874 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
875 5
            ->$repositoryMethod($collection->getOwner());
876
877 5
        if (! $cursor instanceof Iterator) {
878
            throw new \BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
879
        }
880
881 5
        if (! empty($mapping['prime'])) {
882 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
883 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
884 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
885
886 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
887
        }
888
889 5
        return $cursor;
890
    }
891
892
    /**
893
     * Prepare a projection array by converting keys, which are PHP property
894
     * names, to MongoDB field names.
895
     *
896
     * @param array $fields
897
     * @return array
898
     */
899 14
    public function prepareProjection(array $fields)
900
    {
901 14
        $preparedFields = [];
902
903 14
        foreach ($fields as $key => $value) {
904 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
905
        }
906
907 14
        return $preparedFields;
908
    }
909
910
    /**
911
     * @param string $sort
912
     * @return int
913
     */
914 25
    private function getSortDirection($sort)
915
    {
916 25
        switch (strtolower((string) $sort)) {
917 25
            case 'desc':
918 15
                return -1;
919
920 22
            case 'asc':
921 13
                return 1;
922
        }
923
924 12
        return $sort;
925
    }
926
927
    /**
928
     * Prepare a sort specification array by converting keys to MongoDB field
929
     * names and changing direction strings to int.
930
     *
931
     * @param array $fields
932
     * @return array
933
     */
934 141
    public function prepareSort(array $fields)
935
    {
936 141
        $sortFields = [];
937
938 141
        foreach ($fields as $key => $value) {
939 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
940
        }
941
942 141
        return $sortFields;
943
    }
944
945
    /**
946
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
947
     *
948
     * @param string $fieldName
949
     * @return string
950
     */
951 433
    public function prepareFieldName($fieldName)
952
    {
953 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
954
955 433
        return $fieldNames[0][0];
956
    }
957
958
    /**
959
     * Adds discriminator criteria to an already-prepared query.
960
     *
961
     * This method should be used once for query criteria and not be used for
962
     * nested expressions. It should be called before
963
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
964
     *
965
     * @param array $preparedQuery
966
     * @return array
967
     */
968 492
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
969
    {
970
        /* If the class has a discriminator field, which is not already in the
971
         * criteria, inject it now. The field/values need no preparation.
972
         */
973 492
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
974 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
975 29
            if (count($discriminatorValues) === 1) {
976 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
977
            } else {
978 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
979
            }
980
        }
981
982 492
        return $preparedQuery;
983
    }
984
985
    /**
986
     * Adds filter criteria to an already-prepared query.
987
     *
988
     * This method should be used once for query criteria and not be used for
989
     * nested expressions. It should be called after
990
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
991
     *
992
     * @param array $preparedQuery
993
     * @return array
994
     */
995 493
    public function addFilterToPreparedQuery(array $preparedQuery)
996
    {
997
        /* If filter criteria exists for this class, prepare it and merge
998
         * over the existing query.
999
         *
1000
         * @todo Consider recursive merging in case the filter criteria and
1001
         * prepared query both contain top-level $and/$or operators.
1002
         */
1003 493
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
1004 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...
1005 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1006
        }
1007
1008 493
        return $preparedQuery;
1009
    }
1010
1011
    /**
1012
     * Prepares the query criteria or new document object.
1013
     *
1014
     * PHP field names and types will be converted to those used by MongoDB.
1015
     *
1016
     * @param array $query
1017
     * @param bool  $isNewObj
1018
     * @return array
1019
     */
1020 525
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1021
    {
1022 525
        $preparedQuery = [];
1023
1024 525
        foreach ($query as $key => $value) {
1025
            // Recursively prepare logical query clauses
1026 483
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
1027 20
                foreach ($value as $k2 => $v2) {
1028 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1029
                }
1030 20
                continue;
1031
            }
1032
1033 483
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1034 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1035 38
                continue;
1036
            }
1037
1038 483
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1039 483
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1040 483
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1041 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1042 483
                    : Type::convertPHPToDatabaseValue($preparedValue);
1043
            }
1044
        }
1045
1046 525
        return $preparedQuery;
1047
    }
1048
1049
    /**
1050
     * Prepares a query value and converts the PHP value to the database value
1051
     * if it is an identifier.
1052
     *
1053
     * It also handles converting $fieldName to the database name if they are different.
1054
     *
1055
     * @param string        $fieldName
1056
     * @param mixed         $value
1057
     * @param ClassMetadata $class        Defaults to $this->class
1058
     * @param bool          $prepareValue Whether or not to prepare the value
1059
     * @param bool          $inNewObj     Whether or not newObj is being prepared
1060
     * @return array An array of tuples containing prepared field names and values
1061
     */
1062 876
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1063
    {
1064 876
        $class = $class ?? $this->class;
1065
1066
        // @todo Consider inlining calls to ClassMetadata methods
1067
1068
        // Process all non-identifier fields by translating field names
1069 876
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1070 247
            $mapping = $class->fieldMappings[$fieldName];
1071 247
            $fieldName = $mapping['name'];
1072
1073 247
            if (! $prepareValue) {
1074 52
                return [[$fieldName, $value]];
1075
            }
1076
1077
            // Prepare mapped, embedded objects
1078 205
            if (! empty($mapping['embedded']) && is_object($value) &&
1079 205
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1080 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1081
            }
1082
1083 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...
1084
                try {
1085 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1086 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...
1087
                    // do nothing in case passed object is not mapped document
1088
                }
1089
            }
1090
1091
            // No further preparation unless we're dealing with a simple reference
1092
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1093 190
            $arrayValue = (array) $value;
1094 190
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1095 126
                return [[$fieldName, $value]];
1096
            }
1097
1098
            // Additional preparation for one or more simple reference values
1099 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1100
1101 91
            if (! is_array($value)) {
1102 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1103
            }
1104
1105
            // Objects without operators or with DBRef fields can be converted immediately
1106 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1107 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1108
            }
1109
1110 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1111
        }
1112
1113
        // Process identifier fields
1114 789
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1115 334
            $fieldName = '_id';
1116
1117 334
            if (! $prepareValue) {
1118 42
                return [[$fieldName, $value]];
1119
            }
1120
1121 295
            if (! is_array($value)) {
1122 272
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1123
            }
1124
1125
            // Objects without operators or with DBRef fields can be converted immediately
1126 61
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1127 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1128
            }
1129
1130 56
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1131
        }
1132
1133
        // No processing for unmapped, non-identifier, non-dotted field names
1134 553
        if (strpos($fieldName, '.') === false) {
1135 414
            return [[$fieldName, $value]];
1136
        }
1137
1138
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1139
         *
1140
         * We can limit parsing here, since at most three segments are
1141
         * significant: "fieldName.objectProperty" with an optional index or key
1142
         * for collections stored as either BSON arrays or objects.
1143
         */
1144 152
        $e = explode('.', $fieldName, 4);
1145
1146
        // No further processing for unmapped fields
1147 152
        if (! isset($class->fieldMappings[$e[0]])) {
1148 6
            return [[$fieldName, $value]];
1149
        }
1150
1151 147
        $mapping = $class->fieldMappings[$e[0]];
1152 147
        $e[0] = $mapping['name'];
1153
1154
        // Hash and raw fields will not be prepared beyond the field name
1155 147
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1156 1
            $fieldName = implode('.', $e);
1157
1158 1
            return [[$fieldName, $value]];
1159
        }
1160
1161 146
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1162 146
                && isset($e[2])) {
1163 1
            $objectProperty = $e[2];
1164 1
            $objectPropertyPrefix = $e[1] . '.';
1165 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1166 145
        } elseif ($e[1] !== '$') {
1167 144
            $fieldName = $e[0] . '.' . $e[1];
1168 144
            $objectProperty = $e[1];
1169 144
            $objectPropertyPrefix = '';
1170 144
            $nextObjectProperty = implode('.', array_slice($e, 2));
1171 1
        } elseif (isset($e[2])) {
1172 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1173 1
            $objectProperty = $e[2];
1174 1
            $objectPropertyPrefix = $e[1] . '.';
1175 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1176
        } else {
1177 1
            $fieldName = $e[0] . '.' . $e[1];
1178
1179 1
            return [[$fieldName, $value]];
1180
        }
1181
1182
        // No further processing for fields without a targetDocument mapping
1183 146
        if (! isset($mapping['targetDocument'])) {
1184 3
            if ($nextObjectProperty) {
1185
                $fieldName .= '.' . $nextObjectProperty;
1186
            }
1187
1188 3
            return [[$fieldName, $value]];
1189
        }
1190
1191 143
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1192
1193
        // No further processing for unmapped targetDocument fields
1194 143
        if (! $targetClass->hasField($objectProperty)) {
1195 25
            if ($nextObjectProperty) {
1196
                $fieldName .= '.' . $nextObjectProperty;
1197
            }
1198
1199 25
            return [[$fieldName, $value]];
1200
        }
1201
1202 123
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1203 123
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1204
1205
        // Prepare DBRef identifiers or the mapped field's property path
1206 123
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID)
1207 105
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1208 123
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1209
1210
        // Process targetDocument identifier fields
1211 123
        if ($objectPropertyIsId) {
1212 106
            if (! $prepareValue) {
1213 7
                return [[$fieldName, $value]];
1214
            }
1215
1216 99
            if (! is_array($value)) {
1217 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1218
            }
1219
1220
            // Objects without operators or with DBRef fields can be converted immediately
1221 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1222 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1223
            }
1224
1225 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1226
        }
1227
1228
        /* The property path may include a third field segment, excluding the
1229
         * collection item pointer. If present, this next object property must
1230
         * be processed recursively.
1231
         */
1232 17
        if ($nextObjectProperty) {
1233
            // Respect the targetDocument's class metadata when recursing
1234 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1235 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1236 14
                : null;
1237
1238 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1239
1240 14
            return array_map(function ($preparedTuple) use ($fieldName) {
1241 14
                list($key, $value) = $preparedTuple;
1242
1243 14
                return [$fieldName . '.' . $key, $value];
1244 14
            }, $fieldNames);
1245
        }
1246
1247 5
        return [[$fieldName, $value]];
1248
    }
1249
1250
    /**
1251
     * Prepares a query expression.
1252
     *
1253
     * @param array|object  $expression
1254
     * @param ClassMetadata $class
1255
     * @return array
1256
     */
1257 78
    private function prepareQueryExpression($expression, $class)
1258
    {
1259 78
        foreach ($expression as $k => $v) {
1260
            // Ignore query operators whose arguments need no type conversion
1261 78
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1262 16
                continue;
1263
            }
1264
1265
            // Process query operators whose argument arrays need type conversion
1266 78
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1267 76
                foreach ($v as $k2 => $v2) {
1268 76
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1269
                }
1270 76
                continue;
1271
            }
1272
1273
            // Recursively process expressions within a $not operator
1274 18
            if ($k === '$not' && is_array($v)) {
1275 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1276 15
                continue;
1277
            }
1278
1279 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1280
        }
1281
1282 78
        return $expression;
1283
    }
1284
1285
    /**
1286
     * Checks whether the value has DBRef fields.
1287
     *
1288
     * This method doesn't check if the the value is a complete DBRef object,
1289
     * although it should return true for a DBRef. Rather, we're checking that
1290
     * the value has one or more fields for a DBref. In practice, this could be
1291
     * $elemMatch criteria for matching a DBRef.
1292
     *
1293
     * @param mixed $value
1294
     * @return bool
1295
     */
1296 79
    private function hasDBRefFields($value)
1297
    {
1298 79
        if (! is_array($value) && ! is_object($value)) {
1299
            return false;
1300
        }
1301
1302 79
        if (is_object($value)) {
1303
            $value = get_object_vars($value);
1304
        }
1305
1306 79
        foreach ($value as $key => $_) {
1307 79
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1308 79
                return true;
1309
            }
1310
        }
1311
1312 78
        return false;
1313
    }
1314
1315
    /**
1316
     * Checks whether the value has query operators.
1317
     *
1318
     * @param mixed $value
1319
     * @return bool
1320
     */
1321 83
    private function hasQueryOperators($value)
1322
    {
1323 83
        if (! is_array($value) && ! is_object($value)) {
1324
            return false;
1325
        }
1326
1327 83
        if (is_object($value)) {
1328
            $value = get_object_vars($value);
1329
        }
1330
1331 83
        foreach ($value as $key => $_) {
1332 83
            if (isset($key[0]) && $key[0] === '$') {
1333 83
                return true;
1334
            }
1335
        }
1336
1337 11
        return false;
1338
    }
1339
1340
    /**
1341
     * Gets the array of discriminator values for the given ClassMetadata
1342
     *
1343
     * @return array
1344
     */
1345 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1346
    {
1347 29
        $discriminatorValues = [$metadata->discriminatorValue];
1348 29
        foreach ($metadata->subClasses as $className) {
1349 8
            $key = array_search($className, $metadata->discriminatorMap);
1350 8
            if (! $key) {
1351
                continue;
1352
            }
1353
1354 8
            $discriminatorValues[] = $key;
1355
        }
1356
1357
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1358 29
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
1359 2
            $discriminatorValues[] = null;
1360
        }
1361
1362 29
        return $discriminatorValues;
1363
    }
1364
1365 546
    private function handleCollections($document, $options)
1366
    {
1367
        // Collection deletions (deletions of complete collections)
1368 546
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1369 103
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1370 93
                continue;
1371
            }
1372
1373 30
            $this->cp->delete($coll, $options);
1374
        }
1375
        // Collection updates (deleteRows, updateRows, insertRows)
1376 546
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1377 103
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1378 28
                continue;
1379
            }
1380
1381 96
            $this->cp->update($coll, $options);
1382
        }
1383
        // Take new snapshots from visited collections
1384 546
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1385 226
            $coll->takeSnapshot();
1386
        }
1387 546
    }
1388
1389
    /**
1390
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1391
     * Also, shard key field should be present in actual document data.
1392
     *
1393
     * @param object $document
1394
     * @param string $shardKeyField
1395
     * @param array  $actualDocumentData
1396
     *
1397
     * @throws MongoDBException
1398
     */
1399 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1400
    {
1401 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1402 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1403
1404 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1405 4
        $fieldName = $fieldMapping['fieldName'];
1406
1407 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1408
            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...
1409
        }
1410
1411 4
        if (! isset($actualDocumentData[$fieldName])) {
1412
            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...
1413
        }
1414 4
    }
1415
1416
    /**
1417
     * Get shard key aware query for single document.
1418
     *
1419
     * @param object $document
1420
     *
1421
     * @return array
1422
     */
1423 263
    private function getQueryForDocument($document)
1424
    {
1425 263
        $id = $this->uow->getDocumentIdentifier($document);
1426 263
        $id = $this->class->getDatabaseIdentifierValue($id);
1427
1428 263
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1429 263
        $query = array_merge(['_id' => $id], $shardKeyQueryPart);
1430
1431 263
        return $query;
1432
    }
1433
1434
    /**
1435
     * @param array $options
1436
     *
1437
     * @return array
1438
     */
1439 547
    private function getWriteOptions(array $options = [])
1440
    {
1441 547
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1442 547
        $documentOptions = [];
1443 547
        if ($this->class->hasWriteConcern()) {
1444 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1445
        }
1446
1447 547
        return array_merge($defaultOptions, $documentOptions, $options);
1448
    }
1449
1450
    /**
1451
     * @param string $fieldName
1452
     * @param mixed  $value
1453
     * @param array  $mapping
1454
     * @param bool   $inNewObj
1455
     * @return array
1456
     */
1457 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1458
    {
1459 15
        $reference = $this->dm->createReference($value, $mapping);
1460 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1461 8
            return [[$fieldName, $reference]];
1462
        }
1463
1464 6
        switch ($mapping['storeAs']) {
1465
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1466
                $keys = ['id' => true];
1467
                break;
1468
1469
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1470
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1471 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1472
1473 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1474 5
                    unset($keys['$db']);
1475
                }
1476
1477 6
                if (isset($mapping['targetDocument'])) {
1478 4
                    unset($keys['$ref'], $keys['$db']);
1479
                }
1480 6
                break;
1481
1482
            default:
1483
                throw new \InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1484
        }
1485
1486 6
        if ($mapping['type'] === 'many') {
1487 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1488
        }
1489
1490 4
        return array_map(
1491 4
            function ($key) use ($reference, $fieldName) {
1492 4
                return [$fieldName . '.' . $key, $reference[$key]];
1493 4
            },
1494 4
            array_keys($keys)
1495
        );
1496
    }
1497
}
1498