Completed
Pull Request — master (#1746)
by Jeremy
08:56
created

DocumentPersister::isQueuedForUpsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
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
use MongoDB\Driver\Exception\WriteException;
29
30
/**
31
 * The DocumentPersister is responsible for persisting documents.
32
 *
33
 * @since       1.0
34
 */
35
class DocumentPersister
36
{
37
    /**
38
     * The PersistenceBuilder instance.
39
     *
40
     * @var PersistenceBuilder
41
     */
42
    private $pb;
43
44
    /**
45
     * The DocumentManager instance.
46
     *
47
     * @var DocumentManager
48
     */
49
    private $dm;
50
51
    /**
52
     * The EventManager instance
53
     *
54
     * @var EventManager
55
     */
56
    private $evm;
57
58
    /**
59
     * The UnitOfWork instance.
60
     *
61
     * @var UnitOfWork
62
     */
63
    private $uow;
64
65
    /**
66
     * The ClassMetadata instance for the document type being persisted.
67
     *
68
     * @var ClassMetadata
69
     */
70
    private $class;
71
72
    /**
73
     * The MongoCollection instance for this document.
74
     *
75
     * @var Collection
76
     */
77
    private $collection;
78
79
    /**
80
     * Array of queued inserts for the persister to insert.
81
     *
82
     * @var array
83
     */
84
    private $queuedInserts = array();
85
86
    /**
87
     * Array of queued inserts for the persister to insert.
88
     *
89
     * @var array
90
     */
91
    private $queuedUpserts = array();
92
93
    /**
94
     * The CriteriaMerger instance.
95
     *
96
     * @var CriteriaMerger
97
     */
98
    private $cm;
99
100
    /**
101
     * The CollectionPersister instance.
102
     *
103
     * @var CollectionPersister
104
     */
105
    private $cp;
106
107
    /**
108
     * The HydratorFactory instance.
109
     *
110
     * @var HydratorFactory
111
     */
112
    private $hydratorFactory;
113
114
    /**
115
     * Initializes this instance.
116
     *
117
     * @param PersistenceBuilder $pb
118
     * @param DocumentManager $dm
119
     * @param EventManager $evm
120
     * @param UnitOfWork $uow
121
     * @param HydratorFactory $hydratorFactory
122
     * @param ClassMetadata $class
123
     * @param CriteriaMerger $cm
124
     */
125 1074
    public function __construct(
126
        PersistenceBuilder $pb,
127
        DocumentManager $dm,
128
        EventManager $evm,
129
        UnitOfWork $uow,
130
        HydratorFactory $hydratorFactory,
131
        ClassMetadata $class,
132
        CriteriaMerger $cm = null
133
    ) {
134 1074
        $this->pb = $pb;
135 1074
        $this->dm = $dm;
136 1074
        $this->evm = $evm;
137 1074
        $this->cm = $cm ?: new CriteriaMerger();
138 1074
        $this->uow = $uow;
139 1074
        $this->hydratorFactory = $hydratorFactory;
140 1074
        $this->class = $class;
141 1074
        $this->collection = $dm->getDocumentCollection($class->name);
142 1074
        $this->cp = $this->uow->getCollectionPersister();
143 1074
    }
144
145
    /**
146
     * @return array
147
     */
148
    public function getInserts()
149
    {
150
        return $this->queuedInserts;
151
    }
152
153
    /**
154
     * @param object $document
155
     * @return bool
156
     */
157
    public function isQueuedForInsert($document)
158
    {
159
        return isset($this->queuedInserts[spl_object_hash($document)]);
160
    }
161
162
    /**
163
     * Adds a document to the queued insertions.
164
     * The document remains queued until {@link executeInserts} is invoked.
165
     *
166
     * @param object $document The document to queue for insertion.
167
     */
168 476
    public function addInsert($document)
169
    {
170 476
        $this->queuedInserts[spl_object_hash($document)] = $document;
171 476
    }
172
173
    /**
174
     * @return array
175
     */
176
    public function getUpserts()
177
    {
178
        return $this->queuedUpserts;
179
    }
180
181
    /**
182
     * @param object $document
183
     * @return boolean
184
     */
185
    public function isQueuedForUpsert($document)
186
    {
187
        return isset($this->queuedUpserts[spl_object_hash($document)]);
188
    }
189
190
    /**
191
     * Adds a document to the queued upserts.
192
     * The document remains queued until {@link executeUpserts} is invoked.
193
     *
194
     * @param object $document The document to queue for insertion.
195
     */
196 84
    public function addUpsert($document)
197
    {
198 84
        $this->queuedUpserts[spl_object_hash($document)] = $document;
199 84
    }
200
201
    /**
202
     * Gets the ClassMetadata instance of the document class this persister is used for.
203
     *
204
     * @return ClassMetadata
205
     */
206
    public function getClassMetadata()
207
    {
208
        return $this->class;
209
    }
210
211
    /**
212
     * Executes all queued document insertions.
213
     *
214
     * Queued documents without an ID will inserted in a batch and queued
215
     * documents with an ID will be upserted individually.
216
     *
217
     * If no inserts are queued, invoking this method is a NOOP.
218
     *
219
     * @param array $options Options for batchInsert() and update() driver methods
220
     */
221 476
    public function executeInserts(array $options = array())
222
    {
223 476
        if ( ! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
224
            return;
225
        }
226
227 476
        $inserts = array();
228 476
        $options = $this->getWriteOptions($options);
229 476
        foreach ($this->queuedInserts as $oid => $document) {
230 476
            $data = $this->pb->prepareInsertData($document);
231
232
            // Set the initial version for each insert
233 475 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
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...
234 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
235 20
                $nextVersion = null;
236 20
                if ($versionMapping['type'] === 'int') {
237 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
238 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
239 2
                } elseif ($versionMapping['type'] === 'date') {
240 2
                    $nextVersionDateTime = new \DateTime();
241 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
242 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
243
                }
244 20
                $data[$versionMapping['name']] = $nextVersion;
245
            }
246
247 475
            $inserts[] = $data;
248
        }
249
250 475
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
251
            try {
252 475
                $this->collection->insertMany($inserts, $options);
253 6
            } catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

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

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

Loading history...
254 6
                $this->queuedInserts = array();
255 6
                throw $e;
256
            }
257
        }
