Completed
Pull Request — master (#1385)
by Andreas
11:49
created

DocumentPersister::guardMissingShardKey()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 9
cts 10
cp 0.9
rs 8.8571
cc 5
eloc 9
nc 3
nop 3
crap 5.025
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\Common\Persistence\Mapping\MappingException;
24
use Doctrine\MongoDB\CursorInterface;
25
use Doctrine\ODM\MongoDB\Cursor;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
28
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
29
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
30
use Doctrine\ODM\MongoDB\LockException;
31
use Doctrine\ODM\MongoDB\LockMode;
32
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
33
use Doctrine\ODM\MongoDB\MongoDBException;
34
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
35
use Doctrine\ODM\MongoDB\Proxy\Proxy;
36
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
37
use Doctrine\ODM\MongoDB\Query\Query;
38
use Doctrine\ODM\MongoDB\Types\Type;
39
use Doctrine\ODM\MongoDB\UnitOfWork;
40
41
/**
42
 * The DocumentPersister is responsible for persisting documents.
43
 *
44
 * @since       1.0
45
 */
46
class DocumentPersister
47
{
48
    /**
49
     * The PersistenceBuilder instance.
50
     *
51
     * @var PersistenceBuilder
52
     */
53
    private $pb;
54
55
    /**
56
     * The DocumentManager instance.
57
     *
58
     * @var DocumentManager
59
     */
60
    private $dm;
61
62
    /**
63
     * The EventManager instance
64
     *
65
     * @var EventManager
66
     */
67
    private $evm;
68
69
    /**
70
     * The UnitOfWork instance.
71
     *
72
     * @var UnitOfWork
73
     */
74
    private $uow;
75
76
    /**
77
     * The ClassMetadata instance for the document type being persisted.
78
     *
79
     * @var ClassMetadata
80
     */
81
    private $class;
82
83
    /**
84
     * The MongoCollection instance for this document.
85
     *
86
     * @var \MongoCollection
87
     */
88
    private $collection;
89
90
    /**
91
     * Array of queued inserts for the persister to insert.
92
     *
93
     * @var array
94
     */
95
    private $queuedInserts = array();
96
97
    /**
98
     * Array of queued inserts for the persister to insert.
99
     *
100
     * @var array
101
     */
102
    private $queuedUpserts = array();
103
104
    /**
105
     * The CriteriaMerger instance.
106
     *
107
     * @var CriteriaMerger
108
     */
109
    private $cm;
110
111
    /**
112
     * The CollectionPersister instance.
113
     *
114
     * @var CollectionPersister
115
     */
116
    private $cp;
117
118
    /**
119
     * Initializes this instance.
120
     *
121
     * @param PersistenceBuilder $pb
122
     * @param DocumentManager $dm
123
     * @param EventManager $evm
124
     * @param UnitOfWork $uow
125
     * @param HydratorFactory $hydratorFactory
126
     * @param ClassMetadata $class
127
     * @param CriteriaMerger $cm
128
     */
129 706
    public function __construct(
130
        PersistenceBuilder $pb,
131
        DocumentManager $dm,
132
        EventManager $evm,
133
        UnitOfWork $uow,
134
        HydratorFactory $hydratorFactory,
135
        ClassMetadata $class,
136
        CriteriaMerger $cm = null
137
    ) {
138 706
        $this->pb = $pb;
139 706
        $this->dm = $dm;
140 706
        $this->evm = $evm;
141 706
        $this->cm = $cm ?: new CriteriaMerger();
142 706
        $this->uow = $uow;
143 706
        $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...
144 706
        $this->class = $class;
145 706
        $this->collection = $dm->getDocumentCollection($class->name);
0 ignored issues
show
Documentation Bug introduced by
It seems like $dm->getDocumentCollection($class->name) of type object<Doctrine\MongoDB\Collection> is incompatible with the declared type object<MongoCollection> of property $collection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
146 706
        $this->cp = $this->uow->getCollectionPersister();
147 706
    }
148
149
    /**
150
     * @return array
151
     */
152
    public function getInserts()
153
    {
154
        return $this->queuedInserts;
155
    }
156
157
    /**
158
     * @param object $document
159
     * @return bool
160
     */
161
    public function isQueuedForInsert($document)
162
    {
163
        return isset($this->queuedInserts[spl_object_hash($document)]);
164
    }
165
166
    /**
167
     * Adds a document to the queued insertions.
168
     * The document remains queued until {@link executeInserts} is invoked.
169
     *
170
     * @param object $document The document to queue for insertion.
171
     */
172 503
    public function addInsert($document)
173
    {
174 503
        $this->queuedInserts[spl_object_hash($document)] = $document;
175 503
    }
176
177
    /**
178
     * @return array
179
     */
180
    public function getUpserts()
181
    {
182
        return $this->queuedUpserts;
183
    }
184
185
    /**
186
     * @param object $document
187
     * @return boolean
188
     */
189
    public function isQueuedForUpsert($document)
190
    {
191
        return isset($this->queuedUpserts[spl_object_hash($document)]);
192
    }
193
194
    /**
195
     * Adds a document to the queued upserts.
196
     * The document remains queued until {@link executeUpserts} is invoked.
197
     *
198
     * @param object $document The document to queue for insertion.
199
     */
200 77
    public function addUpsert($document)
201
    {
202 77
        $this->queuedUpserts[spl_object_hash($document)] = $document;
203 77
    }
204
205
    /**
206
     * Gets the ClassMetadata instance of the document class this persister is used for.
207
     *
208
     * @return ClassMetadata
209
     */
210
    public function getClassMetadata()
211
    {
212
        return $this->class;
213
    }
214
215
    /**
216
     * Executes all queued document insertions.
217
     *
218
     * Queued documents without an ID will inserted in a batch and queued
219
     * documents with an ID will be upserted individually.
220
     *
221
     * If no inserts are queued, invoking this method is a NOOP.
222
     *
223
     * @param array $options Options for batchInsert() and update() driver methods
224
     */
225 503
    public function executeInserts(array $options = array())
226
    {
227 503
        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...
228
            return;
229
        }
230
231 503
        $inserts = array();
232 503
        foreach ($this->queuedInserts as $oid => $document) {
233 503
            $data = $this->pb->prepareInsertData($document);
234
235
            // Set the initial version for each insert
236 502 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...
237 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
238 39
                if ($versionMapping['type'] === 'int') {
239 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
240 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
241 2
                } elseif ($versionMapping['type'] === 'date') {
242 2
                    $nextVersionDateTime = new \DateTime();
243 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
244 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
245
                }
246 39
                $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...
247
            }
248
249 502
            $inserts[$oid] = $data;
250
        }
251
252 502
        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...
253
            try {
254 502
                $this->collection->batchInsert($inserts, $options);
255 7
            } catch (\MongoException $e) {
256 7
                $this->queuedInserts = array();
257 7
                throw $e;
258
            }
259
        }
