Completed
Push — master ( fc4c80...420611 )
by Maciej
10s
created

ODM/MongoDB/Persisters/DocumentPersister.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Persisters;
4
5
use Doctrine\Common\EventManager;
6
use Doctrine\Common\Persistence\Mapping\MappingException;
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
12
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
13
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
14
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
15
use Doctrine\ODM\MongoDB\LockException;
16
use Doctrine\ODM\MongoDB\LockMode;
17
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
18
use Doctrine\ODM\MongoDB\MongoDBException;
19
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
20
use Doctrine\ODM\MongoDB\Proxy\Proxy;
21
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
22
use Doctrine\ODM\MongoDB\Query\Query;
23
use Doctrine\ODM\MongoDB\Types\Type;
24
use Doctrine\ODM\MongoDB\UnitOfWork;
25
use MongoDB\Collection;
26
use MongoDB\Driver\Cursor;
27
use MongoDB\Driver\Exception\Exception as DriverException;
28
29
/**
30
 * The DocumentPersister is responsible for persisting documents.
31
 *
32
 * @since       1.0
33
 */
34
class DocumentPersister
35
{
36
    /**
37
     * The PersistenceBuilder instance.
38
     *
39
     * @var PersistenceBuilder
40
     */
41
    private $pb;
42
43
    /**
44
     * The DocumentManager instance.
45
     *
46
     * @var DocumentManager
47
     */
48
    private $dm;
49
50
    /**
51
     * The EventManager instance
52
     *
53
     * @var EventManager
54
     */
55
    private $evm;
56
57
    /**
58
     * The UnitOfWork instance.
59
     *
60
     * @var UnitOfWork
61
     */
62
    private $uow;
63
64
    /**
65
     * The ClassMetadata instance for the document type being persisted.
66
     *
67
     * @var ClassMetadata
68
     */
69
    private $class;
70
71
    /**
72
     * The MongoCollection instance for this document.
73
     *
74
     * @var Collection
75
     */
76
    private $collection;
77
78
    /**
79
     * Array of queued inserts for the persister to insert.
80
     *
81
     * @var array
82
     */
83
    private $queuedInserts = array();
84
85
    /**
86
     * Array of queued inserts for the persister to insert.
87
     *
88
     * @var array
89
     */
90
    private $queuedUpserts = array();
91
92
    /**
93
     * The CriteriaMerger instance.
94
     *
95
     * @var CriteriaMerger
96
     */
97
    private $cm;
98
99
    /**
100
     * The CollectionPersister instance.
101
     *
102
     * @var CollectionPersister
103
     */
104
    private $cp;
105
106
    /**
107
     * Initializes this instance.
108
     *
109
     * @param PersistenceBuilder $pb
110
     * @param DocumentManager $dm
111
     * @param EventManager $evm
112
     * @param UnitOfWork $uow
113
     * @param HydratorFactory $hydratorFactory
114
     * @param ClassMetadata $class
115
     * @param CriteriaMerger $cm
116
     */
117 1074
    public function __construct(
118
        PersistenceBuilder $pb,
119
        DocumentManager $dm,
120
        EventManager $evm,
121
        UnitOfWork $uow,
122
        HydratorFactory $hydratorFactory,
123
        ClassMetadata $class,
124
        CriteriaMerger $cm = null
125
    ) {
126 1074
        $this->pb = $pb;
127 1074
        $this->dm = $dm;
128 1074
        $this->evm = $evm;
129 1074
        $this->cm = $cm ?: new CriteriaMerger();
130 1074
        $this->uow = $uow;
131 1074
        $this->hydratorFactory = $hydratorFactory;
132 1074
        $this->class = $class;
133 1074
        $this->collection = $dm->getDocumentCollection($class->name);
134 1074
        $this->cp = $this->uow->getCollectionPersister();
135 1074
    }
136
137
    /**
138
     * @return array
139
     */
140
    public function getInserts()
141
    {
142
        return $this->queuedInserts;
143
    }
144
145
    /**
146
     * @param object $document
147
     * @return bool
148
     */
149
    public function isQueuedForInsert($document)
150
    {
151
        return isset($this->queuedInserts[spl_object_hash($document)]);
152
    }
153
154
    /**
155
     * Adds a document to the queued insertions.
156
     * The document remains queued until {@link executeInserts} is invoked.
157
     *
158
     * @param object $document The document to queue for insertion.
159
     */
160 476
    public function addInsert($document)
161
    {
162 476
        $this->queuedInserts[spl_object_hash($document)] = $document;
163 476
    }
164
165
    /**
166
     * @return array
167
     */
168
    public function getUpserts()
169
    {
170
        return $this->queuedUpserts;
171
    }
172
173
    /**
174
     * @param object $document
175
     * @return boolean
176
     */
177
    public function isQueuedForUpsert($document)
178
    {
179
        return isset($this->queuedUpserts[spl_object_hash($document)]);
180
    }
181
182
    /**
183
     * Adds a document to the queued upserts.
184
     * The document remains queued until {@link executeUpserts} is invoked.
185
     *
186
     * @param object $document The document to queue for insertion.
187
     */
188 84
    public function addUpsert($document)
189
    {
190 84
        $this->queuedUpserts[spl_object_hash($document)] = $document;
191 84
    }
192
193
    /**
194
     * Gets the ClassMetadata instance of the document class this persister is used for.
195
     *
196
     * @return ClassMetadata
197
     */
198
    public function getClassMetadata()
199
    {
200
        return $this->class;
201
    }
202
203
    /**
204
     * Executes all queued document insertions.
205
     *
206
     * Queued documents without an ID will inserted in a batch and queued
207
     * documents with an ID will be upserted individually.
208
     *
209
     * If no inserts are queued, invoking this method is a NOOP.
210
     *
211
     * @param array $options Options for batchInsert() and update() driver methods
212
     */
213 476
    public function executeInserts(array $options = array())
214
    {
215 476
        if ( ! $this->queuedInserts) {
216
            return;
217
        }
218
219 476
        $inserts = array();
220 476
        $options = $this->getWriteOptions($options);
221 476
        foreach ($this->queuedInserts as $oid => $document) {
222 476
            $data = $this->pb->prepareInsertData($document);
223
224
            // Set the initial version for each insert
225 475 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
227 20
                if ($versionMapping['type'] === 'int') {
228 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
229 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
230 2
                } elseif ($versionMapping['type'] === 'date') {
231 2
                    $nextVersionDateTime = new \DateTime();
232 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
233 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
234
                }
235 20
                $data[$versionMapping['name']] = $nextVersion;
236
            }
237
238 475
            $inserts[] = $data;
239
        }
240
241 475
        if ($inserts) {
242
            try {
243 475
                $this->collection->insertMany($inserts, $options);
244 6
            } catch (DriverException $e) {
245 6
                $this->queuedInserts = array();
246 6
                throw $e;
247
            }
248
        }
249
250
        /* All collections except for ones using addToSet have already been
251
         * saved. We have left these to be handled separately to avoid checking
252
         * collection for uniqueness on PHP side.
253
         */
254 475
        foreach ($this->queuedInserts as $document) {
255 475
            $this->handleCollections($document, $options);
256
        }
257
258 475
        $this->queuedInserts = array();
259 475
    }