258
259
        /* All collections except for ones using addToSet have already been
260
         * saved. We have left these to be handled separately to avoid checking
261
         * collection for uniqueness on PHP side.
262
         */
263 475
        foreach ($this->queuedInserts as $document) {
264 475
            $this->handleCollections($document, $options);
265
        }
266
267 475
        $this->queuedInserts = array();
268 475
    }
269
270
    /**
271
     * Executes all queued document upserts.
272
     *
273
     * Queued documents with an ID are upserted individually.
274
     *
275
     * If no upserts are queued, invoking this method is a NOOP.
276
     *
277
     * @param array $options Options for batchInsert() and update() driver methods
278
     */
279 84
    public function executeUpserts(array $options = array())
280
    {
281 84
        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...
282
            return;
283
        }
284
285 84
        $options = $this->getWriteOptions($options);
286 84
        foreach ($this->queuedUpserts as $oid => $document) {
287
            try {
288 84
                $this->executeUpsert($document, $options);
289 84
                $this->handleCollections($document, $options);
290 84
                unset($this->queuedUpserts[$oid]);
291
            } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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

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

Loading history...
292
                unset($this->queuedUpserts[$oid]);
293 84
                throw $e;
294
            }
295
        }
296 84
    }
297
298
    /**
299
     * Executes a single upsert in {@link executeUpserts}
300
     *
301
     * @param object $document
302
     * @param array  $options
303
     */
304 84
    private function executeUpsert($document, array $options)
305
    {
306 84
        $options['upsert'] = true;
307 84
        $criteria = $this->getQueryForDocument($document);
308
309 84
        $data = $this->pb->prepareUpsertData($document);
310
311
        // Set the initial version for each upsert
312 84 View Code Duplication
        if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
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...
313 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
314 2
            $nextVersion = null;
315 2
            if ($versionMapping['type'] === 'int') {
316 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
317 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
318 1
            } elseif ($versionMapping['type'] === 'date') {
319 1
                $nextVersionDateTime = new \DateTime();
320 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
321 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
322
            }
323 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
324
        }