260
261
        /* All collections except for ones using addToSet have already been
262
         * saved. We have left these to be handled separately to avoid checking
263
         * collection for uniqueness on PHP side.
264
         */
265 502
        foreach ($this->queuedInserts as $document) {
266 502
            $this->handleCollections($document, $options);
267
        }
268
269 502
        $this->queuedInserts = array();
270 502
    }
271
272
    /**
273
     * Executes all queued document upserts.
274
     *
275
     * Queued documents with an ID are upserted individually.
276
     *
277
     * If no upserts are queued, invoking this method is a NOOP.
278
     *
279
     * @param array $options Options for batchInsert() and update() driver methods
280
     */
281 77
    public function executeUpserts(array $options = array())
282
    {
283 77
        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...
284
            return;
285
        }
286
287 77
        foreach ($this->queuedUpserts as $oid => $document) {
288
            try {
289 77
                $this->executeUpsert($document, $options);
290 77
                $this->handleCollections($document, $options);
291 77
                unset($this->queuedUpserts[$oid]);
292
            } catch (\MongoException $e) {
293
                unset($this->queuedUpserts[$oid]);
294 77
                throw $e;
295
            }
296
        }
297 77
    }
298
299
    /**
300
     * Executes a single upsert in {@link executeUpserts}
301
     *
302
     * @param object $document
303
     * @param array  $options
304
     */
305 77
    private function executeUpsert($document, array $options)
306
    {
307 77
        $options['upsert'] = true;
308 77
        $criteria = $this->getQueryForDocument($document);
309
310 77
        $data = $this->pb->prepareUpsertData($document);
311
312
        // Set the initial version for each upsert
313 77 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...
314 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
315 3
            if ($versionMapping['type'] === 'int') {
316 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
317 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
318 1
            } elseif ($versionMapping['type'] === 'date') {
319 1
                $nextVersionDateTime = new \DateTime();
320 1
                $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
321 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
322
            }
323 3
            $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...
324
        }
325
326 77
        foreach (array_keys($criteria) as $field) {
327 77
            unset($data['$set'][$field]);
328
        }
329
330
        // Do not send an empty $set modifier
331 77
        if (empty($data['$set'])) {
332 13
            unset($data['$set']);
333
        }
334
335
        /* If there are no modifiers remaining, we're upserting a document with
336
         * an identifier as its only field. Since a document with the identifier
337
         * may already exist, the desired behavior is "insert if not exists" and
338
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
339
         * the identifier to the same value in our criteria.
340
         *
341
         * This will fail for versions before MongoDB 2.6, which require an
342
         * empty $set modifier. The best we can do (without attempting to check
343
         * server versions in advance) is attempt the 2.6+ behavior and retry
344
         * after the relevant exception.
345
         *
346
         * See: https://jira.mongodb.org/browse/SERVER-12266
347
         */
