Completed
Pull Request — master (#1787)
by Stefano
15:28
created

DocumentPersister::createDocument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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