Completed
Push — master ( 23b3b5...5850fd )
by Andreas
11:22
created

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