348 77
        if (empty($data)) {
349 13
            $retry = true;
350 13
            $data = array('$set' => array('_id' => $criteria['_id']));
351
        }
352
353
        try {
354 77
            $this->collection->update($criteria, $data, $options);
355 65
            return;
356 13
        } catch (\MongoCursorException $e) {
357 13
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
358
                throw $e;
359
            }
360
        }
361
362 13
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
363 13
    }
364
365
    /**
366
     * Updates the already persisted document if it has any new changesets.
367
     *
368
     * @param object $document
369
     * @param array $options Array of options to be used with update()
370
     * @throws \Doctrine\ODM\MongoDB\LockException
371
     */
372 221
    public function update($document, array $options = array())
373
    {
374 221
        $update = $this->pb->prepareUpdateData($document);
375
376 221
        $query = $this->getQueryForDocument($document);
377
378 219
        foreach (array_keys($query) as $field) {
379 219
            unset($update['$set'][$field]);
380
        }
381
382 219
        if (empty($update['$set'])) {
383 91
            unset($update['$set']);
384
        }
385
386
387
        // Include versioning logic to set the new version value in the database
388
        // and to ensure the version has not changed since this document object instance
389
        // was fetched from the database
390 219
        if ($this->class->isVersioned) {
391 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
392 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
393 31
            if ($versionMapping['type'] === 'int') {
394 28
                $nextVersion = $currentVersion + 1;
395 28
                $update['$inc'][$versionMapping['name']] = 1;
396 28
                $query[$versionMapping['name']] = $currentVersion;
397 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
398 3
            } elseif ($versionMapping['type'] === 'date') {
399 3
                $nextVersion = new \DateTime();
400 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
401 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
402 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
403
            }
404
        }
405
406 219
        if ( ! empty($update)) {
407
            // Include locking logic so that if the document object in memory is currently
408
            // locked then it will remove it, otherwise it ensures the document is not locked.
409 153
            if ($this->class->isLockable) {
410 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
411 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
412 11
                if ($isLocked) {
413 2
                    $update['$unset'] = array($lockMapping['name'] => true);
414
                } else {
415 9
                    $query[$lockMapping['name']] = array('$exists' => false);
416
                }
417
            }
418
419 153
            $result = $this->collection->update($query, $update, $options);
420
421 153 View Code Duplication
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
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...
422 5
                throw LockException::lockFailed($document);
423
            }
424
        }
425
426 215
        $this->handleCollections($document, $options);
427 215
    }
428
429
    /**
430
     * Removes document from mongo
431
     *
432
     * @param mixed $document
433
     * @param array $options Array of options to be used with remove()
434
     * @throws \Doctrine\ODM\MongoDB\LockException
435
     */
436 29
    public function delete($document, array $options = array())
437
    {
438 29
        $query = $this->getQueryForDocument($document);
439
440 29
        if ($this->class->isLockable) {
441 2
            $query[$this->class->lockField] = array('$exists' => false);
442
        }
443
444 29
        $result = $this->collection->remove($query, $options);
445
446 29 View Code Duplication
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
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...
447 2
            throw LockException::lockFailed($document);
448
        }
449 27
    }
450
451
    /**
452
     * Refreshes a managed document.
453
     *
454
     * @param string $id
455
     * @param object $document The document to refresh.
456
     *
457
     * @deprecated The first argument is deprecated.
458
     */
459 21
    public function refresh($id, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
460
    {
461 21
        $query = $this->getQueryForDocument($document);
462 21
        $data = $this->collection->findOne($query);
463 21
        $data = $this->hydratorFactory->hydrate($document, $data);
464 21
        $this->uow->setOriginalDocumentData($document, $data);
465 21
    }
466
467
    /**
468
     * Finds a document by a set of criteria.
469
     *
470
     * If a scalar or MongoId is provided for $criteria, it will be used to
471
     * match an _id value.
472
     *
473
     * @param mixed   $criteria Query criteria
474
     * @param object  $document Document to load the data into. If not specified, a new document is created.
475
     * @param array   $hints    Hints for document creation
476
     * @param integer $lockMode
477
     * @param array   $sort     Sort array for Cursor::sort()
478
     * @throws \Doctrine\ODM\MongoDB\LockException
479
     * @return object|null The loaded and managed document instance or null if no document was found
480
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
481
     */
482 364
    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...
483
    {
484
        // TODO: remove this
485 364
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
486
            $criteria = array('_id' => $criteria);
487
        }
488
489 364
        $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...
490 364
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
491 364
        $criteria = $this->addFilterToPreparedQuery($criteria);
492
493 364
        $cursor = $this->collection->find($criteria);
494
495 364
        if (null !== $sort) {
496 102
            $cursor->sort($this->prepareSortOrProjection($sort));
497
        }
498
499 364
        $result = $cursor->getSingleResult();
500
501 364
        if ($this->class->isLockable) {
502 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
503 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
504 1
                throw LockException::lockFailed($result);
505
            }
506
        }