325
326 84
        foreach (array_keys($criteria) as $field) {
327 84
            unset($data['$set'][$field]);
328
        }
329
330
        // Do not send an empty $set modifier
331 84
        if (empty($data['$set'])) {
332 17
            unset($data['$set']);
333
        }
334
335
        /* If there are no modifiers remaining, we're upserting a document with
336
         * an identifier as its only field. Since a document with the identifier
337
         * may already exist, the desired behavior is "insert if not exists" and
338
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
339
         * the identifier to the same value in our criteria.
340
         *
341
         * This will fail for versions before MongoDB 2.6, which require an
342
         * empty $set modifier. The best we can do (without attempting to check
343
         * server versions in advance) is attempt the 2.6+ behavior and retry
344
         * after the relevant exception.
345
         *
346
         * See: https://jira.mongodb.org/browse/SERVER-12266
347
         */
348 84
        if (empty($data)) {
349 17
            $retry = true;
350 17
            $data = array('$set' => array('_id' => $criteria['_id']));
351
        }
352
353
        try {
354 84
            $this->collection->updateOne($criteria, $data, $options);
355 84
            return;
356
        } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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

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

Loading history...
357
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
358
                throw $e;
359
            }
360
        }
361
362
        $this->collection->updateOne($criteria, array('$set' => new \stdClass), $options);
363
    }
364
365
    /**
366
     * Updates the already persisted document if it has any new changesets.
367
     *
368
     * @param object $document
369
     * @param array $options Array of options to be used with update()
370
     * @throws \Doctrine\ODM\MongoDB\LockException
371
     */
372 195
    public function update($document, array $options = array())
373
    {
374 195
        $update = $this->pb->prepareUpdateData($document);
375
376 195
        $query = $this->getQueryForDocument($document);
377
378 195
        foreach (array_keys($query) as $field) {
379 195
            unset($update['$set'][$field]);
380
        }
381
382 195
        if (empty($update['$set'])) {
383 89
            unset($update['$set']);
384
        }
385
386
387
        // Include versioning logic to set the new version value in the database
388
        // and to ensure the version has not changed since this document object instance
389
        // was fetched from the database
390 195
        $nextVersion = null;
391 195
        if ($this->class->isVersioned) {
392 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
393 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
394 13
            if ($versionMapping['type'] === 'int') {
395 10
                $nextVersion = $currentVersion + 1;
396 10
                $update['$inc'][$versionMapping['name']] = 1;
397 10
                $query[$versionMapping['name']] = $currentVersion;
398 3
            } elseif ($versionMapping['type'] === 'date') {
399 3
                $nextVersion = new \DateTime();
400 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
401 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
402
            }
403
        }
404
405 195
        if ( ! empty($update)) {
406
            // Include locking logic so that if the document object in memory is currently
407
            // locked then it will remove it, otherwise it ensures the document is not locked.
408 129
            if ($this->class->isLockable) {
409 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
410 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
411 11
                if ($isLocked) {
412 2
                    $update['$unset'] = array($lockMapping['name'] => true);
413
                } else {
414 9
                    $query[$lockMapping['name']] = array('$exists' => false);
415
                }
416
            }
417
418 129
            $options = $this->getWriteOptions($options);
419
420 129
            $result = $this->collection->updateOne($query, $update, $options);
421
422 129
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
423 4
                throw LockException::lockFailed($document);
424 125
            } elseif ($this->class->isVersioned) {
425 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
426
            }
427
        }
428
429 191
        $this->handleCollections($document, $options);
430 191
    }
431
432
    /**
433
     * Removes document from mongo
434
     *
435
     * @param mixed $document
436
     * @param array $options Array of options to be used with remove()
437
     * @throws \Doctrine\ODM\MongoDB\LockException
438
     */
439 32
    public function delete($document, array $options = array())
440
    {
441 32
        $query = $this->getQueryForDocument($document);
442
443 32
        if ($this->class->isLockable) {
444 2
            $query[$this->class->lockField] = array('$exists' => false);
445
        }
446
447 32
        $options = $this->getWriteOptions($options);
448
449 32
        $result = $this->collection->deleteOne($query, $options);
450
451 32
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
452 2
            throw LockException::lockFailed($document);
453
        }
