Completed
Pull Request — master (#1728)
by Pascal
09:37
created

DocumentPersister::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 11
cts 11
cp 1
rs 9.4285
cc 2
eloc 17
nc 2
nop 7
crap 2
1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Persisters;
4
5
use Doctrine\Common\EventManager;
6
use Doctrine\Common\Persistence\Mapping\MappingException;
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
12
use Doctrine\ODM\MongoDB\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 1075
    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 1075
        $this->pb = $pb;
128 1075
        $this->dm = $dm;
129 1075
        $this->evm = $evm;
130 1075
        $this->cm = $cm ?: new CriteriaMerger();
131 1075
        $this->uow = $uow;
132 1075
        $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 1075
        $this->class = $class;
134 1075
        $this->collection = $dm->getDocumentCollection($class->name);
135 1075
        $this->cp = $this->uow->getCollectionPersister();
136 1075
    }
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 477
    public function addInsert($document)
162
    {
163 477
        $this->queuedInserts[spl_object_hash($document)] = $document;
164 477
    }
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 84
    public function addUpsert($document)
190
    {
191 84
        $this->queuedUpserts[spl_object_hash($document)] = $document;
192 84
    }
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 477
    public function executeInserts(array $options = array())
215
    {
216 477
        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 477
        $inserts = array();
221 477
        $options = $this->getWriteOptions($options);
222 477
        foreach ($this->queuedInserts as $oid => $document) {
223 477
            $data = $this->pb->prepareInsertData($document);
224
225
            // Set the initial version for each insert
226 476 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 476
            $inserts[] = $data;
240
        }
241
242 476
        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 476
                $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 476
        foreach ($this->queuedInserts as $document) {
256 476
            $this->handleCollections($document, $options);
257
        }
258
259 476
        $this->queuedInserts = array();
260 476
    }
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 84
    public function executeUpserts(array $options = array())
272
    {
273 84
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
274
            return;
275
        }
276
277 84
        $options = $this->getWriteOptions($options);
278 84
        foreach ($this->queuedUpserts as $oid => $document) {
279
            try {
280 84
                $this->executeUpsert($document, $options);
281 84
                $this->handleCollections($document, $options);
282 84
                unset($this->queuedUpserts[$oid]);
283
            } catch (\MongoException $e) {
284
                unset($this->queuedUpserts[$oid]);
285 84
                throw $e;
286
            }
287
        }
288 84
    }
289
290
    /**
291
     * Executes a single upsert in {@link executeUpserts}
292
     *
293
     * @param object $document
294
     * @param array  $options
295
     */
296 84
    private function executeUpsert($document, array $options)
297
    {
298 84
        $options['upsert'] = true;
299 84
        $criteria = $this->getQueryForDocument($document);
300
301 84
        $data = $this->pb->prepareUpsertData($document);
302
303
        // Set the initial version for each upsert
304 84 View Code Duplication
        if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
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 84
        foreach (array_keys($criteria) as $field) {
318 84
            unset($data['$set'][$field]);
319
        }
320
321
        // Do not send an empty $set modifier
322 84
        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 84
        if (empty($data)) {
340 17
            $retry = true;
341 17
            $data = array('$set' => array('_id' => $criteria['_id']));
342
        }
343
344
        try {
345 84
            $this->collection->updateOne($criteria, $data, $options);
346 84
            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 195
    public function update($document, array $options = array())
364
    {
365 195
        $update = $this->pb->prepareUpdateData($document);
366
367 195
        $query = $this->getQueryForDocument($document);
368
369 195
        foreach (array_keys($query) as $field) {
370 195
            unset($update['$set'][$field]);
371
        }
372
373 195
        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 195
        $nextVersion = null;
382 195
        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 195
        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 129
            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 129
            $options = $this->getWriteOptions($options);
410
411 129
            $result = $this->collection->updateOne($query, $update, $options);
412
413 129
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
414 4
                throw LockException::lockFailed($document);
415 125
            } elseif ($this->class->isVersioned) {
416 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
417
            }
418
        }
419
420 191
        $this->handleCollections($document, $options);
421 191
    }
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 32
    public function delete($document, array $options = array())
431
    {
432 32
        $query = $this->getQueryForDocument($document);
433
434 32
        if ($this->class->isLockable) {
435 2
            $query[$this->class->lockField] = array('$exists' => false);
436
        }
437
438 32
        $options = $this->getWriteOptions($options);
439
440 32
        $result = $this->collection->deleteOne($query, $options);
441
442 32
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
443 2
            throw LockException::lockFailed($document);
444
        }
