Completed
Pull Request — master (#1803)
by Maciej
21:08
created

DocumentPersister::load()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 9.0294

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 13
cts 14
cp 0.9286
rs 4.909
c 0
b 0
f 0
cc 9
eloc 16
nc 24
nop 5
crap 9.0294
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 1078
    public function __construct(
136
        PersistenceBuilder $pb,
137
        DocumentManager $dm,
138
        UnitOfWork $uow,
139
        HydratorFactory $hydratorFactory,
140
        ClassMetadata $class,
141
        ?CriteriaMerger $cm = null
142
    ) {
143 1078
        $this->pb = $pb;
144 1078
        $this->dm = $dm;
145 1078
        $this->cm = $cm ?: new CriteriaMerger();
146 1078
        $this->uow = $uow;
147 1078
        $this->hydratorFactory = $hydratorFactory;
148 1078
        $this->class = $class;
149 1078
        $this->collection = $dm->getDocumentCollection($class->name);
150 1078
        $this->cp = $this->uow->getCollectionPersister();
151 1078
    }
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 480
    public function addInsert($document)
177
    {
178 480
        $this->queuedInserts[spl_object_hash($document)] = $document;
179 480
    }
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 480
    public function executeInserts(array $options = [])
230
    {
231 480
        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 480
        $inserts = [];
236 480
        $options = $this->getWriteOptions($options);
237 480
        foreach ($this->queuedInserts as $oid => $document) {
238 480
            $data = $this->pb->prepareInsertData($document);
239
240
            // Set the initial version for each insert
241 479
            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 479
            $inserts[] = $data;
256
        }
257
258 479
        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 479
                $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 479
        foreach ($this->queuedInserts as $document) {
272 479
            $this->handleCollections($document, $options);
273
        }
274
275 479
        $this->queuedInserts = [];
276 479
    }
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 34
    public function delete($document, array $options = [])
453
    {
454 34
        $query = $this->getQueryForDocument($document);
455
456 34
        if ($this->class->isLockable) {
457 2
            $query[$this->class->lockField] = ['$exists' => false];
458
        }
459
460 34
        $options = $this->getWriteOptions($options);
461
462 34
        $result = $this->collection->deleteOne($query, $options);
463
464 34
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
465 2
            throw LockException::lockFailed($document);
466
        }
467 32
    }
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
        if ($data === null) {
479 20
            throw MongoDBException::cannotRefreshDocument();
480 20
        }
481
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
482
        $this->uow->setOriginalDocumentData($document, $data);
483
    }
484
485
    /**
486
     * Finds a document by a set of criteria.
487
     *
488
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
489
     * be used to match an _id value.
490
     *
491
     * @param mixed  $criteria Query criteria
492
     * @param object $document Document to load the data into. If not specified, a new document is created.
493
     * @param array  $hints    Hints for document creation
494
     * @param int    $lockMode
495
     * @param array  $sort     Sort array for Cursor::sort()
496
     * @throws LockException
497 344
     * @return object|null The loaded and managed document instance or null if no document was found
498
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
499
     */
500 344
    public function load($criteria, $document = null, array $hints = [], $lockMode = 0, ?array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
501
    {
502
        // TODO: remove this
503
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof ObjectId) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
504 344
            $criteria = ['_id' => $criteria];
505 344
        }
506 344
507
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
508 344
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
509 344
        $criteria = $this->addFilterToPreparedQuery($criteria);
510 92
511
        $options = [];
512 344
        if ($sort !== null) {
513
            $options['sort'] = $this->prepareSort($sort);
514 344
        }
515 1
        $result = $this->collection->findOne($criteria, $options);
516 1
        $result = $result !== null ? (array) $result : null;
517 1
518
        if ($this->class->isLockable) {
519
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
520
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
521 343
                throw LockException::lockFailed($result);
522
            }
523
        }
524
525
        return $this->createDocument($result, $document, $hints);
526
    }