454 30
    }
455
456
    /**
457
     * Refreshes a managed document.
458
     *
459
     * @param object $document The document to refresh.
460
     */
461 20
    public function refresh($document)
462
    {
463 20
        $query = $this->getQueryForDocument($document);
464 20
        $data = $this->collection->findOne($query);
465 20
        $data = $this->hydratorFactory->hydrate($document, $data);
466 20
        $this->uow->setOriginalDocumentData($document, $data);
467 20
    }
468
469
    /**
470
     * Finds a document by a set of criteria.
471
     *
472
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
473
     * be used to match an _id value.
474
     *
475
     * @param mixed   $criteria Query criteria
476
     * @param object  $document Document to load the data into. If not specified, a new document is created.
477
     * @param array   $hints    Hints for document creation
478
     * @param integer $lockMode
479
     * @param array   $sort     Sort array for Cursor::sort()
480
     * @throws \Doctrine\ODM\MongoDB\LockException
481
     * @return object|null The loaded and managed document instance or null if no document was found
482
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
483
     */
484 342
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

    return array();
}

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

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

Loading history...
492 342
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
493 342
        $criteria = $this->addFilterToPreparedQuery($criteria);
494
495 342
        $options = [];
496 342
        if (null !== $sort) {
497 92
            $options['sort'] = $this->prepareSort($sort);
498
        }
499 342
        $result = $this->collection->findOne($criteria, $options);
500
501 342
        if ($this->class->isLockable) {
502 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
503 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
504 1
                throw LockException::lockFailed($result);
505
            }
506
        }
507
508 341
        return $this->createDocument($result, $document, $hints);
509
    }
510
511
    /**
512
     * Finds documents by a set of criteria.
513
     *
514
     * @param array        $criteria Query criteria
515
     * @param array        $sort     Sort array for Cursor::sort()
516
     * @param integer|null $limit    Limit for Cursor::limit()
517
     * @param integer|null $skip     Skip for Cursor::skip()
518
     * @return Iterator
519
     */
520 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
521
    {
522 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
523 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
524 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
525
526 22
        $options = [];
527 22
        if (null !== $sort) {
528 11
            $options['sort'] = $this->prepareSort($sort);
529
        }
530
531 22
        if (null !== $limit) {
532 10
            $options['limit'] = $limit;
533
        }
534
535 22
        if (null !== $skip) {
536 1
            $options['skip'] = $skip;
537
        }
538
539 22
        $baseCursor = $this->collection->find($criteria, $options);
540 22
        $cursor = $this->wrapCursor($baseCursor);
541
542 22
        return $cursor;
543
    }
544
545
    /**
546
     * @param object $document
547
     *
548
     * @return array
549
     * @throws MongoDBException
550
     */
551 268
    private function getShardKeyQuery($document)
552
    {
553 268
        if ( ! $this->class->isSharded()) {
554 264
            return array();
555
        }
556
557 4
        $shardKey = $this->class->getShardKey();
558 4
        $keys = array_keys($shardKey['keys']);
559 4
        $data = $this->uow->getDocumentActualData($document);
560
561 4
        $shardKeyQueryPart = array();
562 4
        foreach ($keys as $key) {
563 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
564 4
            $this->guardMissingShardKey($document, $key, $data);
565
566 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
567 1
                $reference = $this->prepareReference(
568 1
                    $key,
569 1
                    $data[$mapping['fieldName']],
570 1
                    $mapping,
571 1
                    false
572
                );
573 1
                foreach ($reference as $keyValue) {
574 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
575
                }
576
            } else {
577 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
578 4
                $shardKeyQueryPart[$key] = $value;
579
            }
580
        }
581
582 4
        return $shardKeyQueryPart;
583
    }
584
585
    /**
586
     * Wraps the supplied base cursor in the corresponding ODM class.
587
     *
588
     * @param Cursor $baseCursor
589
     * @return Iterator
590
     */
591 22
    private function wrapCursor(Cursor $baseCursor): Iterator
592
    {
593 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
594
    }
