Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

DocumentPersister   F

Complexity

Total Complexity 247

Size/Duplication

Total Lines 1437
Duplicated Lines 2.51 %

Coupling/Cohesion

Components 1
Dependencies 25

Test Coverage

Coverage 93.66%

Importance

Changes 0
Metric Value
wmc 247
lcom 1
cbo 25
dl 36
loc 1437
ccs 547
cts 584
cp 0.9366
rs 0.5217
c 0
b 0
f 0

46 Methods

Rating   Name   Duplication   Size   Complexity  
B getShardKeyQuery() 0 33 6
A wrapCursor() 0 4 1
A exists() 0 5 1
A lock() 0 8 1
A unlock() 0 8 1
A createDocument() 0 14 3
C loadEmbedManyCollection() 0 29 7
A loadReferenceManyCollectionInverseSide() 0 8 2
B loadCollection() 0 21 6
A __construct() 0 19 2
A getInserts() 0 4 1
A isQueuedForInsert() 0 4 1
A addInsert() 0 4 1
A getUpserts() 0 4 1
A isQueuedForUpsert() 0 4 1
A addUpsert() 0 4 1
A getClassMetadata() 0 4 1
C executeInserts() 12 47 9
A executeUpserts() 0 18 4
C executeUpsert() 12 59 10
C update() 0 59 13
B delete() 0 16 5
A refresh() 0 7 1
C load() 0 26 8
B loadAll() 0 24 4
F loadReferenceManyCollectionOwningSide() 0 69 18
D createReferenceManyInverseSideQuery() 0 39 9
A loadReferenceManyWithRepositoryMethod() 0 13 3
A createReferenceManyWithRepositoryMethodCursor() 0 21 3
A prepareProjection() 0 10 2
A getSortDirection() 0 12 3
A prepareSort() 0 10 2
A prepareFieldName() 0 6 1
A addDiscriminatorToPreparedQuery() 0 16 4
A addFilterToPreparedQuery() 0 14 2
D prepareQueryOrNewObj() 0 28 10
F prepareQueryElement() 9 187 48
C prepareQueryExpression() 0 27 8
B hasDBRefFields() 0 18 8
B hasQueryOperators() 0 18 7
B getClassDiscriminatorValues() 3 16 5
B handleCollections() 0 19 6
B guardMissingShardKey() 0 16 5
A getQueryForDocument() 0 10 1
A getWriteOptions() 0 10 2
D prepareReference() 0 40 9

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocumentPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentPersister, and based on these observations, apply Extract Interface, too.

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\ODM\MongoDB\DocumentManager;
25
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
26
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
27
use Doctrine\ODM\MongoDB\Iterator\Iterator;
28
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
29
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
30
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
31
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
32
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
33
use Doctrine\ODM\MongoDB\LockException;
34
use Doctrine\ODM\MongoDB\LockMode;
35
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
36
use Doctrine\ODM\MongoDB\MongoDBException;
37
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
38
use Doctrine\ODM\MongoDB\Proxy\Proxy;
39
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
40
use Doctrine\ODM\MongoDB\Query\Query;
41
use Doctrine\ODM\MongoDB\Types\Type;
42
use Doctrine\ODM\MongoDB\UnitOfWork;
43
use MongoDB\Collection;
44
use MongoDB\Driver\Cursor;
45
use MongoDB\Driver\Exception\Exception as DriverException;
46
47
/**
48
 * The DocumentPersister is responsible for persisting documents.
49
 *
50
 * @since       1.0
51
 */