527
528
    /**
529
     * Finds documents by a set of criteria.
530
     *
531
     * @param array    $criteria Query criteria
532
     * @param array    $sort     Sort array for Cursor::sort()
533 22
     * @param int|null $limit    Limit for Cursor::limit()
534
     * @param int|null $skip     Skip for Cursor::skip()
535 22
     * @return Iterator
536 22
     */
537 22
    public function loadAll(array $criteria = [], ?array $sort = null, $limit = null, $skip = null)
538
    {
539 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
540 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
541 11
        $criteria = $this->addFilterToPreparedQuery($criteria);
542
543
        $options = [];
544 22
        if ($sort !== null) {
545 10
            $options['sort'] = $this->prepareSort($sort);
546
        }
547
548 22
        if ($limit !== null) {
549 1
            $options['limit'] = $limit;
550
        }
551
552 22
        if ($skip !== null) {
553 22
            $options['skip'] = $skip;
554
        }
555 22
556
        $baseCursor = $this->collection->find($criteria, $options);
557
        $cursor = $this->wrapCursor($baseCursor);
558
559
        return $cursor;
560
    }
561
562
    /**
563
     * @param object $document
564 270
     *
565
     * @return array
566 270
     * @throws MongoDBException
567 266
     */
568
    private function getShardKeyQuery($document)
569
    {
570 4
        if (! $this->class->isSharded()) {
571 4
            return [];
572 4
        }
573
574 4
        $shardKey = $this->class->getShardKey();
575 4
        $keys = array_keys($shardKey['keys']);
576 4
        $data = $this->uow->getDocumentActualData($document);
577 4
578
        $shardKeyQueryPart = [];
579 4
        foreach ($keys as $key) {
580 1
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
581 1
            $this->guardMissingShardKey($document, $key, $data);
582 1
583 1
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
584 1
                $reference = $this->prepareReference(
585
                    $key,
586 1
                    $data[$mapping['fieldName']],
587 1
                    $mapping,
588
                    false
589
                );
590 3
                foreach ($reference as $keyValue) {
591 4
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
592
                }
593
            } else {
594
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
595 4
                $shardKeyQueryPart[$key] = $value;
596
            }
597
        }
598
599
        return $shardKeyQueryPart;
600
    }
601
602 22
    /**
603
     * Wraps the supplied base cursor in the corresponding ODM class.
604 22
     *
605
     */
606
    private function wrapCursor(Cursor $baseCursor): Iterator
607
    {
608
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
609
    }
610
611
    /**
612
     * Checks whether the given managed document exists in the database.
613 3
     *
614
     * @param object $document
615 3
     * @return bool TRUE if the document exists in the database, FALSE otherwise.
616 3
     */
617
    public function exists($document)
618
    {
619
        $id = $this->class->getIdentifierObject($document);
620
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
621
    }
622
623
    /**
624
     * Locks document by storing the lock mode on the mapped lock field.
625 5
     *
626
     * @param object $document
627 5
     * @param int    $lockMode
628 5
     */
629 5
    public function lock($document, $lockMode)
630 5
    {
631 5
        $id = $this->uow->getDocumentIdentifier($document);
632 5
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
633
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
634
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
635
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
636
    }
637
638
    /**
639 1
     * Releases any lock that exists on this document.
640
     *
641 1
     * @param object $document
642 1
     */
643 1
    public function unlock($document)
644 1
    {
645 1
        $id = $this->uow->getDocumentIdentifier($document);
646 1
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
647
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
648
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
649
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
650
    }
651
652
    /**
653
     * Creates or fills a single document object from an query result.
654
     *
655
     * @param array|null $result   The query result.
656 343
     * @param object     $document The document object to fill, if any.
657
     * @param array      $hints    Hints for document creation.
658 343
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
659 113
     */
660
    private function createDocument($result, $document = null, array $hints = [])
661
    {
662 299
        if ($result === null) {
663 24
            return null;
664 24
        }
665 24
666
        if ($document !== null) {
667
            $hints[Query::HINT_REFRESH] = true;
668 299
            $id = $this->class->getPHPIdentifierValue($result['_id']);
669
            $this->uow->registerManaged($document, $id, $result);
670
        }
671
672
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
673
    }
