Completed
Pull Request — master (#1709)
by Andreas
16:45 queued 14:38
created

DocumentPersister::createDocument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 3
crap 3
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 && isset($data[$embeddedMetadata->identifier])
693 24
                    ? $data[$embeddedMetadata->identifier]
694 82
                    : null;
695
696 82
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
697 81
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
698
                }
699 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
700 10
                    $collection->set($key, $embeddedDocumentObject);
701
                } else {
702 82
                    $collection->add($embeddedDocumentObject);
703
                }
704
            }
705
        }
706 109
    }
707
708 60
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
709
    {
710 60
        $hints = $collection->getHints();
711 60
        $mapping = $collection->getMapping();
712 60
        $groupedIds = array();
713
714 60
        $sorted = isset($mapping['sort']) && $mapping['sort'];
715
716 60
        foreach ($collection->getMongoData() as $key => $reference) {
717 54
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
718 54
            $identifier = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
719 54
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
720
721
            // create a reference to the class and id
722 54
            $reference = $this->dm->getReference($className, $id);
723
724
            // no custom sort so add the references right now in the order they are embedded
725 54
            if ( ! $sorted) {
726 53
                if (CollectionHelper::isHash($mapping['strategy'])) {
727 2
                    $collection->set($key, $reference);
728
                } else {
729 51
                    $collection->add($reference);
730
                }
731
            }
732
733
            // only query for the referenced object if it is not already initialized or the collection is sorted
734 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...
735 54
                $groupedIds[$className][] = $identifier;
736
            }
737
        }
738 60
        foreach ($groupedIds as $className => $ids) {
739 39
            $class = $this->dm->getClassMetadata($className);
740 39
            $mongoCollection = $this->dm->getDocumentCollection($className);
741 39
            $criteria = $this->cm->merge(
742 39
                array('_id' => array('$in' => array_values($ids))),
743 39
                $this->dm->getFilterCollection()->getFilterCriteria($class),
744 39
                $mapping['criteria'] ?? array()
745
            );
746 39
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
747
748 39
            $options = [];
749 39
            if (isset($mapping['sort'])) {
750 39
                $options['sort'] = $this->prepareSort($mapping['sort']);
751
            }
752 39
            if (isset($mapping['limit'])) {
753
                $options['limit'] = $mapping['limit'];
754
            }
755 39
            if (isset($mapping['skip'])) {
756
                $options['skip'] = $mapping['skip'];
757
            }
758 39
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
759
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
760
            }
761
762 39
            $cursor = $mongoCollection->find($criteria, $options);
763 39
            $documents = $cursor->toArray();
764 39
            foreach ($documents as $documentData) {
765 38
                $document = $this->uow->getById($documentData['_id'], $class);
766 38
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
767 38
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
768 38
                    $this->uow->setOriginalDocumentData($document, $data);
769 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...
770
                }
771 38
                if ($sorted) {
772 39
                    $collection->add($document);
773
                }
774
            }
775
        }
776 60
    }
777
778 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
779
    {
780 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
781 17
        $documents = $query->execute()->toArray();
782 17
        foreach ($documents as $key => $document) {
783 16
            $collection->add($document);
784
        }
785 17
    }
786
787
    /**
788
     * @param PersistentCollectionInterface $collection
789
     *
790
     * @return Query
791
     */
792 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
793
    {
794 17
        $hints = $collection->getHints();
795 17
        $mapping = $collection->getMapping();
796 17
        $owner = $collection->getOwner();
797 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
798 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
799 17
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
800 17
        $mappedByFieldName = ClassMetadataInfo::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
801
802 17
        $criteria = $this->cm->merge(
803 17
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
804 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
805 17
            $mapping['criteria'] ?? array()
806
        );
807 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
808 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
809 17
            ->setQueryArray($criteria);
810
811 17
        if (isset($mapping['sort'])) {
812 17
            $qb->sort($mapping['sort']);
813
        }
814 17
        if (isset($mapping['limit'])) {
815 2
            $qb->limit($mapping['limit']);
816
        }
817 17
        if (isset($mapping['skip'])) {
818
            $qb->skip($mapping['skip']);
819
        }
820
821 17
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
822
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
823
        }
824
825 17
        foreach ($mapping['prime'] as $field) {
826 4
            $qb->field($field)->prime(true);
827
        }
828
829 17
        return $qb->getQuery();
830
    }
831
832 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
833
    {
834 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
835 5
        $mapping = $collection->getMapping();
836 5
        $documents = $cursor->toArray();
837 5
        foreach ($documents as $key => $obj) {
838 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
839 1
                $collection->set($key, $obj);
840
            } else {
841 5
                $collection->add($obj);
842
            }
843
        }
844 5
    }
845
846
    /**
847
     * @param PersistentCollectionInterface $collection
848
     *
849
     * @return \Iterator
850
     */
851 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
852
    {
853 5
        $mapping = $collection->getMapping();
854 5
        $repositoryMethod = $mapping['repositoryMethod'];
855 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
856 5
            ->$repositoryMethod($collection->getOwner());
857
858 5
        if ( ! $cursor instanceof Iterator) {
859
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object");
860
        }
861
862 5
        if (!empty($mapping['prime'])) {
863 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
864 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
865 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
866
867 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
868
        }
869
870 5
        return $cursor;
871
    }