507
508 363
        return $this->createDocument($result, $document, $hints);
509
    }
510
511
    /**
512
     * Finds documents by a set of criteria.
513
     *
514
     * @param array        $criteria Query criteria
515
     * @param array        $sort     Sort array for Cursor::sort()
516
     * @param integer|null $limit    Limit for Cursor::limit()
517
     * @param integer|null $skip     Skip for Cursor::skip()
518
     * @return Cursor
519
     */
520 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
521
    {
522 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
523 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
524 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
525
526 22
        $baseCursor = $this->collection->find($criteria);
527 22
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

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...
528
529 22
        if (null !== $sort) {
530 3
            $cursor->sort($sort);
531
        }
532
533 22
        if (null !== $limit) {
534 2
            $cursor->limit($limit);
535
        }
536
537 22
        if (null !== $skip) {
538 2
            $cursor->skip($skip);
539
        }
540
541 22
        return $cursor;
542
    }
543
544
    /**
545
     * @param object $document
546
     *
547
     * @return array
548
     * @throws MongoDBException
549
     */
550 285
    private function getShardKeyQuery($document)
551
    {
552 285
        if ( ! $this->class->isSharded()) {
553 276
            return array();
554
        }
555
556 9
        $shardKey = $this->class->getShardKey();
557 9
        $keys = array_keys($shardKey['keys']);
558 9
        $data = $this->uow->getDocumentActualData($document);
559
560 9
        $shardKeyQueryPart = array();
561 9
        foreach ($keys as $key) {
562 9
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
563 9
            $this->guardMissingShardKey($document, $key, $data);
564 7
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
565 7
            $shardKeyQueryPart[$key] = $value;
566
        }
567
568 7
        return $shardKeyQueryPart;
569
    }
570
571
    /**
572
     * Wraps the supplied base cursor in the corresponding ODM class.
573
     *
574
     * @param CursorInterface $baseCursor
575
     * @return Cursor
576
     */
577 22
    private function wrapCursor(CursorInterface $baseCursor)
578
    {
579 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
580
    }
581
582
    /**
583
     * Checks whether the given managed document exists in the database.
584
     *
585
     * @param object $document
586
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
587
     */
588 3
    public function exists($document)
589
    {
590 3
        $id = $this->class->getIdentifierObject($document);
591 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
592
    }
593
594
    /**
595
     * Locks document by storing the lock mode on the mapped lock field.
596
     *
597
     * @param object $document
598
     * @param int $lockMode
599
     */
600 5
    public function lock($document, $lockMode)
601
    {
602 5
        $id = $this->uow->getDocumentIdentifier($document);
603 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
604 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
605 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
606 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
607 5
    }
608
609
    /**
610
     * Releases any lock that exists on this document.
611
     *
612
     * @param object $document
613
     */
614 1
    public function unlock($document)
615
    {
616 1
        $id = $this->uow->getDocumentIdentifier($document);
617 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
618 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
619 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
620 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
621 1
    }
622
623
    /**
624
     * Creates or fills a single document object from an query result.
625
     *
626
     * @param object $result The query result.
627
     * @param object $document The document object to fill, if any.
628
     * @param array $hints Hints for document creation.
629
     * @return object The filled and managed document object or NULL, if the query result is empty.
630
     */
631 363
    private function createDocument($result, $document = null, array $hints = array())
632
    {
633 363
        if ($result === null) {
634 116
            return null;
635
        }
636
637 310
        if ($document !== null) {
638 37
            $hints[Query::HINT_REFRESH] = true;
639 37
            $id = $this->class->getPHPIdentifierValue($result['_id']);
640 37
            $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...
641
        }
642
643 310
        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...
644
    }
645
646
    /**
647
     * Loads a PersistentCollection data. Used in the initialize() method.
648
     *
649
     * @param PersistentCollectionInterface $collection
650
     */
651 164
    public function loadCollection(PersistentCollectionInterface $collection)
652
    {
653 164
        $mapping = $collection->getMapping();
654 164
        switch ($mapping['association']) {
655 164
            case ClassMetadata::EMBED_MANY:
656 114
                $this->loadEmbedManyCollection($collection);
657 114
                break;
658
659 66
            case ClassMetadata::REFERENCE_MANY:
660 66
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
661 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
662
                } else {
663 63
                    if ($mapping['isOwningSide']) {
664 53
                        $this->loadReferenceManyCollectionOwningSide($collection);
665
                    } else {
666 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
667
                    }
668
                }
669 66
                break;
670
        }
671 164
    }
