Completed
Pull Request — master (#1757)
by Maciej
10:01
created

loadReferenceManyWithRepositoryMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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