872
873
    /**
874
     * Prepare a projection array by converting keys, which are PHP property
875
     * names, to MongoDB field names.
876
     *
877
     * @param array $fields
878
     * @return array
879
     */
880 14
    public function prepareProjection(array $fields)
881
    {
882 14
        $preparedFields = array();
883
884 14
        foreach ($fields as $key => $value) {
885 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
886
        }
887
888 14
        return $preparedFields;
889
    }
890
891
    /**
892
     * @param $sort
893
     * @return int
894
     */
895 25
    private function getSortDirection($sort)
896
    {
897 25
        switch (strtolower($sort)) {
898
            case 'desc':
899 15
                return -1;
900
901
            case 'asc':
902 13
                return 1;
903
        }
904
905 12
        return $sort;
906
    }
907
908
    /**
909
     * Prepare a sort specification array by converting keys to MongoDB field
910
     * names and changing direction strings to int.
911
     *
912
     * @param array $fields
913
     * @return array
914
     */
915 141
    public function prepareSort(array $fields)
916
    {
917 141
        $sortFields = [];
918
919 141
        foreach ($fields as $key => $value) {
920 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
921
        }
922
923 141
        return $sortFields;
924
    }
925
926
    /**
927
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
928
     *
929
     * @param string $fieldName
930
     * @return string
931
     */
932 433
    public function prepareFieldName($fieldName)
933
    {
934 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
935
936 433
        return $fieldNames[0][0];
937
    }
938
939
    /**
940
     * Adds discriminator criteria to an already-prepared query.
941
     *
942
     * This method should be used once for query criteria and not be used for
943
     * nested expressions. It should be called before
944
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
945
     *
946
     * @param array $preparedQuery
947
     * @return array
948
     */
949 498
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
950
    {
951
        /* If the class has a discriminator field, which is not already in the
952
         * criteria, inject it now. The field/values need no preparation.
953
         */
954 498
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
955 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
956 29
            if (count($discriminatorValues) === 1) {
957 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
958
            } else {
959 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
960
            }
961
        }
962
963 498
        return $preparedQuery;
964
    }
965
966
    /**
967
     * Adds filter criteria to an already-prepared query.
968
     *
969
     * This method should be used once for query criteria and not be used for
970
     * nested expressions. It should be called after
971
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
972
     *
973
     * @param array $preparedQuery
974
     * @return array
975
     */
976 499
    public function addFilterToPreparedQuery(array $preparedQuery)
977
    {
978
        /* If filter criteria exists for this class, prepare it and merge
979
         * over the existing query.
980
         *
981
         * @todo Consider recursive merging in case the filter criteria and
982
         * prepared query both contain top-level $and/$or operators.
983
         */
984 499
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
985 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
986
        }
987
988 499
        return $preparedQuery;
989
    }
990
991
    /**
992
     * Prepares the query criteria or new document object.
993
     *
994
     * PHP field names and types will be converted to those used by MongoDB.
995
     *
996
     * @param array $query
997
     * @param bool $isNewObj
998
     * @return array
999
     */
1000 531
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1001
    {
1002 531
        $preparedQuery = array();
1003
1004 531
        foreach ($query as $key => $value) {
1005
            // Recursively prepare logical query clauses
1006 489
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1007 20
                foreach ($value as $k2 => $v2) {
1008 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1009
                }
1010 20
                continue;
1011
            }
1012
1013 489
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1014 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1015 38
                continue;
1016
            }
1017
1018 489
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1019 489
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1020 489
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1021 133
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1022 489
                    : Type::convertPHPToDatabaseValue($preparedValue);
1023
            }
1024
        }
1025
1026 531
        return $preparedQuery;
1027
    }
1028
1029
    /**
1030
     * Prepares a query value and converts the PHP value to the database value
1031
     * if it is an identifier.
1032
     *
1033
     * It also handles converting $fieldName to the database name if they are different.
1034
     *
1035
     * @param string $fieldName
1036
     * @param mixed $value
1037
     * @param ClassMetadata $class        Defaults to $this->class
1038
     * @param bool $prepareValue Whether or not to prepare the value
1039
     * @param bool $inNewObj Whether or not newObj is being prepared
1040
     * @return array An array of tuples containing prepared field names and values
1041
     */
1042 882
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1043
    {
1044 882
        $class = $class ?? $this->class;
1045
1046
        // @todo Consider inlining calls to ClassMetadataInfo methods
1047
1048
        // Process all non-identifier fields by translating field names
1049 882
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1050 248
            $mapping = $class->fieldMappings[$fieldName];
1051 248
            $fieldName = $mapping['name'];
1052
1053 248
            if ( ! $prepareValue) {
1054 52
                return [[$fieldName, $value]];
1055
            }
1056
1057
            // Prepare mapped, embedded objects
1058 206
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1059 206
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1060 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1061
            }
1062
1063 204
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoDB\BSON\ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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