672
673 114
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
674
    {
675 114
        $embeddedDocuments = $collection->getMongoData();
676 114
        $mapping = $collection->getMapping();
677 114
        $owner = $collection->getOwner();
678 114
        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...
679 85
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
680 85
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
681 85
                $embeddedMetadata = $this->dm->getClassMetadata($className);
682 85
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
683
684 85
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
685
686 85
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
687 85
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
688 21
                    ? $data[$embeddedMetadata->identifier]
689 85
                    : null;
690
691 85
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
692 85
                if (CollectionHelper::isHash($mapping['strategy'])) {
693 25
                    $collection->set($key, $embeddedDocumentObject);
694
                } else {
695 85
                    $collection->add($embeddedDocumentObject);
696
                }
697
            }
698
        }
699 114
    }
700
701 53
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
702
    {
703 53
        $hints = $collection->getHints();
704 53
        $mapping = $collection->getMapping();
705 53
        $groupedIds = array();
706
707 53
        $sorted = isset($mapping['sort']) && $mapping['sort'];
708
709 53
        foreach ($collection->getMongoData() as $key => $reference) {
710 48
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
711 5
                $className = $mapping['targetDocument'];
712 5
                $mongoId = $reference;
713
            } else {
714 44
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
715 44
                $mongoId = $reference['$id'];
716
            }
717 48
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
718
719
            // create a reference to the class and id
720 48
            $reference = $this->dm->getReference($className, $id);
721
722
            // no custom sort so add the references right now in the order they are embedded
723 48
            if ( ! $sorted) {
724 47
                if (CollectionHelper::isHash($mapping['strategy'])) {
725 2
                    $collection->set($key, $reference);
726
                } else {
727 45
                    $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 48
            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 48
                $groupedIds[$className][] = $mongoId;
734
            }
735
        }
736 53
        foreach ($groupedIds as $className => $ids) {
737 35
            $class = $this->dm->getClassMetadata($className);
738 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
739 35
            $criteria = $this->cm->merge(
740 35
                array('_id' => array('$in' => array_values($ids))),
741 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
742 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
743
            );
744 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
745 35
            $cursor = $mongoCollection->find($criteria);
746 35
            if (isset($mapping['sort'])) {
747 35
                $cursor->sort($mapping['sort']);
748
            }
749 35
            if (isset($mapping['limit'])) {
750
                $cursor->limit($mapping['limit']);
751
            }
752 35
            if (isset($mapping['skip'])) {
753
                $cursor->skip($mapping['skip']);
754
            }
755 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
756
                $cursor->slaveOkay(true);
757
            }
758 35 View Code Duplication
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
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...
759
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
760
            }
761 35
            $documents = $cursor->toArray(false);
762 35
            foreach ($documents as $documentData) {
763 34
                $document = $this->uow->getById($documentData['_id'], $class);
764 34
                $data = $this->hydratorFactory->hydrate($document, $documentData);
765 34
                $this->uow->setOriginalDocumentData($document, $data);
766 34
                $document->__isInitialized__ = true;
767 34
                if ($sorted) {
768 35
                    $collection->add($document);
769
                }
770
            }
771
        }
772 53
    }
773
774 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
775
    {
776 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
777 14
        $documents = $query->execute()->toArray(false);
778 14
        foreach ($documents as $key => $document) {
779 13
            $collection->add($document);
780
        }
781 14
    }
782
783
    /**
784
     * @param PersistentCollectionInterface $collection
785
     *
786
     * @return Query
787
     */
788 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
789
    {
790 16
        $hints = $collection->getHints();
791 16
        $mapping = $collection->getMapping();
792 16
        $owner = $collection->getOwner();
793 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
794 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
795 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
796 16
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
797 16
        $criteria = $this->cm->merge(
798 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
799 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
800 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
801
        );
802 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
803 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
804 16
            ->setQueryArray($criteria);
805
806 16
        if (isset($mapping['sort'])) {
807 16
            $qb->sort($mapping['sort']);
808
        }
809 16
        if (isset($mapping['limit'])) {
810 1
            $qb->limit($mapping['limit']);
811
        }
812 16
        if (isset($mapping['skip'])) {
813
            $qb->skip($mapping['skip']);
814
        }
815 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
816
            $qb->slaveOkay(true);
817
        }
818 16 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
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...
819
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
820
        }
821
822 16
        return $qb->getQuery();
823
    }
824
825 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
826
    {
827 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
828 3
        $mapping = $collection->getMapping();
829 3
        $documents = $cursor->toArray(false);
0 ignored issues
show
Unused Code introduced by
The call to CursorInterface::toArray() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
830 3
        foreach ($documents as $key => $obj) {
831 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
832 1
                $collection->set($key, $obj);
833
            } else {
834 3
                $collection->add($obj);
835
            }
836
        }
837 3
    }
838
839
    /**
840
     * @param PersistentCollectionInterface $collection
841
     *
842
     * @return CursorInterface
843
     */