52
class DocumentPersister
53
{
54
    /**
55
     * The PersistenceBuilder instance.
56
     *
57
     * @var PersistenceBuilder
58
     */
59
    private $pb;
60
61
    /**
62
     * The DocumentManager instance.
63
     *
64
     * @var DocumentManager
65
     */
66
    private $dm;
67
68
    /**
69
     * The EventManager instance
70
     *
71
     * @var EventManager
72
     */
73
    private $evm;
74
75
    /**
76
     * The UnitOfWork instance.
77
     *
78
     * @var UnitOfWork
79
     */
80
    private $uow;
81
82
    /**
83
     * The ClassMetadata instance for the document type being persisted.
84
     *
85
     * @var ClassMetadata
86
     */
87
    private $class;
88
89
    /**
90
     * The MongoCollection instance for this document.
91
     *
92
     * @var Collection
93
     */
94
    private $collection;
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedInserts = array();
102
103
    /**
104
     * Array of queued inserts for the persister to insert.
105
     *
106
     * @var array
107
     */
108
    private $queuedUpserts = array();
109
110
    /**
111
     * The CriteriaMerger instance.
112
     *
113
     * @var CriteriaMerger
114
     */
115
    private $cm;
116
117
    /**
118
     * The CollectionPersister instance.
119
     *
120
     * @var CollectionPersister
121
     */
122
    private $cp;
123
124
    /**
125
     * Initializes this instance.
126
     *
127
     * @param PersistenceBuilder $pb
128
     * @param DocumentManager $dm
129
     * @param EventManager $evm
130
     * @param UnitOfWork $uow
131
     * @param HydratorFactory $hydratorFactory
132
     * @param ClassMetadata $class
133
     * @param CriteriaMerger $cm
134
     */
135 1084
    public function __construct(
136
        PersistenceBuilder $pb,
137
        DocumentManager $dm,
138
        EventManager $evm,
139
        UnitOfWork $uow,
140
        HydratorFactory $hydratorFactory,
141
        ClassMetadata $class,
142
        CriteriaMerger $cm = null
143
    ) {
144 1084
        $this->pb = $pb;
145 1084
        $this->dm = $dm;
146 1084
        $this->evm = $evm;
147 1084
        $this->cm = $cm ?: new CriteriaMerger();
148 1084
        $this->uow = $uow;
149 1084
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
150 1084
        $this->class = $class;
151 1084
        $this->collection = $dm->getDocumentCollection($class->name);
152 1084
        $this->cp = $this->uow->getCollectionPersister();
153 1084
    }
154
155
    /**
156
     * @return array
157
     */
158
    public function getInserts()
159
    {
160
        return $this->queuedInserts;
161
    }
162
163
    /**
164
     * @param object $document
165
     * @return bool
166
     */
167
    public function isQueuedForInsert($document)
168
    {
169
        return isset($this->queuedInserts[spl_object_hash($document)]);
170
    }
171
172
    /**
173
     * Adds a document to the queued insertions.
174
     * The document remains queued until {@link executeInserts} is invoked.
175
     *
176
     * @param object $document The document to queue for insertion.
177
     */
178 485
    public function addInsert($document)
179
    {
180 485
        $this->queuedInserts[spl_object_hash($document)] = $document;
181 485
    }
182
183
    /**
184
     * @return array
185
     */
186
    public function getUpserts()
187
    {
188
        return $this->queuedUpserts;
189
    }
190
191
    /**
192
     * @param object $document
193
     * @return boolean
194
     */
195
    public function isQueuedForUpsert($document)
196
    {
197
        return isset($this->queuedUpserts[spl_object_hash($document)]);
198
    }
199
200
    /**
201
     * Adds a document to the queued upserts.
202
     * The document remains queued until {@link executeUpserts} is invoked.
203
     *
204
     * @param object $document The document to queue for insertion.
205
     */
206 85
    public function addUpsert($document)
207
    {
208 85
        $this->queuedUpserts[spl_object_hash($document)] = $document;
209 85
    }
210
211
    /**
212
     * Gets the ClassMetadata instance of the document class this persister is used for.
213
     *
214
     * @return ClassMetadata
215
     */
216
    public function getClassMetadata()
217
    {
218
        return $this->class;
219
    }
220
221
    /**
222
     * Executes all queued document insertions.
223
     *
224
     * Queued documents without an ID will inserted in a batch and queued
225
     * documents with an ID will be upserted individually.
226
     *
227
     * If no inserts are queued, invoking this method is a NOOP.
228
     *
229
     * @param array $options Options for batchInsert() and update() driver methods
230
     */
231 485
    public function executeInserts(array $options = array())
232
    {
233 485
        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...
234
            return;
235
        }
236
237 485
        $inserts = array();
238 485
        $options = $this->getWriteOptions($options);
239 485
        foreach ($this->queuedInserts as $oid => $document) {
240 485
            $data = $this->pb->prepareInsertData($document);
241
242
            // Set the initial version for each insert
243 484 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...
244 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
245 20
                if ($versionMapping['type'] === 'int') {
246 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
247 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
248 2
                } elseif ($versionMapping['type'] === 'date') {
249 2
                    $nextVersionDateTime = new \DateTime();
250 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
251 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
252
                }
253 20
                $data[$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
254
            }
255
256 484
            $inserts[] = $data;
257
        }
258
259 484
        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...
260
            try {
261 484
                $this->collection->insertMany($inserts, $options);
262 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...
263 6
                $this->queuedInserts = array();
264 6
                throw $e;
265
            }
266
        }
267
268
        /* All collections except for ones using addToSet have already been
269
         * saved. We have left these to be handled separately to avoid checking
270
         * collection for uniqueness on PHP side.
271
         */
272 484
        foreach ($this->queuedInserts as $document) {
273 484
            $this->handleCollections($document, $options);
274
        }
275
276 484
        $this->queuedInserts = array();
277 484
    }
278
279
    /**
280
     * Executes all queued document upserts.
281
     *
282
     * Queued documents with an ID are upserted individually.
283
     *
284
     * If no upserts are queued, invoking this method is a NOOP.
285
     *
286
     * @param array $options Options for batchInsert() and update() driver methods
287
     */
288 85
    public function executeUpserts(array $options = array())
289
    {
290 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...
291
            return;
292
        }
293
294 85
        $options = $this->getWriteOptions($options);
295 85
        foreach ($this->queuedUpserts as $oid => $document) {
296
            try {
297 85
                $this->executeUpsert($document, $options);
298 85
                $this->handleCollections($document, $options);
299 85
                unset($this->queuedUpserts[$oid]);
300
            } catch (\MongoException $e) {
301
                unset($this->queuedUpserts[$oid]);
302 85
                throw $e;
303
            }
304
        }
305 85
    }
306
307
    /**
308
     * Executes a single upsert in {@link executeUpserts}
309
     *
310
     * @param object $document
311
     * @param array  $options
312
     */
313 85
    private function executeUpsert($document, array $options)
314
    {
315 85
        $options['upsert'] = true;
316 85
        $criteria = $this->getQueryForDocument($document);
317
318 85
        $data = $this->pb->prepareUpsertData($document);
319
320
        // Set the initial version for each upsert
321 85 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...
322 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
323 2
            if ($versionMapping['type'] === 'int') {
324 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
325 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
326 1
            } elseif ($versionMapping['type'] === 'date') {
327 1
                $nextVersionDateTime = new \DateTime();
328 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
329 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
330
            }
331 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
332
        }
