Completed
Push — master ( 8a5bc1...66dea1 )
by Andreas
23:33
created

DocumentPersister::loadEmbedManyCollection()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6

Importance

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