844 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
845
    {
846 3
        $hints = $collection->getHints();
847 3
        $mapping = $collection->getMapping();
848 3
        $repositoryMethod = $mapping['repositoryMethod'];
849 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
850 3
            ->$repositoryMethod($collection->getOwner());
851
852 3
        if ( ! $cursor instanceof CursorInterface) {
853
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
854
        }
855
856 3
        if (isset($mapping['sort'])) {
857 3
            $cursor->sort($mapping['sort']);
858
        }
859 3
        if (isset($mapping['limit'])) {
860
            $cursor->limit($mapping['limit']);
861
        }
862 3
        if (isset($mapping['skip'])) {
863
            $cursor->skip($mapping['skip']);
864
        }
865 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
866
            $cursor->slaveOkay(true);
867
        }
868 3 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
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...
869
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
870
        }
871
872 3
        return $cursor;
873
    }
874
875
    /**
876
     * Prepare a sort or projection array by converting keys, which are PHP
877
     * property names, to MongoDB field names.
878
     *
879
     * @param array $fields
880
     * @return array
881
     */
882 139
    public function prepareSortOrProjection(array $fields)
883
    {
884 139
        $preparedFields = array();
885
886 139
        foreach ($fields as $key => $value) {
887 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
888
        }
889
890 139
        return $preparedFields;
891
    }
892
893
    /**
894
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
895
     *
896
     * @param string $fieldName
897
     * @return string
898
     */
899 85
    public function prepareFieldName($fieldName)
900
    {
901 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
902
903 85
        return $fieldName;
904
    }
905
906
    /**
907
     * Adds discriminator criteria to an already-prepared query.
908
     *
909
     * This method should be used once for query criteria and not be used for
910
     * nested expressions. It should be called before
911
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
912
     *
913
     * @param array $preparedQuery
914
     * @return array
915
     */
916 492
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
917
    {
918
        /* If the class has a discriminator field, which is not already in the
919
         * criteria, inject it now. The field/values need no preparation.
920
         */
921 492
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
922 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
923 21
            if (count($discriminatorValues) === 1) {
924 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
925
            } else {
926 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
927
            }
928
        }
929
930 492
        return $preparedQuery;
931
    }
932
933
    /**
934
     * Adds filter criteria to an already-prepared query.
935
     *
936
     * This method should be used once for query criteria and not be used for
937
     * nested expressions. It should be called after
938
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
939
     *
940
     * @param array $preparedQuery
941
     * @return array
942
     */
943 493
    public function addFilterToPreparedQuery(array $preparedQuery)
944
    {
945
        /* If filter criteria exists for this class, prepare it and merge
946
         * over the existing query.
947
         *
948
         * @todo Consider recursive merging in case the filter criteria and
949
         * prepared query both contain top-level $and/$or operators.
950
         */
951 493
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
952 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
953
        }
954
955 493
        return $preparedQuery;
956
    }
957
958
    /**
959
     * Prepares the query criteria or new document object.
960
     *
961
     * PHP field names and types will be converted to those used by MongoDB.
962
     *
963
     * @param array $query
964
     * @return array
965
     */
966 526
    public function prepareQueryOrNewObj(array $query)
967
    {
968 526
        $preparedQuery = array();
969
970 526
        foreach ($query as $key => $value) {
971
            // Recursively prepare logical query clauses
972 488
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
973 20
                foreach ($value as $k2 => $v2) {
974 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
975
                }
976 20
                continue;
977
            }
978
979 488
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
980 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
981 20
                continue;
982
            }
983
984 488
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
985
986 488
            $preparedQuery[$key] = is_array($value)
987 122
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
988 488
                : Type::convertPHPToDatabaseValue($value);
989
        }
990
991 526
        return $preparedQuery;
992
    }
993
994
    /**
995
     * Prepares a query value and converts the PHP value to the database value
996
     * if it is an identifier.
997
     *
998
     * It also handles converting $fieldName to the database name if they are different.
999
     *
1000
     * @param string $fieldName
1001
     * @param mixed $value
1002
     * @param ClassMetadata $class        Defaults to $this->class
1003
     * @param boolean $prepareValue Whether or not to prepare the value
1004
     * @return array        Prepared field name and value
1005
     */
1006 519
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
1007
    {
1008 519
        $class = isset($class) ? $class : $this->class;
1009
1010
        // @todo Consider inlining calls to ClassMetadataInfo methods
1011
1012
        // Process all non-identifier fields by translating field names
1013 519
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1014 240
            $mapping = $class->fieldMappings[$fieldName];
1015 240
            $fieldName = $mapping['name'];
1016
1017 240
            if ( ! $prepareValue) {
1018 62
                return array($fieldName, $value);
1019
            }
1020
1021
            // Prepare mapped, embedded objects
1022 198
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1023 198
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1024 3
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
1025
            }