333
334 85
        foreach (array_keys($criteria) as $field) {
335 85
            unset($data['$set'][$field]);
336
        }
337
338
        // Do not send an empty $set modifier
339 85
        if (empty($data['$set'])) {
340 17
            unset($data['$set']);
341
        }
342
343
        /* If there are no modifiers remaining, we're upserting a document with
344
         * an identifier as its only field. Since a document with the identifier
345
         * may already exist, the desired behavior is "insert if not exists" and
346
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
347
         * the identifier to the same value in our criteria.
348
         *
349
         * This will fail for versions before MongoDB 2.6, which require an
350
         * empty $set modifier. The best we can do (without attempting to check
351
         * server versions in advance) is attempt the 2.6+ behavior and retry
352
         * after the relevant exception.
353
         *
354
         * See: https://jira.mongodb.org/browse/SERVER-12266
355
         */
356 85
        if (empty($data)) {
357 17
            $retry = true;
358 17
            $data = array('$set' => array('_id' => $criteria['_id']));
359
        }
360
361
        try {
362 85
            $this->collection->updateOne($criteria, $data, $options);
363 85
            return;
364
        } catch (\MongoCursorException $e) {
365
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
366
                throw $e;
367
            }
368
        }
369
370
        $this->collection->updateOne($criteria, array('$set' => new \stdClass), $options);
371
    }
372
373
    /**
374
     * Updates the already persisted document if it has any new changesets.
375
     *
376
     * @param object $document
377
     * @param array $options Array of options to be used with update()
378
     * @throws \Doctrine\ODM\MongoDB\LockException
379
     */
380 199
    public function update($document, array $options = array())
381
    {
382 199
        $update = $this->pb->prepareUpdateData($document);
383
384 199
        $query = $this->getQueryForDocument($document);
385
386 199
        foreach (array_keys($query) as $field) {
387 199
            unset($update['$set'][$field]);
388
        }
389
390 199
        if (empty($update['$set'])) {
391 89
            unset($update['$set']);
392
        }
393
394
395
        // Include versioning logic to set the new version value in the database
396
        // and to ensure the version has not changed since this document object instance
397
        // was fetched from the database
398 199
        $nextVersion = null;
399 199
        if ($this->class->isVersioned) {
400 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
401 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
402 13
            if ($versionMapping['type'] === 'int') {
403 10
                $nextVersion = $currentVersion + 1;
404 10
                $update['$inc'][$versionMapping['name']] = 1;
405 10
                $query[$versionMapping['name']] = $currentVersion;
406 3
            } elseif ($versionMapping['type'] === 'date') {
407 3
                $nextVersion = new \DateTime();
408 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
409 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
410
            }
411
        }
412
413 199
        if ( ! empty($update)) {
414
            // Include locking logic so that if the document object in memory is currently
415
            // locked then it will remove it, otherwise it ensures the document is not locked.
416 133
            if ($this->class->isLockable) {
417 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
418 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
419 11
                if ($isLocked) {
420 2
                    $update['$unset'] = array($lockMapping['name'] => true);
421
                } else {
422 9
                    $query[$lockMapping['name']] = array('$exists' => false);
423
                }
424
            }
425
426 133
            $options = $this->getWriteOptions($options);
427
428 133
            $result = $this->collection->updateOne($query, $update, $options);
429
430 133
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
431 4
                throw LockException::lockFailed($document);
432 129
            } elseif ($this->class->isVersioned) {
433 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
434
            }
435
        }
436
437 195
        $this->handleCollections($document, $options);
438 195
    }
439
440
    /**
441
     * Removes document from mongo
442
     *
443
     * @param mixed $document
444
     * @param array $options Array of options to be used with remove()
445
     * @throws \Doctrine\ODM\MongoDB\LockException
446
     */
447 33
    public function delete($document, array $options = array())
448
    {
449 33
        $query = $this->getQueryForDocument($document);
450
451 33
        if ($this->class->isLockable) {
452 2
            $query[$this->class->lockField] = array('$exists' => false);
453
        }
454
455 33
        $options = $this->getWriteOptions($options);
456
457 33
        $result = $this->collection->deleteOne($query, $options);
458
459 33
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
460 2
            throw LockException::lockFailed($document);
461
        }
462 31
    }
463
464
    /**
465
     * Refreshes a managed document.
466
     *
467
     * @param object $document The document to refresh.
468
     */
469 20
    public function refresh($document)
470
    {
471 20
        $query = $this->getQueryForDocument($document);
472 20
        $data = $this->collection->findOne($query);
473 20
        $data = $this->hydratorFactory->hydrate($document, $data);
474 20
        $this->uow->setOriginalDocumentData($document, $data);
475 20
    }
476
477
    /**
478
     * Finds a document by a set of criteria.
479
     *
480
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
481
     * be used to match an _id value.
482
     *
483
     * @param mixed   $criteria Query criteria
484
     * @param object  $document Document to load the data into. If not specified, a new document is created.
485
     * @param array   $hints    Hints for document creation
486
     * @param integer $lockMode
487
     * @param array   $sort     Sort array for Cursor::sort()
488
     * @throws \Doctrine\ODM\MongoDB\LockException
489
     * @return object|null The loaded and managed document instance or null if no document was found
490
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
491
     */
492 348
    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...
493
    {
494
        // TODO: remove this
495 348
        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...
496
            $criteria = array('_id' => $criteria);
497
        }