674
675 164
    /**
676
     * Loads a PersistentCollection data. Used in the initialize() method.
677 164
     *
678 164
     */
679
    public function loadCollection(PersistentCollectionInterface $collection)
680 109
    {
681 109
        $mapping = $collection->getMapping();
682
        switch ($mapping['association']) {
683
            case ClassMetadata::EMBED_MANY:
684 77
                $this->loadEmbedManyCollection($collection);
685 5
                break;
686
687 73
            case ClassMetadata::REFERENCE_MANY:
688 61
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
689
                    $this->loadReferenceManyWithRepositoryMethod($collection);
690 17
                } else {
691
                    if ($mapping['isOwningSide']) {
692
                        $this->loadReferenceManyCollectionOwningSide($collection);
693 77
                    } else {
694
                        $this->loadReferenceManyCollectionInverseSide($collection);
695 164
                    }
696
                }
697 109
                break;
698
        }
699 109
    }
700 109
701 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
702 109
    {
703 58
        $embeddedDocuments = $collection->getMongoData();
704
        $mapping = $collection->getMapping();
705
        $owner = $collection->getOwner();
706 82
        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...
707 82
            return;
708 82
        }
709 82
710
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
711 82
            $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
712
            $embeddedMetadata = $this->dm->getClassMetadata($className);
713 82
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
714 82
715
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
716 82
717 81
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
718
            $id = $data[$embeddedMetadata->identifier] ?? null;
719 82
720 10
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
721
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
722 82
            }
723
            if (CollectionHelper::isHash($mapping['strategy'])) {
724
                $collection->set($key, $embeddedDocumentObject);
725 82
            } else {
726
                $collection->add($embeddedDocumentObject);
727 61
            }
728
        }
729 61
    }
730 61
731 61
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
732
    {
733 61
        $hints = $collection->getHints();
734
        $mapping = $collection->getMapping();
735 61
        $groupedIds = [];
736 55
737 55
        $sorted = isset($mapping['sort']) && $mapping['sort'];
738 55
739
        foreach ($collection->getMongoData() as $key => $reference) {
740
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
741 55
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
742
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
743
744 55
            // create a reference to the class and id
745 54
            $reference = $this->dm->getReference($className, $id);
746 2
747
            // no custom sort so add the references right now in the order they are embedded
748 52
            if (! $sorted) {
749
                if (CollectionHelper::isHash($mapping['strategy'])) {
750
                    $collection->set($key, $reference);
751
                } else {
752
                    $collection->add($reference);
753 55
                }
754 22
            }
755
756
            // only query for the referenced object if it is not already initialized or the collection is sorted
757 40
            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...
758
                continue;
759 61
            }
760 40
761 40
            $groupedIds[$className][] = $identifier;
762 40
        }
763 40
        foreach ($groupedIds as $className => $ids) {
764 40
            $class = $this->dm->getClassMetadata($className);
765 40
            $mongoCollection = $this->dm->getDocumentCollection($className);
766
            $criteria = $this->cm->merge(
767 40
                ['_id' => ['$in' => array_values($ids)]],
768
                $this->dm->getFilterCollection()->getFilterCriteria($class),
769 40
                $mapping['criteria'] ?? []
770 40
            );
771 40
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
772
773 40
            $options = [];
774
            if (isset($mapping['sort'])) {
775
                $options['sort'] = $this->prepareSort($mapping['sort']);
776 40
            }
777
            if (isset($mapping['limit'])) {
778
                $options['limit'] = $mapping['limit'];
779 40
            }
780
            if (isset($mapping['skip'])) {
781
                $options['skip'] = $mapping['skip'];
782
            }
783 40
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
784 40
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
785 40
            }
786 39
787 39
            $cursor = $mongoCollection->find($criteria, $options);
788 39
            $documents = $cursor->toArray();