445 30
    }
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 342
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

    return array();
}

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

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

Loading history...
483 342
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
484 342
        $criteria = $this->addFilterToPreparedQuery($criteria);
485
486 342
        $options = [];
487 342
        if (null !== $sort) {
488 92
            $options['sort'] = $this->prepareSort($sort);
489
        }
490 342
        $result = $this->collection->findOne($criteria, $options);
491
492 342
        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 341
        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 268
    private function getShardKeyQuery($document)
543
    {
544 268
        if ( ! $this->class->isSharded()) {
545 264
            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 341
    private function createDocument($result, $document = null, array $hints = array())
637
    {
638 341
        if ($result === null) {
639 112
            return null;
640
        }
641
642 300
        if ($document !== null) {
643 38
            $hints[Query::HINT_REFRESH] = true;
644 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
645 38
            $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 300
        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
694 82
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
695 81
                    $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
                }
697 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
698 10
                    $collection->set($key, $embeddedDocumentObject);
699
                } else {
700 82
                    $collection->add($embeddedDocumentObject);
701
                }
702
            }
703
        }
704 109
    }
705
706 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
707
    {
708 60
        $hints = $collection->getHints();
709 60
        $mapping = $collection->getMapping();
710 60
        $groupedIds = array();
711
712 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
713
714 60
        foreach ($collection->getMongoData() as $key => $reference) {
715 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
716 54
            $identifier = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
717 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
718
719
            // create a reference to the class and id
720 54
            $reference = $this->dm->getReference($className, $id);
721
722
            // no custom sort so add the references right now in the order they are embedded
723 54
            if ( ! $sorted) {
724 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
725 2
                    $collection->set($key, $reference);
726
                } else {
727 51
                    $collection->add($reference);
728
                }
729
            }
730
731
            // only query for the referenced object if it is not already initialized or the collection is sorted
732 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...
733 54
                $groupedIds[$className][] = $identifier;
734
            }
735
        }
736 60
        foreach ($groupedIds as $className => $ids) {
737 39
            $class = $this->dm->getClassMetadata($className);
738 39
            $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
            );
744 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
745
746 39
            $options = [];
747 39
            if (isset($mapping['sort'])) {
748 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
749
            }
750 39
            if (isset($mapping['limit'])) {
751
                $options['limit'] = $mapping['limit'];
752
            }
753 39
            if (isset($mapping['skip'])) {
754
                $options['skip'] = $mapping['skip'];
755
            }
756 39
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
757
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
758
            }
759
760 39
            $cursor = $mongoCollection->find($criteria, $options);
761 39
            $documents = $cursor->toArray();
762 39
            foreach ($documents as $documentData) {
763 38
                $document = $this->uow->getById($documentData['_id'], $class);
764 38
                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
                }
769 38
                if ($sorted) {
770 39
                    $collection->add($document);
771
                }
772
            }
773
        }
774 60
    }
775
776 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
777
    {
778 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
779 17
        $documents = $query->execute()->toArray();
780 17
        foreach ($documents as $key => $document) {
781 16
            $collection->add($document);
782
        }
783 17
    }
784
785
    /**
786
     * @param PersistentCollectionInterface $collection
787
     *
788
     * @return Query
789
     */
790 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
791
    {
792 17
        $hints = $collection->getHints();
793 17
        $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
800 17
        $criteria = $this->cm->merge(
801 17
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
802 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
803 17
            $mapping['criteria'] ?? array()
804
        );
805 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
806 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
807 17
            ->setQueryArray($criteria);
808
809 17
        if (isset($mapping['sort'])) {
810 17
            $qb->sort($mapping['sort']);
811
        }
812 17
        if (isset($mapping['limit'])) {
813 2
            $qb->limit($mapping['limit']);
814
        }
815 17
        if (isset($mapping['skip'])) {
816
            $qb->skip($mapping['skip']);
817
        }
818
819 17
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
820
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
821
        }
822
823 17
        foreach ($mapping['prime'] as $field) {
824 4
            $qb->field($field)->prime(true);
825
        }
826
827 17
        return $qb->getQuery();
828
    }
829
830 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
831
    {
832 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
833 5
        $mapping = $collection->getMapping();
834 5
        $documents = $cursor->toArray();
835 5
        foreach ($documents as $key => $obj) {
836 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
837 1
                $collection->set($key, $obj);
838
            } else {
839 5
                $collection->add($obj);
840
            }
841
        }