498
499 348
        $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...
500 348
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
501 348
        $criteria = $this->addFilterToPreparedQuery($criteria);
502
503 348
        $options = [];
504 348
        if (null !== $sort) {
505 92
            $options['sort'] = $this->prepareSort($sort);
506
        }
507 348
        $result = $this->collection->findOne($criteria, $options);
508
509 348
        if ($this->class->isLockable) {
510 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
511 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
512 1
                throw LockException::lockFailed($result);
513
            }
514
        }
515
516 347
        return $this->createDocument($result, $document, $hints);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->collection->findOne($criteria, $options) on line 507 can also be of type array or null; however, Doctrine\ODM\MongoDB\Per...ister::createDocument() does only seem to accept object, 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...
517
    }
518
519
    /**
520
     * Finds documents by a set of criteria.
521
     *
522
     * @param array        $criteria Query criteria
523
     * @param array        $sort     Sort array for Cursor::sort()
524
     * @param integer|null $limit    Limit for Cursor::limit()
525
     * @param integer|null $skip     Skip for Cursor::skip()
526
     * @return Iterator
527
     */
528 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
529
    {
530 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
531 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
532 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
533
534 22
        $options = [];
535 22
        if (null !== $sort) {
536 11
            $options['sort'] = $this->prepareSort($sort);
537
        }
538
539 22
        if (null !== $limit) {
540 10
            $options['limit'] = $limit;
541
        }
542
543 22
        if (null !== $skip) {
544 1
            $options['skip'] = $skip;
545
        }
546
547 22
        $baseCursor = $this->collection->find($criteria, $options);
548 22
        $cursor = $this->wrapCursor($baseCursor);
549
550 22
        return $cursor;
551
    }
552
553
    /**
554
     * @param object $document
555
     *
556
     * @return array
557
     * @throws MongoDBException
558
     */
559 274
    private function getShardKeyQuery($document)
560
    {
561 274
        if ( ! $this->class->isSharded()) {
562 270
            return array();
563
        }
564
565 4
        $shardKey = $this->class->getShardKey();
566 4
        $keys = array_keys($shardKey['keys']);
567 4
        $data = $this->uow->getDocumentActualData($document);
568
569 4
        $shardKeyQueryPart = array();
570 4
        foreach ($keys as $key) {
571 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
572 4
            $this->guardMissingShardKey($document, $key, $data);
573
574 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
575 1
                $reference = $this->prepareReference(
576 1
                    $key,
577 1
                    $data[$mapping['fieldName']],
578 1
                    $mapping,
579 1
                    false
580
                );
581 1
                foreach ($reference as $keyValue) {
582 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
583
                }
584
            } else {
585 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
586 4
                $shardKeyQueryPart[$key] = $value;
587
            }
588
        }
589
590 4
        return $shardKeyQueryPart;
591
    }
592
593
    /**
594
     * Wraps the supplied base cursor in the corresponding ODM class.
595
     *
596
     * @param Cursor $baseCursor
597
     * @return Iterator
598
     */
599 22
    private function wrapCursor(Cursor $baseCursor): Iterator
600
    {
601 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
602
    }
603
604
    /**
605
     * Checks whether the given managed document exists in the database.
606
     *
607
     * @param object $document
608
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
609
     */
610 3
    public function exists($document)
611
    {
612 3
        $id = $this->class->getIdentifierObject($document);
613 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
614
    }
615
616
    /**
617
     * Locks document by storing the lock mode on the mapped lock field.
618
     *
619
     * @param object $document
620
     * @param int $lockMode
621
     */
622 5
    public function lock($document, $lockMode)
623
    {
624 5
        $id = $this->uow->getDocumentIdentifier($document);
625 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
626 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
627 5
        $this->collection->updateOne($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
628 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
629 5
    }
630
631
    /**
632
     * Releases any lock that exists on this document.
633
     *
634
     * @param object $document
635
     */
636 1
    public function unlock($document)
637
    {
638 1
        $id = $this->uow->getDocumentIdentifier($document);
639 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
640 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
641 1
        $this->collection->updateOne($criteria, array('$unset' => array($lockMapping['name'] => true)));
642 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
643 1
    }
644
645
    /**
646
     * Creates or fills a single document object from an query result.
647
     *
648
     * @param object $result The query result.
649
     * @param object $document The document object to fill, if any.
650
     * @param array $hints Hints for document creation.
651
     * @return object The filled and managed document object or NULL, if the query result is empty.
652
     */
653 347
    private function createDocument($result, $document = null, array $hints = array())
654
    {
655 347
        if ($result === null) {
656 112
            return null;
657
        }
658
659 306
        if ($document !== null) {
660 39
            $hints[Query::HINT_REFRESH] = true;
661 39
            $id = $this->class->getPHPIdentifierValue($result['_id']);
662 39
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
663
        }
664
665 306
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
666
    }
667
668
    /**
669
     * Loads a PersistentCollection data. Used in the initialize() method.
670
     *
671
     * @param PersistentCollectionInterface $collection
672
     */
673 163
    public function loadCollection(PersistentCollectionInterface $collection)
674
    {
675 163
        $mapping = $collection->getMapping();
676 163
        switch ($mapping['association']) {
677 163
            case ClassMetadata::EMBED_MANY:
678 109
                $this->loadEmbedManyCollection($collection);
679 109
                break;
680
681 76
            case ClassMetadata::REFERENCE_MANY:
682 76
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
683 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
684
                } else {
685 72
                    if ($mapping['isOwningSide']) {
686 60
                        $this->loadReferenceManyCollectionOwningSide($collection);
687
                    } else {
688 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
689
                    }
690
                }
691 76
                break;
692
        }
693 163
    }