789 39
            foreach ($documents as $documentData) {
790 39
                $document = $this->uow->getById($documentData['_id'], $class);
791
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
792 39
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
793 38
                    $this->uow->setOriginalDocumentData($document, $data);
794
                    $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...
795
                }
796 40
                if (! $sorted) {
797
                    continue;
798
                }
799 61
800
                $collection->add($document);
801 17
            }
802
        }
803 17
    }
804 17
805 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
806 16
    {
807
        $query = $this->createReferenceManyInverseSideQuery($collection);
808 17
        $documents = $query->execute()->toArray();
809
        foreach ($documents as $key => $document) {
810
            $collection->add($document);
811
        }
812
    }
813
814 17
    /**
815
     *
816 17
     * @return Query
817 17
     */
818 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
819 17
    {
820 17
        $hints = $collection->getHints();
821 17
        $mapping = $collection->getMapping();
822 17
        $owner = $collection->getOwner();
823
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
824 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
825 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
826 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
827 17
828
        $criteria = $this->cm->merge(
829 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
830 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
831 17
            $mapping['criteria'] ?? []
832
        );
833 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
834 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
835
            ->setQueryArray($criteria);
836 17
837 2
        if (isset($mapping['sort'])) {
838
            $qb->sort($mapping['sort']);
839 17
        }
840
        if (isset($mapping['limit'])) {
841
            $qb->limit($mapping['limit']);
842
        }
843 17
        if (isset($mapping['skip'])) {
844
            $qb->skip($mapping['skip']);
845
        }
846
847 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
848 4
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
849
        }
850
851 17
        foreach ($mapping['prime'] as $field) {
852
            $qb->field($field)->prime(true);
853
        }
854 5
855
        return $qb->getQuery();
856 5
    }
857 5
858 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
859 5
    {
860 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
861 1
        $mapping = $collection->getMapping();
862
        $documents = $cursor->toArray();
863 5
        foreach ($documents as $key => $obj) {
864
            if (CollectionHelper::isHash($mapping['strategy'])) {
865
                $collection->set($key, $obj);
866 5
            } else {
867
                $collection->add($obj);
868
            }
869
        }
870
    }
871
872 5
    /**
873
     * @return Iterator
874 5
     */
875 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
876 5
    {
877 5
        $mapping = $collection->getMapping();
878
        $repositoryMethod = $mapping['repositoryMethod'];
879 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
880
            ->$repositoryMethod($collection->getOwner());
881
882
        if (! $cursor instanceof Iterator) {
883 5
            throw new \BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
884 1
        }
885 1
886 1
        if (! empty($mapping['prime'])) {
887
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
888 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
889
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
890
891 5
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
892
        }
893
894
        return $cursor;
895
    }
896
897
    /**
898
     * Prepare a projection array by converting keys, which are PHP property
899
     * names, to MongoDB field names.
900
     *
901 14
     * @param array $fields
902
     * @return array
903 14
     */
904
    public function prepareProjection(array $fields)
905 14
    {
906 14
        $preparedFields = [];
907
908
        foreach ($fields as $key => $value) {
909 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
910
        }
911
912
        return $preparedFields;
913
    }
914
915
    /**
916 25
     * @param string|int $sort
917
     * @return string|int
918 25
     */
919 25
    private function getSortDirection($sort)
920 15
    {
921
        switch (strtolower((string) $sort)) {
922 22
            case 'desc':
923 13
                return -1;
924
925
            case 'asc':
926 12
                return 1;
927
        }
928
929
        return $sort;
930
    }
931
932
    /**
933
     * Prepare a sort specification array by converting keys to MongoDB field
934
     * names and changing direction strings to int.
935
     *
936 142
     * @param array $fields
937
     * @return array
938 142
     */
939
    public function prepareSort(array $fields)
940 142
    {
941 25
        $sortFields = [];
942
943
        foreach ($fields as $key => $value) {
944 142
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
945
        }
946
947
        return $sortFields;
948
    }
949
950
    /**
951
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
952
     *
953 433
     * @param string $fieldName
954
     * @return string
955 433
     */
956
    public function prepareFieldName($fieldName)