842 5
    }
843
844
    /**
845
     * @param PersistentCollectionInterface $collection
846
     *
847
     * @return \Iterator
848
     */
849 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
850
    {
851 5
        $mapping = $collection->getMapping();
852 5
        $repositoryMethod = $mapping['repositoryMethod'];
853 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
854 5
            ->$repositoryMethod($collection->getOwner());
855
856 5
        if ( ! $cursor instanceof Iterator) {
857
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object");
858
        }
859
860 5
        if (!empty($mapping['prime'])) {
861 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
862 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
863 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
864
865 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
866
        }
867
868 5
        return $cursor;
869
    }
870
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 14
    public function prepareProjection(array $fields)
879
    {
880 14
        $preparedFields = array();
881
882 14
        foreach ($fields as $key => $value) {
883 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
884
        }
885
886 14
        return $preparedFields;
887
    }
888
889
    /**
890
     * @param $sort
891
     * @return int
892
     */
893 25
    private function getSortDirection($sort)
894
    {
895 25
        switch (strtolower($sort)) {
896
            case 'desc':
897 15
                return -1;
898
899
            case 'asc':
900 13
                return 1;
901
        }
902
903 12
        return $sort;
904
    }
905
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 141
    public function prepareSort(array $fields)
914
    {
915 141
        $sortFields = [];
916
917 141
        foreach ($fields as $key => $value) {
918 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
919
        }
920
921 141
        return $sortFields;
922
    }
923
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 433
    public function prepareFieldName($fieldName)
931
    {
932 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
933
934 433
        return $fieldNames[0][0];
935
    }