694
695 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
696
    {
697 109
        $embeddedDocuments = $collection->getMongoData();
698 109
        $mapping = $collection->getMapping();
699 109
        $owner = $collection->getOwner();
700 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...
701 82
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
702 82
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
703 82
                $embeddedMetadata = $this->dm->getClassMetadata($className);
704 82
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
705
706 82
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
707
708 82
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
709 82
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
710 24
                    ? $data[$embeddedMetadata->identifier]
711 82
                    : null;
712
713 82
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
714 81
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
715
                }
716 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
717 10
                    $collection->set($key, $embeddedDocumentObject);
718
                } else {
719 82
                    $collection->add($embeddedDocumentObject);
720
                }
721
            }
722
        }
723 109
    }
724
725 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
726
    {
727 60
        $hints = $collection->getHints();
728 60
        $mapping = $collection->getMapping();
729 60
        $groupedIds = array();
730
731 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
732
733 60
        foreach ($collection->getMongoData() as $key => $reference) {
734 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
735 54
            $identifier = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
736 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
737
738
            // create a reference to the class and id
739 54
            $reference = $this->dm->getReference($className, $id);
740
741
            // no custom sort so add the references right now in the order they are embedded
742 54
            if ( ! $sorted) {
743 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
744 2
                    $collection->set($key, $reference);
745
                } else {
746 51
                    $collection->add($reference);
747
                }
748
            }
749
750
            // only query for the referenced object if it is not already initialized or the collection is sorted
751 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...
752 54
                $groupedIds[$className][] = $identifier;
753
            }
754
        }
755 60
        foreach ($groupedIds as $className => $ids) {
756 39
            $class = $this->dm->getClassMetadata($className);
757 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
758 39
            $criteria = $this->cm->merge(
759 39
                array('_id' => array('$in' => array_values($ids))),
760 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
761 39
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
762
            );
763 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
764
765 39
            $options = [];
766 39
            if (isset($mapping['sort'])) {
767 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
768
            }
769 39
            if (isset($mapping['limit'])) {
770
                $options['limit'] = $mapping['limit'];
771
            }
772 39
            if (isset($mapping['skip'])) {
773
                $options['skip'] = $mapping['skip'];
774
            }
775 39
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
776
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
777
            }
778
779 39
            $cursor = $mongoCollection->find($criteria, $options);
780 39
            $documents = $cursor->toArray();
781 39
            foreach ($documents as $documentData) {
782 38
                $document = $this->uow->getById($documentData['_id'], $class);
783 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
784 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
785 38
                    $this->uow->setOriginalDocumentData($document, $data);
786 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...
787
                }
788 38
                if ($sorted) {
789 39
                    $collection->add($document);
790
                }
791
            }
792
        }
793 60
    }
794
795 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
796
    {
797 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
798 17
        $documents = $query->execute()->toArray();
799 17
        foreach ($documents as $key => $document) {
800 16
            $collection->add($document);
801
        }
802 17
    }
803
804
    /**
805
     * @param PersistentCollectionInterface $collection
806
     *
807
     * @return Query
808
     */
809 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
810
    {
811 17
        $hints = $collection->getHints();
812 17
        $mapping = $collection->getMapping();
813 17
        $owner = $collection->getOwner();
814 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
815 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
816 17
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
817 17
        $mappedByFieldName = ClassMetadataInfo::getReferenceFieldName(isset($mappedByMapping['storeAs']) ? $mappedByMapping['storeAs'] : ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
818
819 17
        $criteria = $this->cm->merge(
820 17
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
821 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
822 17
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
823
        );
824 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
825 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
826 17
            ->setQueryArray($criteria);
827
828 17
        if (isset($mapping['sort'])) {
829 17
            $qb->sort($mapping['sort']);
830
        }
831 17
        if (isset($mapping['limit'])) {
832 2
            $qb->limit($mapping['limit']);
833
        }
834 17
        if (isset($mapping['skip'])) {
835
            $qb->skip($mapping['skip']);
836
        }
837
838 17
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
839
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
840
        }
841
842 17
        foreach ($mapping['prime'] as $field) {
843 4
            $qb->field($field)->prime(true);
844
        }
845
846 17
        return $qb->getQuery();
847
    }
848
849 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
850
    {
851 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
852 5
        $mapping = $collection->getMapping();
853 5
        $documents = $cursor->toArray();
854 5
        foreach ($documents as $key => $obj) {
855 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
856 1
                $collection->set($key, $obj);
857
            } else {
858 5
                $collection->add($obj);
859
            }
860
        }
861 5
    }
862
863
    /**
864
     * @param PersistentCollectionInterface $collection
865
     *
866
     * @return \Iterator
867
     */
868 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
869
    {
870 5
        $mapping = $collection->getMapping();
871 5
        $repositoryMethod = $mapping['repositoryMethod'];
872 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
873 5
            ->$repositoryMethod($collection->getOwner());
874
875 5
        if ( ! $cursor instanceof Iterator) {
876
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object");
877
        }
878
879 5
        if (!empty($mapping['prime'])) {
880 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
881 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
882 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
883
884 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
885
        }
886
887 5
        return $cursor;
888
    }