957 433
    {
958
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
959
960
        return $fieldNames[0][0];
961
    }
962
963
    /**
964
     * Adds discriminator criteria to an already-prepared query.
965
     *
966
     * This method should be used once for query criteria and not be used for
967
     * nested expressions. It should be called before
968
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
969
     *
970 497
     * @param array $preparedQuery
971
     * @return array
972
     */
973
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
974
    {
975 497
        /* If the class has a discriminator field, which is not already in the
976 27
         * criteria, inject it now. The field/values need no preparation.
977 27
         */
978 19
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
979
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
980 10
            if (count($discriminatorValues) === 1) {
981
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
982
            } else {
983
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
984 497
            }
985
        }
986
987
        return $preparedQuery;
988
    }
989
990
    /**
991
     * Adds filter criteria to an already-prepared query.
992
     *
993
     * This method should be used once for query criteria and not be used for
994
     * nested expressions. It should be called after
995
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
996
     *
997 498
     * @param array $preparedQuery
998
     * @return array
999
     */
1000
    public function addFilterToPreparedQuery(array $preparedQuery)
1001
    {
1002
        /* If filter criteria exists for this class, prepare it and merge
1003
         * over the existing query.
1004
         *
1005 498
         * @todo Consider recursive merging in case the filter criteria and
1006 498
         * prepared query both contain top-level $and/$or operators.
1007 18
         */
1008
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
1009
        if ($filterCriteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filterCriteria of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1010 498
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1011
        }
1012
1013
        return $preparedQuery;
1014
    }
1015
1016
    /**
1017
     * Prepares the query criteria or new document object.
1018
     *
1019
     * PHP field names and types will be converted to those used by MongoDB.
1020
     *
1021
     * @param array $query
1022 530
     * @param bool  $isNewObj
1023
     * @return array
1024 530
     */
1025
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1026 530
    {
1027
        $preparedQuery = [];
1028 488
1029 20
        foreach ($query as $key => $value) {
1030 20
            // Recursively prepare logical query clauses
1031
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
1032 20
                foreach ($value as $k2 => $v2) {
1033
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1034
                }
1035 488
                continue;
1036 38
            }
1037 38
1038
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1039
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1040 488
                continue;
1041 488
            }
1042 488
1043 134
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1044 488
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1045
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1046
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1047
                    : Type::convertPHPToDatabaseValue($preparedValue);
1048 530
            }
1049
        }
1050
1051
        return $preparedQuery;
1052
    }
1053
1054
    /**
1055
     * Prepares a query value and converts the PHP value to the database value
1056
     * if it is an identifier.
1057
     *
1058
     * It also handles converting $fieldName to the database name if they are different.
1059
     *
1060
     * @param string        $fieldName
1061
     * @param mixed         $value
1062
     * @param ClassMetadata $class        Defaults to $this->class
1063
     * @param bool          $prepareValue Whether or not to prepare the value
1064 881
     * @param bool          $inNewObj     Whether or not newObj is being prepared
1065
     * @return array An array of tuples containing prepared field names and values
1066 881
     */
1067
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1068
    {
1069
        $class = $class ?? $this->class;
1070
1071 881
        // @todo Consider inlining calls to ClassMetadata methods
1072 247
1073 247
        // Process all non-identifier fields by translating field names
1074
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1075 247
            $mapping = $class->fieldMappings[$fieldName];
1076 52
            $fieldName = $mapping['name'];
1077
1078
            if (! $prepareValue) {
1079
                return [[$fieldName, $value]];
1080 205
            }
1081 205
1082 3
            // Prepare mapped, embedded objects
1083
            if (! empty($mapping['embedded']) && is_object($value) &&
1084
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1085 203
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1086
            }
1087 14
1088 1
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1089
                try {
1090
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1091
                } catch (MappingException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Persiste...apping\MappingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1092
                    // do nothing in case passed object is not mapped document
1093
                }
1094
            }
1095 190
1096 190
            // No further preparation unless we're dealing with a simple reference