595
596
    /**
597
     * Checks whether the given managed document exists in the database.
598
     *
599
     * @param object $document
600
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
601
     */
602 3
    public function exists($document)
603
    {
604 3
        $id = $this->class->getIdentifierObject($document);
605 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
606
    }
607
608
    /**
609
     * Locks document by storing the lock mode on the mapped lock field.
610
     *
611
     * @param object $document
612
     * @param int $lockMode
613
     */
614 5
    public function lock($document, $lockMode)
615
    {
616 5
        $id = $this->uow->getDocumentIdentifier($document);
617 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
618 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
619 5
        $this->collection->updateOne($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
620 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
621 5
    }
622
623
    /**
624
     * Releases any lock that exists on this document.
625
     *
626
     * @param object $document
627
     */
628 1
    public function unlock($document)
629
    {
630 1
        $id = $this->uow->getDocumentIdentifier($document);
631 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
632 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
633 1
        $this->collection->updateOne($criteria, array('$unset' => array($lockMapping['name'] => true)));
634 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
635 1
    }
636
637
    /**
638
     * Creates or fills a single document object from an query result.
639
     *
640
     * @param object $result The query result.
641
     * @param object $document The document object to fill, if any.
642
     * @param array $hints Hints for document creation.
643
     * @return object The filled and managed document object or NULL, if the query result is empty.
644
     */
645 341
    private function createDocument($result, $document = null, array $hints = array())
646
    {
647 341
        if ($result === null) {
648 111
            return null;
649
        }
650
651 300
        if ($document !== null) {
652 38
            $hints[Query::HINT_REFRESH] = true;
653 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
654 38
            $this->uow->registerManaged($document, $id, $result);
655
        }
656
657 300
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
658
    }
659
660
    /**
661
     * Loads a PersistentCollection data. Used in the initialize() method.
662
     *
663
     * @param PersistentCollectionInterface $collection
664
     */
665 163
    public function loadCollection(PersistentCollectionInterface $collection)
666
    {
667 163
        $mapping = $collection->getMapping();
668 163
        switch ($mapping['association']) {
669
            case ClassMetadata::EMBED_MANY:
670 109
                $this->loadEmbedManyCollection($collection);
671 109
                break;
672
673
            case ClassMetadata::REFERENCE_MANY:
674 76
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
675 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
676
                } else {
677 72
                    if ($mapping['isOwningSide']) {
678 60
                        $this->loadReferenceManyCollectionOwningSide($collection);
679
                    } else {
680 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
681
                    }
682
                }
683 76
                break;
684
        }
685 163
    }
686
687 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
688
    {
689 109
        $embeddedDocuments = $collection->getMongoData();
690 109
        $mapping = $collection->getMapping();
691 109
        $owner = $collection->getOwner();
692 109
        if ($embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
693 82
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
694 82
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
695 82
                $embeddedMetadata = $this->dm->getClassMetadata($className);
696 82
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
697
698 82
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
699
700 82
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
701 82
                $id = $embeddedMetadata->identifier && $data[$embeddedMetadata->identifier] ?? null;
702
703 82
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
704 81
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
705
                }
706 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
707 10
                    $collection->set($key, $embeddedDocumentObject);
708
                } else {
709 82
                    $collection->add($embeddedDocumentObject);
710
                }
711
            }
712
        }
713 109
    }
714
715 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
716
    {
717 60
        $hints = $collection->getHints();
718 60
        $mapping = $collection->getMapping();
719 60
        $groupedIds = array();
720
721 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
722
723 60
        foreach ($collection->getMongoData() as $key => $reference) {
724 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
725 54
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
726 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
727
728
            // create a reference to the class and id
729 54
            $reference = $this->dm->getReference($className, $id);
730
731
            // no custom sort so add the references right now in the order they are embedded
732 54
            if ( ! $sorted) {
733 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
734 2
                    $collection->set($key, $reference);
735
                } else {
736 51
                    $collection->add($reference);
737
                }
738
            }
739
740
            // only query for the referenced object if it is not already initialized or the collection is sorted
741 54
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
742 54
                $groupedIds[$className][] = $identifier;
743
            }
744
        }