889
890
    /**
891
     * Prepare a projection array by converting keys, which are PHP property
892
     * names, to MongoDB field names.
893
     *
894
     * @param array $fields
895
     * @return array
896
     */
897 14
    public function prepareProjection(array $fields)
898
    {
899 14
        $preparedFields = array();
900
901 14
        foreach ($fields as $key => $value) {
902 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
903
        }
904
905 14
        return $preparedFields;
906
    }
907
908
    /**
909
     * @param $sort
910
     * @return int
911
     */
912 25
    private function getSortDirection($sort)
913
    {
914 25
        switch (strtolower($sort)) {
915 25
            case 'desc':
916 15
                return -1;
917
918 22
            case 'asc':
919 13
                return 1;
920
        }
921
922 12
        return $sort;
923
    }
924
925
    /**
926
     * Prepare a sort specification array by converting keys to MongoDB field
927
     * names and changing direction strings to int.
928
     *
929
     * @param array $fields
930
     * @return array
931
     */
932 141
    public function prepareSort(array $fields)
933
    {
934 141
        $sortFields = [];
935
936 141
        foreach ($fields as $key => $value) {
937 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
938
        }
939
940 141
        return $sortFields;
941
    }
942
943
    /**
944
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
945
     *
946
     * @param string $fieldName
947
     * @return string
948
     */
949 433
    public function prepareFieldName($fieldName)
950
    {
951 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
952
953 433
        return $fieldNames[0][0];
954
    }
955
956
    /**
957
     * Adds discriminator criteria to an already-prepared query.
958
     *
959
     * This method should be used once for query criteria and not be used for
960
     * nested expressions. It should be called before
961
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
962
     *
963
     * @param array $preparedQuery
964
     * @return array
965
     */
966 498
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
967
    {
968
        /* If the class has a discriminator field, which is not already in the
969
         * criteria, inject it now. The field/values need no preparation.
970
         */
971 498
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
972 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
973 29
            if (count($discriminatorValues) === 1) {
974 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
975
            } else {
976 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
977
            }
978
        }
979
980 498
        return $preparedQuery;
981
    }
982
983
    /**
984
     * Adds filter criteria to an already-prepared query.
985
     *
986
     * This method should be used once for query criteria and not be used for
987
     * nested expressions. It should be called after
988
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
989
     *
990
     * @param array $preparedQuery
991
     * @return array
992
     */
993 499
    public function addFilterToPreparedQuery(array $preparedQuery)
994
    {
995
        /* If filter criteria exists for this class, prepare it and merge
996
         * over the existing query.
997
         *
998
         * @todo Consider recursive merging in case the filter criteria and
999
         * prepared query both contain top-level $and/$or operators.
1000
         */
1001 499
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
1002 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1003
        }
1004
1005 499
        return $preparedQuery;
1006
    }
1007
1008
    /**
1009
     * Prepares the query criteria or new document object.
1010
     *
1011
     * PHP field names and types will be converted to those used by MongoDB.
1012
     *
1013
     * @param array $query
1014
     * @param bool $isNewObj
1015
     * @return array
1016
     */
1017 531
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1018
    {
1019 531
        $preparedQuery = array();
1020
1021 531
        foreach ($query as $key => $value) {
1022
            // Recursively prepare logical query clauses
1023 489
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1024 20
                foreach ($value as $k2 => $v2) {
1025 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1026
                }
1027 20
                continue;
1028
            }
1029
1030 489
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1031 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1032 38
                continue;
1033
            }
1034
1035 489
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1036 489
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1037 489
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1038 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1039 489
                    : Type::convertPHPToDatabaseValue($preparedValue);
1040
            }
1041
        }
1042
1043 531
        return $preparedQuery;
1044
    }
1045
1046
    /**
1047
     * Prepares a query value and converts the PHP value to the database value
1048
     * if it is an identifier.
1049
     *
1050
     * It also handles converting $fieldName to the database name if they are different.
1051
     *
1052
     * @param string $fieldName
1053
     * @param mixed $value
1054
     * @param ClassMetadata $class        Defaults to $this->class
1055
     * @param bool $prepareValue Whether or not to prepare the value
1056
     * @param bool $inNewObj Whether or not newObj is being prepared
1057
     * @return array An array of tuples containing prepared field names and values
1058
     */
1059 882
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1060
    {
1061 882
        $class = isset($class) ? $class : $this->class;
1062
1063
        // @todo Consider inlining calls to ClassMetadataInfo methods
1064
1065
        // Process all non-identifier fields by translating field names
1066 882
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1067 248
            $mapping = $class->fieldMappings[$fieldName];
1068 248
            $fieldName = $mapping['name'];
1069
1070 248
            if ( ! $prepareValue) {
1071 52
                return [[$fieldName, $value]];
1072
            }
1073
1074
            // Prepare mapped, embedded objects
1075 206
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1076 206
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1077 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1078
            }
1079
1080 204
            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...
1081
                try {
1082 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1083 1
                } catch (MappingException $e) {
1084
                    // do nothing in case passed object is not mapped document
1085
                }
1086
            }
1087
1088
            // No further preparation unless we're dealing with a simple reference
1089
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1090 191
            $arrayValue = (array) $value;
1091 191
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1092 127
                return [[$fieldName, $value]];
1093
            }
1094
1095
            // Additional preparation for one or more simple reference values
1096 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1097
1098 91
            if ( ! is_array($value)) {
1099 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1100
            }
1101
1102
            // Objects without operators or with DBRef fields can be converted immediately