260
261
    /**
262
     * Executes all queued document upserts.
263
     *
264
     * Queued documents with an ID are upserted individually.
265
     *
266
     * If no upserts are queued, invoking this method is a NOOP.
267
     *
268
     * @param array $options Options for batchInsert() and update() driver methods
269
     */
270 84
    public function executeUpserts(array $options = array())
271
    {
272 84
        if ( ! $this->queuedUpserts) {
273
            return;
274
        }
275
276 84
        $options = $this->getWriteOptions($options);
277 84
        foreach ($this->queuedUpserts as $oid => $document) {
278
            try {
279 84
                $this->executeUpsert($document, $options);
280 84
                $this->handleCollections($document, $options);
281 84
                unset($this->queuedUpserts[$oid]);
282
            } catch (\MongoException $e) {
283
                unset($this->queuedUpserts[$oid]);
284 84
                throw $e;
285
            }
286
        }
287 84
    }
288
289
    /**
290
     * Executes a single upsert in {@link executeUpserts}
291
     *
292
     * @param object $document
293
     * @param array  $options
294
     */
295 84
    private function executeUpsert($document, array $options)
296
    {
297 84
        $options['upsert'] = true;
298 84
        $criteria = $this->getQueryForDocument($document);
299
300 84
        $data = $this->pb->prepareUpsertData($document);
301
302
        // Set the initial version for each upsert
303 84 View Code Duplication
        if ($this->class->isVersioned) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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