1097 126
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1098
            $arrayValue = (array) $value;
1099
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1100
                return [[$fieldName, $value]];
1101 91
            }
1102
1103 91
            // Additional preparation for one or more simple reference values
1104 87
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1105
1106
            if (! is_array($value)) {
1107
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1108 6
            }
1109 3
1110
            // Objects without operators or with DBRef fields can be converted immediately
1111
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1112 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1113
            }
1114
1115
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1116 794
        }
1117 336
1118
        // Process identifier fields
1119 336
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1120 42
            $fieldName = '_id';
1121
1122
            if (! $prepareValue) {
1123 297
                return [[$fieldName, $value]];
1124 271
            }
1125
1126
            if (! is_array($value)) {
1127
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1128 62
            }
1129 6
1130
            // Objects without operators or with DBRef fields can be converted immediately
1131
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1132 57
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1133
            }
1134
1135
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1136 555
        }
1137 413
1138
        // No processing for unmapped, non-identifier, non-dotted field names
1139
        if (strpos($fieldName, '.') === false) {
1140
            return [[$fieldName, $value]];
1141
        }
1142
1143
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1144
         *
1145
         * We can limit parsing here, since at most three segments are
1146 154
         * significant: "fieldName.objectProperty" with an optional index or key
1147
         * for collections stored as either BSON arrays or objects.
1148
         */
1149 154
        $e = explode('.', $fieldName, 4);
1150 6
1151
        // No further processing for unmapped fields
1152
        if (! isset($class->fieldMappings[$e[0]])) {
1153 149
            return [[$fieldName, $value]];
1154 149
        }
1155
1156
        $mapping = $class->fieldMappings[$e[0]];
1157 149
        $e[0] = $mapping['name'];
1158 1
1159
        // Hash and raw fields will not be prepared beyond the field name
1160 1
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1161
            $fieldName = implode('.', $e);
1162
1163 148
            return [[$fieldName, $value]];
1164 148
        }
1165 1
1166 1
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1167 1
                && isset($e[2])) {
1168 147
            $objectProperty = $e[2];
1169 146
            $objectPropertyPrefix = $e[1] . '.';
1170 146
            $nextObjectProperty = implode('.', array_slice($e, 3));
1171 146
        } elseif ($e[1] !== '$') {
1172 146
            $fieldName = $e[0] . '.' . $e[1];
1173 1
            $objectProperty = $e[1];
1174 1
            $objectPropertyPrefix = '';
1175 1
            $nextObjectProperty = implode('.', array_slice($e, 2));
1176 1
        } elseif (isset($e[2])) {
1177 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1178
            $objectProperty = $e[2];
1179 1
            $objectPropertyPrefix = $e[1] . '.';
1180
            $nextObjectProperty = implode('.', array_slice($e, 3));
1181 1
        } else {
1182
            $fieldName = $e[0] . '.' . $e[1];
1183
1184
            return [[$fieldName, $value]];
1185 148
        }
1186 3
1187
        // No further processing for fields without a targetDocument mapping
1188
        if (! isset($mapping['targetDocument'])) {
1189
            if ($nextObjectProperty) {
1190 3
                $fieldName .= '.' . $nextObjectProperty;
1191
            }
1192
1193 145
            return [[$fieldName, $value]];
1194
        }
1195
1196 145
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1197 25
1198
        // No further processing for unmapped targetDocument fields
1199
        if (! $targetClass->hasField($objectProperty)) {
1200
            if ($nextObjectProperty) {
1201 25
                $fieldName .= '.' . $nextObjectProperty;
1202
            }
1203
1204 125
            return [[$fieldName, $value]];
1205 125
        }
1206
1207
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1208 125
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1209 105
1210 125
        // Prepare DBRef identifiers or the mapped field's property path
1211
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID)
1212
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1213 125
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1214 106
1215 7
        // Process targetDocument identifier fields