1103 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...
1104 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1105
            }
1106
1107 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1108
        }
1109
1110
        // Process identifier fields
1111 794
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1112 339
            $fieldName = '_id';
1113
1114 339
            if ( ! $prepareValue) {
1115 42
                return [[$fieldName, $value]];
1116
            }
1117
1118 300
            if ( ! is_array($value)) {
1119 277
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1120
            }
1121
1122
            // Objects without operators or with DBRef fields can be converted immediately
1123 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...
1124 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1125
            }
1126
1127 56
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1128
        }
1129
1130
        // No processing for unmapped, non-identifier, non-dotted field names
1131 553
        if (strpos($fieldName, '.') === false) {
1132 414
            return [[$fieldName, $value]];
1133
        }
1134
1135
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1136
         *
1137
         * We can limit parsing here, since at most three segments are
1138
         * significant: "fieldName.objectProperty" with an optional index or key
1139
         * for collections stored as either BSON arrays or objects.
1140
         */
1141 152
        $e = explode('.', $fieldName, 4);
1142
1143
        // No further processing for unmapped fields
1144 152
        if ( ! isset($class->fieldMappings[$e[0]])) {
1145 6
            return [[$fieldName, $value]];
1146
        }
1147
1148 147
        $mapping = $class->fieldMappings[$e[0]];
1149 147
        $e[0] = $mapping['name'];
1150
1151
        // Hash and raw fields will not be prepared beyond the field name
1152 147
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1153 1
            $fieldName = implode('.', $e);
1154
1155 1
            return [[$fieldName, $value]];
1156
        }
1157
1158 146
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1159 146
                && isset($e[2])) {
1160 1
            $objectProperty = $e[2];
1161 1
            $objectPropertyPrefix = $e[1] . '.';
1162 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1163 145
        } elseif ($e[1] != '$') {
1164 144
            $fieldName = $e[0] . '.' . $e[1];
1165 144
            $objectProperty = $e[1];
1166 144
            $objectPropertyPrefix = '';
1167 144
            $nextObjectProperty = implode('.', array_slice($e, 2));
1168 1
        } elseif (isset($e[2])) {
1169 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1170 1
            $objectProperty = $e[2];
1171 1
            $objectPropertyPrefix = $e[1] . '.';
1172 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1173
        } else {
1174 1
            $fieldName = $e[0] . '.' . $e[1];
1175
1176 1
            return [[$fieldName, $value]];
1177
        }
1178
1179
        // No further processing for fields without a targetDocument mapping
1180 146
        if ( ! isset($mapping['targetDocument'])) {
1181 3
            if ($nextObjectProperty) {
1182
                $fieldName .= '.'.$nextObjectProperty;
1183
            }
1184
1185 3
            return [[$fieldName, $value]];
1186
        }
1187
1188 143
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1189
1190
        // No further processing for unmapped targetDocument fields
1191 143
        if ( ! $targetClass->hasField($objectProperty)) {
1192 25
            if ($nextObjectProperty) {
1193
                $fieldName .= '.'.$nextObjectProperty;
1194
            }
1195
1196 25
            return [[$fieldName, $value]];
1197
        }
1198
1199 123
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1200 123
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1201
1202
        // Prepare DBRef identifiers or the mapped field's property path
1203 123
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1204 105
            ? ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $e[0])
1205 123
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1206
1207
        // Process targetDocument identifier fields
1208 123
        if ($objectPropertyIsId) {
1209 106
            if ( ! $prepareValue) {
1210 7
                return [[$fieldName, $value]];
1211
            }
1212
1213 99
            if ( ! is_array($value)) {
1214 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1215
            }
1216
1217
            // Objects without operators or with DBRef fields can be converted immediately
1218 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...
1219 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1220
            }
1221
1222 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1223
        }
1224
1225
        /* The property path may include a third field segment, excluding the
1226
         * collection item pointer. If present, this next object property must
1227
         * be processed recursively.
1228
         */
1229 17
        if ($nextObjectProperty) {
1230
            // Respect the targetDocument's class metadata when recursing
1231 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1232 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1233 14
                : null;
1234
1235 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1236
1237 14
            return array_map(function ($preparedTuple) use ($fieldName) {
1238 14
                list($key, $value) = $preparedTuple;
1239
1240 14
                return [$fieldName . '.' . $key, $value];
1241 14
            }, $fieldNames);
1242
        }
1243
1244 5
        return [[$fieldName, $value]];
1245
    }
1246
1247
    /**
1248
     * Prepares a query expression.
1249
     *
1250
     * @param array|object  $expression
1251
     * @param ClassMetadata $class
1252
     * @return array
1253
     */
1254 78
    private function prepareQueryExpression($expression, $class)
1255
    {
1256 78
        foreach ($expression as $k => $v) {
1257
            // Ignore query operators whose arguments need no type conversion
1258 78
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1259 16
                continue;
1260
            }
1261
1262
            // Process query operators whose argument arrays need type conversion
1263 78
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1264 76
                foreach ($v as $k2 => $v2) {
1265 76
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1266
                }
1267 76
                continue;
1268
            }
1269
1270
            // Recursively process expressions within a $not operator
1271 18
            if ($k === '$not' && is_array($v)) {
1272 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1273 15
                continue;
1274
            }
1275
1276 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1277
        }
1278
1279 78
        return $expression;
1280
    }