1026
1027 196
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1028
                try {
1029 5
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
1030 1
                } catch (MappingException $e) {
1031
                    // do nothing in case passed object is not mapped document
1032
                }
1033
            }
1034
1035
            // No further preparation unless we're dealing with a simple reference
1036
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1037 192
            $arrayValue = (array) $value;
1038 192
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1039 117
                return array($fieldName, $value);
1040
            }
1041
1042
            // Additional preparation for one or more simple reference values
1043 103
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1044
1045 103
            if ( ! is_array($value)) {
1046 99
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1047
            }
1048
1049
            // Objects without operators or with DBRef fields can be converted immediately
1050 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...
1051 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1052
            }
1053
1054 6
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1055
        }
1056
1057
        // Process identifier fields
1058 391
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1059 323
            $fieldName = '_id';
1060
1061 323
            if ( ! $prepareValue) {
1062 16
                return array($fieldName, $value);
1063
            }
1064
1065 309
            if ( ! is_array($value)) {
1066 287
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1067
            }
1068
1069
            // Objects without operators or with DBRef fields can be converted immediately
1070 55 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...
1071 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1072
            }
1073
1074 50
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1075
        }
1076
1077
        // No processing for unmapped, non-identifier, non-dotted field names
1078 101
        if (strpos($fieldName, '.') === false) {
1079 44
            return array($fieldName, $value);
1080
        }
1081
1082
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1083
         *
1084
         * We can limit parsing here, since at most three segments are
1085
         * significant: "fieldName.objectProperty" with an optional index or key
1086
         * for collections stored as either BSON arrays or objects.
1087
         */
1088 63
        $e = explode('.', $fieldName, 4);
1089
1090
        // No further processing for unmapped fields
1091 63
        if ( ! isset($class->fieldMappings[$e[0]])) {
1092 4
            return array($fieldName, $value);
1093
        }
1094
1095 60
        $mapping = $class->fieldMappings[$e[0]];
1096 60
        $e[0] = $mapping['name'];
1097
1098
        // Hash and raw fields will not be prepared beyond the field name
1099 60
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1100 1
            $fieldName = implode('.', $e);
1101
1102 1
            return array($fieldName, $value);
1103
        }
1104
1105 59
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1106 59
                && isset($e[2])) {
1107 1
            $objectProperty = $e[2];
1108 1
            $objectPropertyPrefix = $e[1] . '.';
1109 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1110 58
        } elseif ($e[1] != '$') {
1111 57
            $fieldName = $e[0] . '.' . $e[1];
1112 57
            $objectProperty = $e[1];
1113 57
            $objectPropertyPrefix = '';
1114 57
            $nextObjectProperty = implode('.', array_slice($e, 2));
1115 1
        } elseif (isset($e[2])) {
1116 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1117 1
            $objectProperty = $e[2];
1118 1
            $objectPropertyPrefix = $e[1] . '.';
1119 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1120
        } else {
1121 1
            $fieldName = $e[0] . '.' . $e[1];
1122
1123 1
            return array($fieldName, $value);
1124
        }
1125
1126
        // No further processing for fields without a targetDocument mapping
1127 59
        if ( ! isset($mapping['targetDocument'])) {
1128 3
            if ($nextObjectProperty) {
1129
                $fieldName .= '.'.$nextObjectProperty;
1130
            }
1131
1132 3
            return array($fieldName, $value);
1133
        }
1134
1135 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1136
1137
        // No further processing for unmapped targetDocument fields
1138 56
        if ( ! $targetClass->hasField($objectProperty)) {
1139 24
            if ($nextObjectProperty) {
1140
                $fieldName .= '.'.$nextObjectProperty;
1141
            }
1142
1143 24
            return array($fieldName, $value);
1144
        }
1145
1146 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1147 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1148
1149
        // Prepare DBRef identifiers or the mapped field's property path
1150 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1151 13
            ? $e[0] . '.$id'
1152 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1153
1154
        // Process targetDocument identifier fields
1155 35
        if ($objectPropertyIsId) {
1156 14
            if ( ! $prepareValue) {
1157 1
                return array($fieldName, $value);
1158
            }
1159
1160 13
            if ( ! is_array($value)) {
1161 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1162
            }
1163
1164
            // Objects without operators or with DBRef fields can be converted immediately
1165 12 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...
1166 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1167
            }
1168
1169 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1170
        }
1171
1172
        /* The property path may include a third field segment, excluding the
1173
         * collection item pointer. If present, this next object property must
1174
         * be processed recursively.
1175
         */
1176 21
        if ($nextObjectProperty) {
1177
            // Respect the targetDocument's class metadata when recursing
1178 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1179 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1180 14
                : null;
1181
1182 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1183
1184 14
            $fieldName .= '.' . $key;
1185
        }