1216
        if ($objectPropertyIsId) {
1217
            if (! $prepareValue) {
1218 99
                return [[$fieldName, $value]];
1219 85
            }
1220
1221
            if (! is_array($value)) {
1222
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1223 16
            }
1224 6
1225
            // Objects without operators or with DBRef fields can be converted immediately
1226
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1227 16
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1228
            }
1229
1230
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1231
        }
1232
1233
        /* The property path may include a third field segment, excluding the
1234 19
         * collection item pointer. If present, this next object property must
1235
         * be processed recursively.
1236 16
         */
1237 10
        if ($nextObjectProperty) {
1238 16
            // Respect the targetDocument's class metadata when recursing
1239
            $nextTargetClass = isset($targetMapping['targetDocument'])
1240 16
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1241 14
                : null;
1242
1243
            if (empty($targetMapping['reference'])) {
1244 4
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1245 1
            } else {
1246
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1247 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1248
                    $nextObjectProperty = '$' . $nextObjectProperty;
1249
                }
1250
                $fieldNames = [[$nextObjectProperty, $value]];
1251 16
            }
1252
1253 16
            return array_map(function ($preparedTuple) use ($fieldName) {
1254 16
                list($key, $value) = $preparedTuple;
1255
1256
                return [$fieldName . '.' . $key, $value];
1257 5
            }, $fieldNames);
1258
        }
1259
1260
        return [[$fieldName, $value]];
1261
    }
1262
1263
    /**
1264
     * Prepares a query expression.
1265
     *
1266
     * @param array|object  $expression
1267 79
     * @param ClassMetadata $class
1268
     * @return array|object
1269 79
     */
1270
    private function prepareQueryExpression($expression, $class)
1271 79
    {
1272 16
        foreach ($expression as $k => $v) {
1273
            // Ignore query operators whose arguments need no type conversion
1274
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1275
                continue;
1276 79
            }
1277 77
1278 77
            // Process query operators whose argument arrays need type conversion
1279
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1280 77
                foreach ($v as $k2 => $v2) {
1281
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1282
                }
1283
                continue;
1284 18
            }
1285 15
1286 15
            // Recursively process expressions within a $not operator
1287
            if ($k === '$not' && is_array($v)) {
1288
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1289 18
                continue;
1290
            }
1291
1292 79
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1293
        }
1294
1295
        return $expression;
1296
    }
1297
1298
    /**
1299
     * Checks whether the value has DBRef fields.
1300
     *
1301
     * This method doesn't check if the the value is a complete DBRef object,
1302
     * although it should return true for a DBRef. Rather, we're checking that
1303
     * the value has one or more fields for a DBref. In practice, this could be
1304
     * $elemMatch criteria for matching a DBRef.
1305
     *
1306 80
     * @param mixed $value
1307
     * @return bool
1308 80
     */
1309
    private function hasDBRefFields($value)
1310
    {
1311
        if (! is_array($value) && ! is_object($value)) {
1312 80
            return false;
1313
        }
1314
1315
        if (is_object($value)) {
1316 80
            $value = get_object_vars($value);
1317 80
        }
1318 80
1319
        foreach ($value as $key => $_) {
1320
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1321
                return true;
1322 79
            }
1323
        }
1324
1325
        return false;
1326
    }
1327
1328
    /**
1329
     * Checks whether the value has query operators.
1330
     *
1331 84
     * @param mixed $value
1332
     * @return bool
1333 84
     */
1334
    private function hasQueryOperators($value)
1335
    {
1336
        if (! is_array($value) && ! is_object($value)) {
1337 84
            return false;
1338
        }
1339
1340
        if (is_object($value)) {
1341 84
            $value = get_object_vars($value);
1342 84
        }
1343 84
1344
        foreach ($value as $key => $_) {
1345
            if (isset($key[0]) && $key[0] === '$') {
1346
                return true;
1347 11
            }
1348
        }
1349
1350
        return false;
1351
    }
1352
1353
    /**
1354
     * Gets the array of discriminator values for the given ClassMetadata
1355 27
     *
1356
     * @return array
1357 27
     */