1281
1282
    /**
1283
     * Checks whether the value has DBRef fields.
1284
     *
1285
     * This method doesn't check if the the value is a complete DBRef object,
1286
     * although it should return true for a DBRef. Rather, we're checking that
1287
     * the value has one or more fields for a DBref. In practice, this could be
1288
     * $elemMatch criteria for matching a DBRef.
1289
     *
1290
     * @param mixed $value
1291
     * @return boolean
1292
     */
1293 79
    private function hasDBRefFields($value)
1294
    {
1295 79
        if ( ! is_array($value) && ! is_object($value)) {
1296
            return false;
1297
        }
1298
1299 79
        if (is_object($value)) {
1300
            $value = get_object_vars($value);
1301
        }
1302
1303 79
        foreach ($value as $key => $_) {
1304 79
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1305 79
                return true;
1306
            }
1307
        }
1308
1309 78
        return false;
1310
    }
1311
1312
    /**
1313
     * Checks whether the value has query operators.
1314
     *
1315
     * @param mixed $value
1316
     * @return boolean
1317
     */
1318 83
    private function hasQueryOperators($value)
1319
    {
1320 83
        if ( ! is_array($value) && ! is_object($value)) {
1321
            return false;
1322
        }
1323
1324 83
        if (is_object($value)) {
1325
            $value = get_object_vars($value);
1326
        }
1327
1328 83
        foreach ($value as $key => $_) {
1329 83
            if (isset($key[0]) && $key[0] === '$') {
1330 83
                return true;
1331
            }
1332
        }
1333
1334 11
        return false;
1335
    }
1336
1337
    /**
1338
     * Gets the array of discriminator values for the given ClassMetadata
1339
     *
1340
     * @param ClassMetadata $metadata
1341
     * @return array
1342
     */
1343 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1344
    {
1345 29
        $discriminatorValues = array($metadata->discriminatorValue);
1346 29
        foreach ($metadata->subClasses as $className) {
1347 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1348 8
                $discriminatorValues[] = $key;
1349
            }
1350
        }
1351
1352
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1353 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...
1354 2
            $discriminatorValues[] = null;
1355
        }
1356
1357 29
        return $discriminatorValues;
1358
    }
1359
1360 557
    private function handleCollections($document, $options)
1361
    {
1362
        // Collection deletions (deletions of complete collections)
1363 557
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1364 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1365 103
                $this->cp->delete($coll, $options);
1366
            }
1367
        }
1368
        // Collection updates (deleteRows, updateRows, insertRows)
1369 557
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1370 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1371 103
                $this->cp->update($coll, $options);
1372
            }
1373
        }
1374
        // Take new snapshots from visited collections
1375 557
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1376 226
            $coll->takeSnapshot();
1377
        }
1378 557
    }
1379
1380
    /**
1381
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1382
     * Also, shard key field should be present in actual document data.
1383
     *
1384
     * @param object $document
1385
     * @param string $shardKeyField
1386
     * @param array  $actualDocumentData
1387
     *
1388
     * @throws MongoDBException
1389
     */
1390 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1391
    {
1392 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1393 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1394
1395 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1396 4
        $fieldName = $fieldMapping['fieldName'];
1397
1398 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1399
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1400
        }
1401
1402 4
        if (!isset($actualDocumentData[$fieldName])) {
1403
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1404
        }
1405 4
    }
1406
1407
    /**
1408
     * Get shard key aware query for single document.
1409
     *
1410
     * @param object $document
1411
     *
1412
     * @return array
1413
     */
1414 270
    private function getQueryForDocument($document)
1415
    {
1416 270
        $id = $this->uow->getDocumentIdentifier($document);
1417 270
        $id = $this->class->getDatabaseIdentifierValue($id);
1418
1419 270
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1420 270
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1421
1422 270
        return $query;
1423
    }
1424
1425
    /**
1426
     * @param array $options
1427
     *
1428
     * @return array
1429
     */
1430 558
    private function getWriteOptions(array $options = array())
1431
    {
1432 558
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1433 558
        $documentOptions = [];
1434 558
        if ($this->class->hasWriteConcern()) {
1435 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1436
        }
1437
1438 558
        return array_merge($defaultOptions, $documentOptions, $options);
1439
    }
1440
1441
    /**
1442
     * @param string $fieldName
1443
     * @param mixed $value
1444
     * @param array $mapping
1445
     * @param bool $inNewObj
1446
     * @return array
1447
     */
1448 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1449
    {
1450 15
        $reference = $this->dm->createReference($value, $mapping);
1451 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1452 8
            return [[$fieldName, $reference]];
1453
        }
1454
1455 6
        switch ($mapping['storeAs']) {
1456 6
            case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
1457
                $keys = ['id' => true];
1458
                break;
1459
1460 6
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF:
1461 1
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1462 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1463
1464 6
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF) {
1465 5
                    unset($keys['$db']);
1466
                }
1467
1468 6
                if (isset($mapping['targetDocument'])) {
1469 4
                    unset($keys['$ref'], $keys['$db']);
1470
                }
1471 6
                break;
1472
1473
            default:
1474
                throw new \InvalidArgumentException("Reference type {$mapping['storeAs']} is invalid.");
1475
        }
1476
1477 6
        if ($mapping['type'] === 'many') {
1478 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1479
        } else {
1480 4
            return array_map(
1481 4
                function ($key) use ($reference, $fieldName) {
1482 4
                    return [$fieldName . '.' . $key, $reference[$key]];
1483 4
                },
1484 4
                array_keys($keys)
1485
            );
1486
        }
1487
    }
1488
}
1489