936
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 493
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
948
    {
949
        /* 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 493
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
953 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
954 29
            if (count($discriminatorValues) === 1) {
955 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
956
            } else {
957 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
958
            }
959
        }
960
961 493
        return $preparedQuery;
962
    }
963
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 494
    public function addFilterToPreparedQuery(array $preparedQuery)
975
    {
976
        /* 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 494
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
983 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
984
        }
985
986 494
        return $preparedQuery;
987
    }
988
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 526
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
999
    {
1000 526
        $preparedQuery = array();
1001
1002 526
        foreach ($query as $key => $value) {
1003
            // Recursively prepare logical query clauses
1004 484
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1005 20
                foreach ($value as $k2 => $v2) {
1006 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1007
                }
1008 20
                continue;
1009
            }
1010
1011 484
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1012 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1013 38
                continue;
1014
            }
1015
1016 484
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1017 484
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1018 484
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1019 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1020 484
                    : Type::convertPHPToDatabaseValue($preparedValue);
1021
            }
1022
        }
1023
1024 526
        return $preparedQuery;
1025
    }
1026
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 877
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1041
    {
1042 877
        $class = $class ?? $this->class;
1043
1044
        // @todo Consider inlining calls to ClassMetadataInfo methods
1045
1046
        // Process all non-identifier fields by translating field names
1047 877
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1048 242
            $mapping = $class->fieldMappings[$fieldName];
1049 242
            $fieldName = $mapping['name'];
1050
1051 242
            if ( ! $prepareValue) {
1052 51
                return [[$fieldName, $value]];
1053
            }
1054
1055
            // Prepare mapped, embedded objects
1056 201
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1057 201
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1058 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1059
            }
1060
1061 199
            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 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1064 1
                } catch (MappingException $e) {
1065
                    // do nothing in case passed object is not mapped document
1066
                }
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 186
            $arrayValue = (array) $value;
1072 186
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1073 122
                return [[$fieldName, $value]];
1074
            }
1075
1076
            // Additional preparation for one or more simple reference values
1077 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1078
1079 91
            if ( ! is_array($value)) {
1080 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1081
            }
1082
1083
            // Objects without operators or with DBRef fields can be converted immediately
1084 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...
1085 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1086
            }
1087
1088 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1089
        }
1090
1091
        // Process identifier fields
1092 790
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1093 330
            $fieldName = '_id';
1094
1095 330
            if ( ! $prepareValue) {
1096 39
                return [[$fieldName, $value]];
1097
            }
1098
1099 294
            if ( ! is_array($value)) {
1100 271
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1101
            }
1102
1103
            // Objects without operators or with DBRef fields can be converted immediately
1104 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...
1105 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1106
            }
1107
1108 56
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1109
        }
1110
1111
        // No processing for unmapped, non-identifier, non-dotted field names
1112 554
        if (strpos($fieldName, '.') === false) {
1113 410
            return [[$fieldName, $value]];
1114
        }
1115
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 153
        $e = explode('.', $fieldName, 4);
1123
1124
        // No further processing for unmapped fields
1125 153
        if ( ! isset($class->fieldMappings[$e[0]])) {
1126 5
            return [[$fieldName, $value]];
1127
        }
1128
1129 148
        $mapping = $class->fieldMappings[$e[0]];
1130 148
        $e[0] = $mapping['name'];
1131
1132
        // Hash and raw fields will not be prepared beyond the field name
1133 148
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1134 1
            $fieldName = implode('.', $e);
1135
1136 1
            return [[$fieldName, $value]];
1137
        }
1138
1139 147
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1140 147
                && isset($e[2])) {
1141 1
            $objectProperty = $e[2];
1142 1
            $objectPropertyPrefix = $e[1] . '.';
1143 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1144 146
        } elseif ($e[1] != '$') {
1145 145
            $fieldName = $e[0] . '.' . $e[1];
1146 145
            $objectProperty = $e[1];
1147 145
            $objectPropertyPrefix = '';
1148 145
            $nextObjectProperty = implode('.', array_slice($e, 2));
1149 1
        } elseif (isset($e[2])) {
1150 1
            $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
        } else {
1155 1
            $fieldName = $e[0] . '.' . $e[1];
1156
1157 1
            return [[$fieldName, $value]];
1158
        }
1159
1160
        // No further processing for fields without a targetDocument mapping
1161 147
        if ( ! isset($mapping['targetDocument']) || ! empty($mapping['embedded'])) {
1162 22
            if ($nextObjectProperty) {
1163 15
                $fieldName .= '.'.$nextObjectProperty;
1164
            }
1165
1166 22
            return [[$fieldName, $value]];
1167
        }
1168
1169 125
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1170
1171
        // No further processing for unmapped targetDocument fields
1172 125
        if ( ! $targetClass->hasField($objectProperty)) {
1173 24
            if ($nextObjectProperty) {
1174
                $fieldName .= '.'.$nextObjectProperty;
1175
            }
1176
1177 24
            return [[$fieldName, $value]];
1178
        }
1179
1180 105
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1181 105
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1182
1183
        // Prepare DBRef identifiers or the mapped field's property path
1184 105
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1185 105
            ? ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $e[0])
1186 105
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1187
1188
        // Process targetDocument identifier fields
1189 105
        if ($objectPropertyIsId) {
1190 105
            if ( ! $prepareValue) {
1191 6
                return [[$fieldName, $value]];
1192
            }
1193
1194 99
            if ( ! is_array($value)) {
1195 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1196
            }
1197
1198
            // Objects without operators or with DBRef fields can be converted immediately
1199 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...
1200 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1201
            }
1202
1203 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1204
        }
1205
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
            $nextTargetClass = isset($targetMapping['targetDocument'])
1213
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1214
                : null;
1215
1216
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1217
1218
            return array_map(function ($preparedTuple) use ($fieldName) {
1219
                list($key, $value) = $preparedTuple;
1220
1221
                return [$fieldName . '.' . $key, $value];
1222
            }, $fieldNames);
1223
        }
1224
1225
        return [[$fieldName, $value]];
1226
    }
1227
1228
    /**
1229
     * Prepares a query expression.
1230
     *
1231
     * @param array|object  $expression
1232
     * @param ClassMetadata $class
1233
     * @return array
1234
     */
1235 78
    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 16
                continue;
1241
            }
1242
1243
            // Process query operators whose argument arrays need type conversion
1244 78
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1245 76
                foreach ($v as $k2 => $v2) {
1246 76
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1247
                }
1248 76
                continue;
1249
            }
1250
1251
            // Recursively process expressions within a $not operator
1252 18
            if ($k === '$not' && is_array($v)) {
1253 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1254 15
                continue;
1255
            }
1256
1257 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1258
        }
1259
1260 78
        return $expression;
1261
    }
1262
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 79
    private function hasDBRefFields($value)
1275
    {
1276 79
        if ( ! is_array($value) && ! is_object($value)) {
1277
            return false;
1278
        }
1279
1280 79
        if (is_object($value)) {
1281
            $value = get_object_vars($value);
1282
        }
1283
1284 79
        foreach ($value as $key => $_) {
1285 79
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1286 79
                return true;
1287
            }
1288
        }