745 60
        foreach ($groupedIds as $className => $ids) {
746 39
            $class = $this->dm->getClassMetadata($className);
747 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
748 39
            $criteria = $this->cm->merge(
749 39
                array('_id' => array('$in' => array_values($ids))),
750 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
751 39
                $mapping['criteria'] ?? array()
752
            );
753 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
754
755 39
            $options = [];
756 39
            if (isset($mapping['sort'])) {
757 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
758
            }
759 39
            if (isset($mapping['limit'])) {
760
                $options['limit'] = $mapping['limit'];
761
            }
762 39
            if (isset($mapping['skip'])) {
763
                $options['skip'] = $mapping['skip'];
764
            }
765 39
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
766
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
767
            }
768
769 39
            $cursor = $mongoCollection->find($criteria, $options);
770 39
            $documents = $cursor->toArray();
771 39
            foreach ($documents as $documentData) {
772 38
                $document = $this->uow->getById($documentData['_id'], $class);
773 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
774 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
775 38
                    $this->uow->setOriginalDocumentData($document, $data);
776 38
                    $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
777
                }
778 38
                if ($sorted) {
779 39
                    $collection->add($document);
780
                }
781
            }
782
        }
783 60
    }
784
785 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
786
    {
787 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
788 17
        $documents = $query->execute()->toArray();
789 17
        foreach ($documents as $key => $document) {
790 16
            $collection->add($document);
791
        }
792 17
    }
793
794
    /**
795
     * @param PersistentCollectionInterface $collection
796
     *
797
     * @return Query
798
     */
799 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
800
    {
801 17
        $hints = $collection->getHints();
802 17
        $mapping = $collection->getMapping();
803 17
        $owner = $collection->getOwner();
804 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
805 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
806 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? array();
807 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
808
809 17
        $criteria = $this->cm->merge(
810 17
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
811 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
812 17
            $mapping['criteria'] ?? array()
813
        );
814 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
815 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
816 17
            ->setQueryArray($criteria);
817
818 17
        if (isset($mapping['sort'])) {
819 17
            $qb->sort($mapping['sort']);
820
        }
821 17
        if (isset($mapping['limit'])) {
822 2
            $qb->limit($mapping['limit']);
823
        }
824 17
        if (isset($mapping['skip'])) {
825
            $qb->skip($mapping['skip']);
826
        }
827
828 17
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
829
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
830
        }
831
832 17
        foreach ($mapping['prime'] as $field) {
833 4
            $qb->field($field)->prime(true);
834
        }
835
836 17
        return $qb->getQuery();
837
    }
838
839 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
840
    {
841 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
842 5
        $mapping = $collection->getMapping();
843 5
        $documents = $cursor->toArray();
844 5
        foreach ($documents as $key => $obj) {
845 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
846 1
                $collection->set($key, $obj);
847
            } else {
848 5
                $collection->add($obj);
849
            }
850
        }
851 5
    }
852
853
    /**
854
     * @param PersistentCollectionInterface $collection
855
     *
856
     * @return \Iterator
857
     */
858 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
859
    {
860 5
        $mapping = $collection->getMapping();
861 5
        $repositoryMethod = $mapping['repositoryMethod'];
862 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
863 5
            ->$repositoryMethod($collection->getOwner());
864
865 5
        if ( ! $cursor instanceof Iterator) {
866
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object");
867
        }
868
869 5
        if (!empty($mapping['prime'])) {
870 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
871 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
872 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
873
874 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
875
        }
876
877 5
        return $cursor;
878
    }
879
880
    /**
881
     * Prepare a projection array by converting keys, which are PHP property
882
     * names, to MongoDB field names.
883
     *
884
     * @param array $fields
885
     * @return array
886
     */
887 14
    public function prepareProjection(array $fields)
888
    {
889 14
        $preparedFields = array();
890
891 14
        foreach ($fields as $key => $value) {
892 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
893
        }
894
895 14
        return $preparedFields;
896
    }
897
898
    /**
899
     * @param string $sort
900
     * @return int
901
     */
902 25
    private function getSortDirection($sort)
903
    {
904 25
        switch (strtolower($sort)) {
905 25
            case 'desc':
906 15
                return -1;
907
908 22
            case 'asc':
909 13
                return 1;
910
        }
911
912 12
        return $sort;
913
    }