1358 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1359 8
    {
1360 8
        $discriminatorValues = [$metadata->discriminatorValue];
1361
        foreach ($metadata->subClasses as $className) {
1362
            $key = array_search($className, $metadata->discriminatorMap);
1363
            if (! $key) {
1364 8
                continue;
1365
            }
1366
1367
            $discriminatorValues[] = $key;
1368 27
        }
1369 2
1370
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1371
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
1372 27
            $discriminatorValues[] = null;
1373
        }
1374
1375 550
        return $discriminatorValues;
1376
    }
1377
1378 550
    private function handleCollections($document, $options)
1379 104
    {
1380 94
        // Collection deletions (deletions of complete collections)
1381
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1382
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1383 30
                continue;
1384
            }
1385
1386 550
            $this->cp->delete($coll, $options);
1387 104
        }
1388 28
        // Collection updates (deleteRows, updateRows, insertRows)
1389
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1390
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1391 97
                continue;
1392
            }
1393
1394 550
            $this->cp->update($coll, $options);
1395 229
        }
1396
        // Take new snapshots from visited collections
1397 550
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1398
            $coll->takeSnapshot();
1399
        }
1400
    }
1401
1402
    /**
1403
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1404
     * Also, shard key field should be present in actual document data.
1405
     *
1406
     * @param object $document
1407
     * @param string $shardKeyField
1408
     * @param array  $actualDocumentData
1409 4
     *
1410
     * @throws MongoDBException
1411 4
     */
1412 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1413
    {
1414 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1415 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1416
1417 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1418
        $fieldName = $fieldMapping['fieldName'];
1419
1420
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1421 4
            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...
1422
        }
1423
1424 4
        if (! isset($actualDocumentData[$fieldName])) {
1425
            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...
1426
        }
1427
    }
1428
1429
    /**
1430
     * Get shard key aware query for single document.
1431
     *
1432
     * @param object $document
1433 266
     *
1434
     * @return array
1435 266
     */
1436 266
    private function getQueryForDocument($document)
1437
    {
1438 266
        $id = $this->uow->getDocumentIdentifier($document);
1439 266
        $id = $this->class->getDatabaseIdentifierValue($id);
1440
1441 266
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1442
        $query = array_merge(['_id' => $id], $shardKeyQueryPart);
1443
1444
        return $query;
1445
    }
1446
1447
    /**
1448
     * @param array $options
1449 551
     *
1450
     * @return array
1451 551
     */
1452 551
    private function getWriteOptions(array $options = [])
1453 551
    {
1454 9
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1455
        $documentOptions = [];
1456
        if ($this->class->hasWriteConcern()) {
1457 551
            $documentOptions['w'] = $this->class->getWriteConcern();
1458
        }
1459
1460
        return array_merge($defaultOptions, $documentOptions, $options);
1461
    }
1462
1463
    /**
1464
     * @param string $fieldName
1465
     * @param mixed  $value
1466
     * @param array  $mapping
1467 15
     * @param bool   $inNewObj
1468
     * @return array
1469 15
     */
1470 14
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1471 8
    {
1472
        $reference = $this->dm->createReference($value, $mapping);
1473
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1474 6
            return [[$fieldName, $reference]];
1475
        }
1476
1477
        switch ($mapping['storeAs']) {
1478
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1479
                $keys = ['id' => true];
1480
                break;
1481 6
1482
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1483 6
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1484 5
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1485
1486
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1487 6
                    unset($keys['$db']);
1488 4
                }
1489
1490 6
                if (isset($mapping['targetDocument'])) {
1491
                    unset($keys['$ref'], $keys['$db']);
1492
                }
1493
                break;
1494
1495
            default:
1496 6
                throw new \InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1497 2
        }
1498
1499
        if ($mapping['type'] === 'many') {
1500 4
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1501
        }
1502 4
1503 4
        return array_map(
1504 4
            function ($key) use ($reference, $fieldName) {
1505
                return [$fieldName . '.' . $key, $reference[$key]];
1506
            },
1507
            array_keys($keys)
1508
        );
1509
    }
1510
}
1511