1289
1290 78
        return false;
1291
    }
1292
1293
    /**
1294
     * Checks whether the value has query operators.
1295
     *
1296
     * @param mixed $value
1297
     * @return boolean
1298
     */
1299 83
    private function hasQueryOperators($value)
1300
    {
1301 83
        if ( ! is_array($value) && ! is_object($value)) {
1302
            return false;
1303
        }
1304
1305 83
        if (is_object($value)) {
1306
            $value = get_object_vars($value);
1307
        }
1308
1309 83
        foreach ($value as $key => $_) {
1310 83
            if (isset($key[0]) && $key[0] === '$') {
1311 83
                return true;
1312
            }
1313
        }
1314
1315 11
        return false;
1316
    }
1317
1318
    /**
1319
     * Gets the array of discriminator values for the given ClassMetadata
1320
     *
1321
     * @param ClassMetadata $metadata
1322
     * @return array
1323
     */
1324 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1325
    {
1326 29
        $discriminatorValues = array($metadata->discriminatorValue);
1327 29
        foreach ($metadata->subClasses as $className) {
1328 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1329 8
                $discriminatorValues[] = $key;
1330
            }
1331
        }
1332
1333
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1334 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...
1335 2
            $discriminatorValues[] = null;
1336
        }
1337
1338 29
        return $discriminatorValues;
1339
    }
1340
1341 548
    private function handleCollections($document, $options)
1342
    {
1343
        // Collection deletions (deletions of complete collections)
1344 548
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1345 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1346 103
                $this->cp->delete($coll, $options);
1347
            }
1348
        }
1349
        // Collection updates (deleteRows, updateRows, insertRows)
1350 548
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1351 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1352 103
                $this->cp->update($coll, $options);
1353
            }
1354
        }
1355
        // Take new snapshots from visited collections
1356 548
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1357 227
            $coll->takeSnapshot();
1358
        }
1359 548
    }
1360
1361
    /**
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 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1372
    {
1373 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1374 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1375
1376 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1377 4
        $fieldName = $fieldMapping['fieldName'];
1378
1379 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1380
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1381
        }
1382
1383 4
        if (!isset($actualDocumentData[$fieldName])) {
1384
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1385
        }
1386 4
    }
1387
1388
    /**
1389
     * Get shard key aware query for single document.
1390
     *
1391
     * @param object $document
1392
     *
1393
     * @return array
1394
     */
1395 264
    private function getQueryForDocument($document)
1396
    {
1397 264
        $id = $this->uow->getDocumentIdentifier($document);
1398 264
        $id = $this->class->getDatabaseIdentifierValue($id);
1399
1400 264
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1401 264
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1402
1403 264
        return $query;
1404
    }
1405
1406
    /**
1407
     * @param array $options
1408
     *
1409
     * @return array
1410
     */
1411 549
    private function getWriteOptions(array $options = array())
1412
    {
1413 549
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1414 549
        $documentOptions = [];
1415 549
        if ($this->class->hasWriteConcern()) {
1416 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1417
        }
1418
1419 549
        return array_merge($defaultOptions, $documentOptions, $options);
1420
    }
1421
1422
    /**
1423
     * @param string $fieldName
1424
     * @param mixed $value
1425
     * @param array $mapping
1426
     * @param bool $inNewObj
1427
     * @return array
1428
     */
1429 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1430
    {
1431 15
        $reference = $this->dm->createReference($value, $mapping);
1432 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1433 8
            return [[$fieldName, $reference]];
1434
        }
1435
1436 6
        switch ($mapping['storeAs']) {
1437
            case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
1438
                $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 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1444
1445 6
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF) {
1446 5
                    unset($keys['$db']);
1447
                }
1448
1449 6
                if (isset($mapping['targetDocument'])) {
1450 4
                    unset($keys['$ref'], $keys['$db']);
1451
                }
1452 6
                break;
1453
1454
            default:
1455
                throw new \InvalidArgumentException("Reference type {$mapping['storeAs']} is invalid.");
1456
        }
1457
1458 6
        if ($mapping['type'] === 'many') {
1459 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1460
        }
1461
1462 4
        return array_map(
1463 4
            function ($key) use ($reference, $fieldName) {
1464 4
                return [$fieldName . '.' . $key, $reference[$key]];
1465 4
            },
1466 4
            array_keys($keys)
1467
        );
1468
    }
1469
}
1470