914
915
    /**
916
     * Prepare a sort specification array by converting keys to MongoDB field
917
     * names and changing direction strings to int.
918
     *
919
     * @param array $fields
920
     * @return array
921
     */
922 141
    public function prepareSort(array $fields)
923
    {
924 141
        $sortFields = [];
925
926 141
        foreach ($fields as $key => $value) {
927 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
928
        }
929
930 141
        return $sortFields;
931
    }
932
933
    /**
934
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
935
     *
936
     * @param string $fieldName
937
     * @return string
938
     */
939 433
    public function prepareFieldName($fieldName)
940
    {
941 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
942
943 433
        return $fieldNames[0][0];
944
    }
945
946
    /**
947
     * Adds discriminator criteria to an already-prepared query.
948
     *
949
     * This method should be used once for query criteria and not be used for
950
     * nested expressions. It should be called before
951
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
952
     *
953
     * @param array $preparedQuery
954
     * @return array
955
     */
956 492
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
957
    {
958
        /* If the class has a discriminator field, which is not already in the
959
         * criteria, inject it now. The field/values need no preparation.
960
         */
961 492
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
962 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
963 29
            if (count($discriminatorValues) === 1) {
964 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
965
            } else {
966 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
967
            }
968
        }
969
970 492
        return $preparedQuery;
971
    }
972
973
    /**
974
     * Adds filter criteria to an already-prepared query.
975
     *
976
     * This method should be used once for query criteria and not be used for
977
     * nested expressions. It should be called after
978
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
979
     *
980
     * @param array $preparedQuery
981
     * @return array
982
     */
983 493
    public function addFilterToPreparedQuery(array $preparedQuery)
984
    {
985
        /* If filter criteria exists for this class, prepare it and merge
986
         * over the existing query.
987
         *
988
         * @todo Consider recursive merging in case the filter criteria and
989
         * prepared query both contain top-level $and/$or operators.
990
         */
991 493
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
992 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
993
        }
994
995 493
        return $preparedQuery;
996
    }
997
998
    /**
999
     * Prepares the query criteria or new document object.
1000
     *
1001
     * PHP field names and types will be converted to those used by MongoDB.
1002
     *
1003
     * @param array $query
1004
     * @param bool $isNewObj
1005
     * @return array
1006
     */
1007 525
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1008
    {
1009 525
        $preparedQuery = array();
1010
1011 525
        foreach ($query as $key => $value) {
1012
            // Recursively prepare logical query clauses
1013 483
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1014 20
                foreach ($value as $k2 => $v2) {
1015 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1016
                }
1017 20
                continue;
1018
            }
1019
1020 483
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1021 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1022 38
                continue;
1023
            }
1024
1025 483
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1026 483
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1027 483
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1028 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1029 483
                    : Type::convertPHPToDatabaseValue($preparedValue);
1030
            }
1031
        }
1032
1033 525
        return $preparedQuery;
1034
    }
1035
1036
    /**
1037
     * Prepares a query value and converts the PHP value to the database value
1038
     * if it is an identifier.
1039
     *
1040
     * It also handles converting $fieldName to the database name if they are different.
1041
     *
1042
     * @param string $fieldName
1043
     * @param mixed $value
1044
     * @param ClassMetadata $class        Defaults to $this->class
1045
     * @param bool $prepareValue Whether or not to prepare the value
1046
     * @param bool $inNewObj Whether or not newObj is being prepared
1047
     * @return array An array of tuples containing prepared field names and values
1048
     */
1049 876
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1050
    {
1051 876
        $class = $class ?? $this->class;
1052
1053
        // @todo Consider inlining calls to ClassMetadata methods
1054
1055
        // Process all non-identifier fields by translating field names
1056 876
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1057 247
            $mapping = $class->fieldMappings[$fieldName];
1058 247
            $fieldName = $mapping['name'];
1059
1060 247
            if ( ! $prepareValue) {
1061 52
                return [[$fieldName, $value]];
1062
            }
1063
1064
            // Prepare mapped, embedded objects
1065 205
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1066 205
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1067 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1068
            }
1069
1070 203
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoDB\BSON\ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

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