1186
1187 21
        return array($fieldName, $value);
1188
    }
1189
1190
    /**
1191
     * Prepares a query expression.
1192
     *
1193
     * @param array|object  $expression
1194
     * @param ClassMetadata $class
1195
     * @return array
1196
     */
1197 68
    private function prepareQueryExpression($expression, $class)
1198
    {
1199 68
        foreach ($expression as $k => $v) {
1200
            // Ignore query operators whose arguments need no type conversion
1201 68
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1202 12
                continue;
1203
            }
1204
1205
            // Process query operators whose argument arrays need type conversion
1206 68
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1207 66
                foreach ($v as $k2 => $v2) {
1208 66
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1209
                }
1210 66
                continue;
1211
            }
1212
1213
            // Recursively process expressions within a $not operator
1214 14
            if ($k === '$not' && is_array($v)) {
1215 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1216 11
                continue;
1217
            }
1218
1219 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1220
        }
1221
1222 68
        return $expression;
1223
    }
1224
1225
    /**
1226
     * Checks whether the value has DBRef fields.
1227
     *
1228
     * This method doesn't check if the the value is a complete DBRef object,
1229
     * although it should return true for a DBRef. Rather, we're checking that
1230
     * the value has one or more fields for a DBref. In practice, this could be
1231
     * $elemMatch criteria for matching a DBRef.
1232
     *
1233
     * @param mixed $value
1234
     * @return boolean
1235
     */
1236 69
    private function hasDBRefFields($value)
1237
    {
1238 69
        if ( ! is_array($value) && ! is_object($value)) {
1239
            return false;
1240
        }
1241
1242 69
        if (is_object($value)) {
1243
            $value = get_object_vars($value);
1244
        }
1245
1246 69
        foreach ($value as $key => $_) {
1247 69
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1248 69
                return true;
1249
            }
1250
        }
1251
1252 68
        return false;
1253
    }
1254
1255
    /**
1256
     * Checks whether the value has query operators.
1257
     *
1258
     * @param mixed $value
1259
     * @return boolean
1260
     */
1261 73
    private function hasQueryOperators($value)
1262
    {
1263 73
        if ( ! is_array($value) && ! is_object($value)) {
1264
            return false;
1265
        }
1266
1267 73
        if (is_object($value)) {
1268
            $value = get_object_vars($value);
1269
        }
1270
1271 73
        foreach ($value as $key => $_) {
1272 73
            if (isset($key[0]) && $key[0] === '$') {
1273 73
                return true;
1274
            }
1275
        }
1276
1277 9
        return false;
1278
    }
1279
1280
    /**
1281
     * Gets the array of discriminator values for the given ClassMetadata
1282
     *
1283
     * @param ClassMetadata $metadata
1284
     * @return array
1285
     */
1286 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1287
    {
1288 21
        $discriminatorValues = array($metadata->discriminatorValue);
1289 21
        foreach ($metadata->subClasses as $className) {
1290 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1291 8
                $discriminatorValues[] = $key;
1292
            }
1293
        }
1294
1295
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1296 21 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...
1297 2
            $discriminatorValues[] = null;
1298
        }
1299
1300 21
        return $discriminatorValues;
1301
    }
1302
1303 567
    private function handleCollections($document, $options)
1304
    {
1305
        // Collection deletions (deletions of complete collections)
1306 567
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1307 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1308 103
                $this->cp->delete($coll, $options);
1309
            }
1310
        }
1311
        // Collection updates (deleteRows, updateRows, insertRows)
1312 567
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1313 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1314 103
                $this->cp->update($coll, $options);
1315
            }
1316
        }
1317
        // Take new snapshots from visited collections
1318 567
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1319 239
            $coll->takeSnapshot();
1320
        }
1321 567
    }
1322
1323
    /**
1324
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1325
     * Also, shard key field should be present in actual document data.
1326
     *
1327
     * @param object $document
1328
     * @param string $shardKeyField
1329
     * @param array  $actualDocumentData
1330
     *
1331
     * @throws MongoDBException
1332
     */
1333 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1334
    {
1335 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1336 9
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1337
1338 9
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1339 9
        $fieldName = $fieldMapping['fieldName'];
1340
1341 9
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1342 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1343
        }
1344
1345 7
        if (!isset($actualDocumentData[$fieldName])) {
1346
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1347
        }
1348 7
    }
1349
1350
    /**
1351
     * Get shard key aware query for single document.
1352
     *
1353
     * @param object $document
1354
     *
1355
     * @return array
1356
     */
1357 282
    private function getQueryForDocument($document)
1358
    {
1359 282
        $id = $this->uow->getDocumentIdentifier($document);
1360 282
        $id = $this->class->getDatabaseIdentifierValue($id);
1361
1362 282
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1363 280
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1364
1365 280
        return $query;
1366
    }
1367
}
1368