Completed
Pull Request — master (#1640)
by Olivier
23:26 queued 21:41
created

DocumentPersister::prepareQueryElement()   F

Complexity

Conditions 48
Paths 314

Size

Total Lines 187
Code Lines 92

Duplication

Lines 9
Ratio 4.81 %

Code Coverage

Tests 89
CRAP Score 48.1831

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 9
loc 187
ccs 89
cts 93
cp 0.957
rs 3.3333
cc 48
eloc 92
nc 314
nop 5
crap 48.1831

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\MongoDB\EagerCursor;
26
use Doctrine\ODM\MongoDB\Cursor;
27
use Doctrine\ODM\MongoDB\DocumentManager;
28
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
29
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
30
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
31
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
32
use Doctrine\ODM\MongoDB\LockException;
33
use Doctrine\ODM\MongoDB\LockMode;
34
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
35
use Doctrine\ODM\MongoDB\MongoDBException;
36
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
37
use Doctrine\ODM\MongoDB\Proxy\Proxy;
38
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
39
use Doctrine\ODM\MongoDB\Query\Query;
40
use Doctrine\ODM\MongoDB\Types\Type;
41
use Doctrine\ODM\MongoDB\UnitOfWork;
42
43
/**
44
 * The DocumentPersister is responsible for persisting documents.
45
 *
46
 * @since       1.0
47
 */
48
class DocumentPersister
49
{
50
    /**
51
     * The PersistenceBuilder instance.
52
     *
53
     * @var PersistenceBuilder
54
     */
55
    private $pb;
56
57
    /**
58
     * The DocumentManager instance.
59
     *
60
     * @var DocumentManager
61
     */
62
    private $dm;
63
64
    /**
65
     * The EventManager instance
66
     *
67
     * @var EventManager
68
     */
69
    private $evm;
70
71
    /**
72
     * The UnitOfWork instance.
73
     *
74
     * @var UnitOfWork
75
     */
76
    private $uow;
77
78
    /**
79
     * The ClassMetadata instance for the document type being persisted.
80
     *
81
     * @var ClassMetadata
82
     */
83
    private $class;
84
85
    /**
86
     * The MongoCollection instance for this document.
87
     *
88
     * @var \MongoCollection
89
     */
90
    private $collection;
91
92
    /**
93
     * Array of queued inserts for the persister to insert.
94
     *
95
     * @var array
96
     */
97
    private $queuedInserts = array();
98
99
    /**
100
     * Array of queued inserts for the persister to insert.
101
     *
102
     * @var array
103
     */
104
    private $queuedUpserts = array();
105
106
    /**
107
     * The CriteriaMerger instance.
108
     *
109
     * @var CriteriaMerger
110
     */
111
    private $cm;
112
113
    /**
114
     * The CollectionPersister instance.
115
     *
116
     * @var CollectionPersister
117
     */
118
    private $cp;
119
120
    /**
121
     * Initializes this instance.
122
     *
123
     * @param PersistenceBuilder $pb
124
     * @param DocumentManager $dm
125
     * @param EventManager $evm
126
     * @param UnitOfWork $uow
127
     * @param HydratorFactory $hydratorFactory
128
     * @param ClassMetadata $class
129
     * @param CriteriaMerger $cm
130
     */
131 766
    public function __construct(
132
        PersistenceBuilder $pb,
133
        DocumentManager $dm,
134
        EventManager $evm,
135
        UnitOfWork $uow,
136
        HydratorFactory $hydratorFactory,
137
        ClassMetadata $class,
138
        CriteriaMerger $cm = null
139
    ) {
140 766
        $this->pb = $pb;
141 766
        $this->dm = $dm;
142 766
        $this->evm = $evm;
143 766
        $this->cm = $cm ?: new CriteriaMerger();
144 766
        $this->uow = $uow;
145 766
        $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...
146 766
        $this->class = $class;
147 766
        $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...
148 766
        $this->cp = $this->uow->getCollectionPersister();
149 766
    }
150
151
    /**
152
     * @return array
153
     */
154
    public function getInserts()
155
    {
156
        return $this->queuedInserts;
157
    }
158
159
    /**
160
     * @param object $document
161
     * @return bool
162
     */
163
    public function isQueuedForInsert($document)
164
    {
165
        return isset($this->queuedInserts[spl_object_hash($document)]);
166
    }
167
168
    /**
169
     * Adds a document to the queued insertions.
170
     * The document remains queued until {@link executeInserts} is invoked.
171
     *
172
     * @param object $document The document to queue for insertion.
173
     */
174 536
    public function addInsert($document)
175
    {
176 536
        $this->queuedInserts[spl_object_hash($document)] = $document;
177 536
    }
178
179
    /**
180
     * @return array
181
     */
182
    public function getUpserts()
183
    {
184
        return $this->queuedUpserts;
185
    }
186
187
    /**
188
     * @param object $document
189
     * @return boolean
190
     */
191
    public function isQueuedForUpsert($document)
192
    {
193
        return isset($this->queuedUpserts[spl_object_hash($document)]);
194
    }
195
196
    /**
197
     * Adds a document to the queued upserts.
198
     * The document remains queued until {@link executeUpserts} is invoked.
199
     *
200
     * @param object $document The document to queue for insertion.
201
     */
202 87
    public function addUpsert($document)
203
    {
204 87
        $this->queuedUpserts[spl_object_hash($document)] = $document;
205 87
    }
206
207
    /**
208
     * Gets the ClassMetadata instance of the document class this persister is used for.
209
     *
210
     * @return ClassMetadata
211
     */
212
    public function getClassMetadata()
213
    {
214
        return $this->class;
215
    }
216
217
    /**
218
     * Executes all queued document insertions.
219
     *
220
     * Queued documents without an ID will inserted in a batch and queued
221
     * documents with an ID will be upserted individually.
222
     *
223
     * If no inserts are queued, invoking this method is a NOOP.
224
     *
225
     * @param array $options Options for batchInsert() and update() driver methods
226
     */
227 536
    public function executeInserts(array $options = array())
228
    {
229 536
        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...
230
            return;
231
        }
232
233 536
        $inserts = array();
234 536
        $options = $this->getWriteOptions($options);
235 536
        foreach ($this->queuedInserts as $oid => $document) {
236 536
            $data = $this->pb->prepareInsertData($document);
237
238
            // Set the initial version for each insert
239 535 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...
240 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
241 39
                if ($versionMapping['type'] === 'int') {
242 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
243 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
244 39
                } elseif ($versionMapping['type'] === 'date') {
245 2
                    $nextVersionDateTime = new \DateTime();
246 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
247 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
248 2
                }
249 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...
250 39
            }
251
252 535
            $inserts[$oid] = $data;
253 535
        }
254
255 535
        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...
256
            try {
257 535
                $this->collection->batchInsert($inserts, $options);
258 535
            } catch (\MongoException $e) {
259 7
                $this->queuedInserts = array();
260 7
                throw $e;
261
            }
262 535
        }
263
264
        /* All collections except for ones using addToSet have already been
265
         * saved. We have left these to be handled separately to avoid checking
266
         * collection for uniqueness on PHP side.
267
         */
268 535
        foreach ($this->queuedInserts as $document) {
269 535
            $this->handleCollections($document, $options);
270 535
        }
271
272 535
        $this->queuedInserts = array();
273 535
    }
274
275
    /**
276
     * Executes all queued document upserts.
277
     *
278
     * Queued documents with an ID are upserted individually.
279
     *
280
     * If no upserts are queued, invoking this method is a NOOP.
281
     *
282
     * @param array $options Options for batchInsert() and update() driver methods
283
     */
284 87
    public function executeUpserts(array $options = array())
285
    {
286 87
        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...
287
            return;
288
        }
289
290 87
        $options = $this->getWriteOptions($options);
291 87
        foreach ($this->queuedUpserts as $oid => $document) {
292
            try {
293 87
                $this->executeUpsert($document, $options);
294 87
                $this->handleCollections($document, $options);
295 87
                unset($this->queuedUpserts[$oid]);
296 87
            } catch (\MongoException $e) {
297
                unset($this->queuedUpserts[$oid]);
298
                throw $e;
299
            }
300 87
        }
301 87
    }
302
303
    /**
304
     * Executes a single upsert in {@link executeUpserts}
305
     *
306
     * @param object $document
307
     * @param array  $options
308
     */
309 87
    private function executeUpsert($document, array $options)
310
    {
311 87
        $options['upsert'] = true;
312 87
        $criteria = $this->getQueryForDocument($document);
313
314 87
        $data = $this->pb->prepareUpsertData($document);
315
316
        // Set the initial version for each upsert
317 87 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...
318 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
319 3
            if ($versionMapping['type'] === 'int') {
320 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
321 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
322 3
            } elseif ($versionMapping['type'] === 'date') {
323 1
                $nextVersionDateTime = new \DateTime();
324 1
                $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
325 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
326 1
            }
327 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...
328 3
        }
329
330 87
        foreach (array_keys($criteria) as $field) {
331 87
            unset($data['$set'][$field]);
332 87
        }
333
334
        // Do not send an empty $set modifier
335 87
        if (empty($data['$set'])) {
336 17
            unset($data['$set']);
337 17
        }
338
339
        /* If there are no modifiers remaining, we're upserting a document with
340
         * an identifier as its only field. Since a document with the identifier
341
         * may already exist, the desired behavior is "insert if not exists" and
342
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
343
         * the identifier to the same value in our criteria.
344
         *
345
         * This will fail for versions before MongoDB 2.6, which require an
346
         * empty $set modifier. The best we can do (without attempting to check
347
         * server versions in advance) is attempt the 2.6+ behavior and retry
348
         * after the relevant exception.
349
         *
350
         * See: https://jira.mongodb.org/browse/SERVER-12266
351
         */
352 87
        if (empty($data)) {
353 17
            $retry = true;
354 17
            $data = array('$set' => array('_id' => $criteria['_id']));
355 17
        }
356
357
        try {
358 87
            $this->collection->update($criteria, $data, $options);
359 87
            return;
360
        } catch (\MongoCursorException $e) {
361
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
362
                throw $e;
363
            }
364
        }
365
366
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
367
    }
368
369
    /**
370
     * Updates the already persisted document if it has any new changesets.
371
     *
372
     * @param object $document
373
     * @param array $options Array of options to be used with update()
374
     * @throws \Doctrine\ODM\MongoDB\LockException
375
     */
376 227
    public function update($document, array $options = array())
377
    {
378 227
        $update = $this->pb->prepareUpdateData($document);
379
380 227
        $query = $this->getQueryForDocument($document);
381
382 225
        foreach (array_keys($query) as $field) {
383 225
            unset($update['$set'][$field]);
384 225
        }
385
386 225
        if (empty($update['$set'])) {
387 94
            unset($update['$set']);
388 94
        }
389
390
391
        // Include versioning logic to set the new version value in the database
392
        // and to ensure the version has not changed since this document object instance
393
        // was fetched from the database
394 225
        $nextVersion = null;
395 225
        if ($this->class->isVersioned) {
396 33
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
397 33
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
398 33
            if ($versionMapping['type'] === 'int') {
399 30
                $nextVersion = $currentVersion + 1;
400 30
                $update['$inc'][$versionMapping['name']] = 1;
401 30
                $query[$versionMapping['name']] = $currentVersion;
402 33
            } elseif ($versionMapping['type'] === 'date') {
403 3
                $nextVersion = new \DateTime();
404 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
405 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
406 3
            }
407 33
        }
408
409 225
        if ( ! empty($update)) {
410
            // Include locking logic so that if the document object in memory is currently
411
            // locked then it will remove it, otherwise it ensures the document is not locked.
412 157
            if ($this->class->isLockable) {
413 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
414 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
415 11
                if ($isLocked) {
416 2
                    $update['$unset'] = array($lockMapping['name'] => true);
417 2
                } else {
418 9
                    $query[$lockMapping['name']] = array('$exists' => false);
419
                }
420 11
            }
421
422 157
            $options = $this->getWriteOptions($options);
423
424 157
            $result = $this->collection->update($query, $update, $options);
425
426 157
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
427 6
                throw LockException::lockFailed($document);
428 152
            } elseif ($this->class->isVersioned) {
429 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
430 28
            }
431 152
        }
432
433 220
        $this->handleCollections($document, $options);
434 220
    }
435
436
    /**
437
     * Removes document from mongo
438
     *
439
     * @param mixed $document
440
     * @param array $options Array of options to be used with remove()
441
     * @throws \Doctrine\ODM\MongoDB\LockException
442
     */
443 34
    public function delete($document, array $options = array())
444
    {
445 34
        $query = $this->getQueryForDocument($document);
446
447 34
        if ($this->class->isLockable) {
448 2
            $query[$this->class->lockField] = array('$exists' => false);
449 2
        }
450
451 34
        $options = $this->getWriteOptions($options);
452
453 34
        $result = $this->collection->remove($query, $options);
454
455 34
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
456 2
            throw LockException::lockFailed($document);
457
        }
458 32
    }
459
460
    /**
461
     * Refreshes a managed document.
462
     *
463
     * @param string $id
464
     * @param object $document The document to refresh.
465
     *
466
     * @deprecated The first argument is deprecated.
467
     */
468 23
    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...
469 1
    {
470 23
        $query = $this->getQueryForDocument($document);
471 22
        $data = $this->collection->findOne($query);
472 22
        $data = $this->hydratorFactory->hydrate($document, $data);
473 22
        $this->uow->setOriginalDocumentData($document, $data);
474 22
    }
475
476
    /**
477
     * Finds a document by a set of criteria.
478
     *
479
     * If a scalar or MongoId is provided for $criteria, it will be used to
480
     * match an _id value.
481
     *
482
     * @param mixed   $criteria Query criteria
483
     * @param object  $document Document to load the data into. If not specified, a new document is created.
484
     * @param array   $hints    Hints for document creation
485
     * @param integer $lockMode
486
     * @param array   $sort     Sort array for Cursor::sort()
487
     * @throws \Doctrine\ODM\MongoDB\LockException
488
     * @return object|null The loaded and managed document instance or null if no document was found
489
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
490
     */
491 379
    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...
492
    {
493
        // TODO: remove this
494 379
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
495
            $criteria = array('_id' => $criteria);
496
        }
497
498 379
        $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...
499 379
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
500 379
        $criteria = $this->addFilterToPreparedQuery($criteria);
501
502 379
        $cursor = $this->collection->find($criteria);
503
504 379
        if (null !== $sort) {
505 105
            $cursor->sort($this->prepareSortOrProjection($sort));
506 105
        }
507
508 379
        $result = $cursor->getSingleResult();
509
510 379
        if ($this->class->isLockable) {
511 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
512 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
513 1
                throw LockException::lockFailed($result);
514
            }
515
        }
516
517 378
        return $this->createDocument($result, $document, $hints);
518
    }
519
520
    /**
521
     * Finds documents by a set of criteria.
522
     *
523
     * @param array        $criteria Query criteria
524
     * @param array        $sort     Sort array for Cursor::sort()
525
     * @param integer|null $limit    Limit for Cursor::limit()
526
     * @param integer|null $skip     Skip for Cursor::skip()
527
     * @return Cursor
528
     */
529 26
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
530
    {
531 26
        $criteria = $this->prepareQueryOrNewObj($criteria);
532 26
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
533 26
        $criteria = $this->addFilterToPreparedQuery($criteria);
534
535 26
        $baseCursor = $this->collection->find($criteria);
536 26
        $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...
537
538 26
        if (null !== $sort) {
539 3
            $cursor->sort($sort);
540 3
        }
541
542 26
        if (null !== $limit) {
543 2
            $cursor->limit($limit);
544 2
        }
545
546 26
        if (null !== $skip) {
547 2
            $cursor->skip($skip);
548 2
        }
549
550 26
        return $cursor;
551
    }
552
553
    /**
554
     * @param object $document
555
     *
556
     * @return array
557
     * @throws MongoDBException
558
     */
559 305
    private function getShardKeyQuery($document)
560
    {
561 305
        if ( ! $this->class->isSharded()) {
562 296
            return array();
563
        }
564
565 9
        $shardKey = $this->class->getShardKey();
566 9
        $keys = array_keys($shardKey['keys']);
567 9
        $data = $this->uow->getDocumentActualData($document);
568
569 9
        $shardKeyQueryPart = array();
570 9
        foreach ($keys as $key) {
571 9
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
572 9
            $this->guardMissingShardKey($document, $key, $data);
573 7
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
574 7
            $shardKeyQueryPart[$key] = $value;
575 7
        }
576
577 7
        return $shardKeyQueryPart;
578
    }
579
580
    /**
581
     * Wraps the supplied base cursor in the corresponding ODM class.
582
     *
583
     * @param CursorInterface $baseCursor
584
     * @return Cursor
585
     */
586 26
    private function wrapCursor(CursorInterface $baseCursor)
587
    {
588 26
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
589
    }
590
591
    /**
592
     * Checks whether the given managed document exists in the database.
593
     *
594
     * @param object $document
595
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
596
     */
597 3
    public function exists($document)
598
    {
599 3
        $id = $this->class->getIdentifierObject($document);
600 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
601
    }
602
603
    /**
604
     * Locks document by storing the lock mode on the mapped lock field.
605
     *
606
     * @param object $document
607
     * @param int $lockMode
608
     */
609 5
    public function lock($document, $lockMode)
610
    {
611 5
        $id = $this->uow->getDocumentIdentifier($document);
612 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
613 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
614 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
615 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
616 5
    }
617
618
    /**
619
     * Releases any lock that exists on this document.
620
     *
621
     * @param object $document
622
     */
623 1
    public function unlock($document)
624
    {
625 1
        $id = $this->uow->getDocumentIdentifier($document);
626 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
627 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
628 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
629 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
630 1
    }
631
632
    /**
633
     * Creates or fills a single document object from an query result.
634
     *
635
     * @param object $result The query result.
636
     * @param object $document The document object to fill, if any.
637
     * @param array $hints Hints for document creation.
638
     * @return object The filled and managed document object or NULL, if the query result is empty.
639
     */
640 378
    private function createDocument($result, $document = null, array $hints = array())
641
    {
642 378
        if ($result === null) {
643 120
            return null;
644
        }
645
646 325
        if ($document !== null) {
647 38
            $hints[Query::HINT_REFRESH] = true;
648 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
649 38
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
650 38
        }
651
652 325
        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...
653
    }
654
655
    /**
656
     * Loads a PersistentCollection data. Used in the initialize() method.
657
     *
658
     * @param PersistentCollectionInterface $collection
659
     */
660 173
    public function loadCollection(PersistentCollectionInterface $collection)
661
    {
662 173
        $mapping = $collection->getMapping();
663 173
        switch ($mapping['association']) {
664 173
            case ClassMetadata::EMBED_MANY:
665 119
                $this->loadEmbedManyCollection($collection);
666 119
                break;
667
668 71
            case ClassMetadata::REFERENCE_MANY:
669 71
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
670 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
671 4
                } else {
672 66
                    if ($mapping['isOwningSide']) {
673 55
                        $this->loadReferenceManyCollectionOwningSide($collection);
674 55
                    } else {
675 15
                        $this->loadReferenceManyCollectionInverseSide($collection);
676
                    }
677
                }
678 70
                break;
679 172
        }
680 172
    }
681
682 119
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
683
    {
684 119
        $embeddedDocuments = $collection->getMongoData();
685 119
        $mapping = $collection->getMapping();
686 119
        $owner = $collection->getOwner();
687 119
        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...
688 90
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
689 90
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
690 90
                $embeddedMetadata = $this->dm->getClassMetadata($className);
691 90
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
692
693 90
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
694
695 90
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
696 90
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
697 90
                    ? $data[$embeddedMetadata->identifier]
698 90
                    : null;
699
700 90
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
701 89
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
702 89
                }
703 90
                if (CollectionHelper::isHash($mapping['strategy'])) {
704 25
                    $collection->set($key, $embeddedDocumentObject);
705 25
                } else {
706 72
                    $collection->add($embeddedDocumentObject);
707
                }
708 90
            }
709 90
        }
710 119
    }
711
712 55
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
713
    {
714 55
        $hints = $collection->getHints();
715 55
        $mapping = $collection->getMapping();
716 55
        $groupedIds = array();
717
718 55
        $sorted = isset($mapping['sort']) && $mapping['sort'];
719
720 55
        foreach ($collection->getMongoData() as $key => $reference) {
721 50
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
722 5
                $className = $mapping['targetDocument'];
723 5
                $mongoId = $reference;
724 5
            } else {
725 46
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
726 46
                $mongoId = $reference['$id'];
727
            }
728 50
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
729
730
            // create a reference to the class and id
731 50
            $reference = $this->dm->getReference($className, $id);
732
733
            // no custom sort so add the references right now in the order they are embedded
734 50
            if ( ! $sorted) {
735 49
                if (CollectionHelper::isHash($mapping['strategy'])) {
736 2
                    $collection->set($key, $reference);
737 2
                } else {
738 47
                    $collection->add($reference);
739
                }
740 49
            }
741
742
            // only query for the referenced object if it is not already initialized or the collection is sorted
743 50
            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...
744 35
                $groupedIds[$className][] = $mongoId;
745 35
            }
746 55
        }
747 55
        foreach ($groupedIds as $className => $ids) {
748 35
            $class = $this->dm->getClassMetadata($className);
749 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
750 35
            $criteria = $this->cm->merge(
751 35
                array('_id' => array('$in' => array_values($ids))),
752 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
753 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
754 35
            );
755 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
756 35
            $cursor = $mongoCollection->find($criteria);
757 35
            if (isset($mapping['sort'])) {
758 35
                $cursor->sort($mapping['sort']);
759 35
            }
760 35
            if (isset($mapping['limit'])) {
761
                $cursor->limit($mapping['limit']);
762
            }
763 35
            if (isset($mapping['skip'])) {
764
                $cursor->skip($mapping['skip']);
765
            }
766 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

Loading history...
767
                $cursor->slaveOkay(true);
768
            }
769 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...
770
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
771
            }
772 35
            $documents = $cursor->toArray(false);
773 35
            foreach ($documents as $documentData) {
774 34
                $document = $this->uow->getById($documentData['_id'], $class);
775 35
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
776 34
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
777 34
                    $this->uow->setOriginalDocumentData($document, $data);
778 34
                    $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...
779 34
                }
780 34
                if ($sorted) {
781 1
                    $collection->add($document);
782 1
                }
783 35
            }
784 55
        }
785 55
    }
786
787 15
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
788
    {
789 15
        $query = $this->createReferenceManyInverseSideQuery($collection);
790 15
        $documents = $query->execute()->toArray(false);
791 15
        foreach ($documents as $key => $document) {
792 14
            $collection->add($document);
793 15
        }
794 15
    }
795
796
    /**
797
     * @param PersistentCollectionInterface $collection
798
     *
799
     * @return Query
800
     */
801 18
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
802
    {
803 18
        $hints = $collection->getHints();
804 18
        $mapping = $collection->getMapping();
805 18
        $owner = $collection->getOwner();
806 18
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
807 18
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
808 18
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
809 18
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
810 18
        $criteria = $this->cm->merge(
811 18
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
812 18
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
813 18
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
814 18
        );
815 18
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
816 18
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
817 18
            ->setQueryArray($criteria);
818
819 18
        if (isset($mapping['sort'])) {
820 18
            $qb->sort($mapping['sort']);
821 18
        }
822 18
        if (isset($mapping['limit'])) {
823 2
            $qb->limit($mapping['limit']);
824 2
        }
825 18
        if (isset($mapping['skip'])) {
826
            $qb->skip($mapping['skip']);
827
        }
828 18
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

Loading history...
829
            $qb->slaveOkay(true);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Query\Builder::slaveOkay() has been deprecated with message: in version 1.2 - use setReadPreference instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
830
        }
831 18 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...
832
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
833
        }
834 18
        foreach ($mapping['prime'] as $field) {
835 4
            $qb->field($field)->prime(true);
836 18
        }
837
838 18
        return $qb->getQuery();
839
    }
840
841 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
842
    {
843 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
844 4
        $mapping = $collection->getMapping();
845 4
        $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...
846 4
        foreach ($documents as $key => $obj) {
847 4
            if (CollectionHelper::isHash($mapping['strategy'])) {
848 1
                $collection->set($key, $obj);
849 1
            } else {
850 3
                $collection->add($obj);
851
            }
852 4
        }
853 4
    }
854
855
    /**
856
     * @param PersistentCollectionInterface $collection
857
     *
858
     * @return CursorInterface
859
     */
860 6
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
861
    {
862 6
        $hints = $collection->getHints();
863 6
        $mapping = $collection->getMapping();
864 6
        $repositoryMethod = $mapping['repositoryMethod'];
865 6
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
866 6
            ->$repositoryMethod($collection->getOwner());
867
868 6
        if ( ! $cursor instanceof CursorInterface) {
869
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
870
        }
871
872 6
        if (!empty($mapping['prime'])) {
873 2
            if (!$cursor instanceof Cursor) {
874
                throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a Cursor to allow for priming");
875
            }
876
877 2
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
878 2
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
879
880 2
            $cursor->enableReferencePriming($primers, $referencePrimer);
881 1
        }
882
883 5
        if (isset($mapping['sort'])) {
884 5
            $cursor->sort($mapping['sort']);
885 5
        }
886 5
        if (isset($mapping['limit'])) {
887 1
            $cursor->limit($mapping['limit']);
888 1
        }
889 5
        if (isset($mapping['skip'])) {
890
            $cursor->skip($mapping['skip']);
891
        }
892 5
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

Loading history...
893
            $cursor->slaveOkay(true);
894
        }
895 5 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...
896
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
897
        }
898
899 5
        return $cursor;
900
    }
901
902
    /**
903
     * Prepare a sort or projection array by converting keys, which are PHP
904
     * property names, to MongoDB field names.
905
     *
906
     * @param array $fields
907
     * @return array
908
     */
909 141
    public function prepareSortOrProjection(array $fields)
910
    {
911 141
        $preparedFields = array();
912
913 141
        foreach ($fields as $key => $value) {
914 39
            $preparedFields[$this->prepareFieldName($key)] = $value;
915 141
        }
916
917 141
        return $preparedFields;
918
    }
919
920
    /**
921
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
922
     *
923
     * @param string $fieldName
924
     * @return string
925
     */
926 95
    public function prepareFieldName($fieldName)
927
    {
928 95
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
929
930 95
        return $fieldNames[0][0];
931
    }
932
933
    /**
934
     * Adds discriminator 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 before
938
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
939
     *
940
     * @param array $preparedQuery
941
     * @return array
942
     */
943 531
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
944
    {
945
        /* If the class has a discriminator field, which is not already in the
946
         * criteria, inject it now. The field/values need no preparation.
947
         */
948 531
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
949 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
950 29
            if (count($discriminatorValues) === 1) {
951 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
952 21
            } else {
953 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
954
            }
955 29
        }
956
957 531
        return $preparedQuery;
958
    }
959
960
    /**
961
     * Adds filter criteria to an already-prepared query.
962
     *
963
     * This method should be used once for query criteria and not be used for
964
     * nested expressions. It should be called after
965
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
966
     *
967
     * @param array $preparedQuery
968
     * @return array
969
     */
970 532
    public function addFilterToPreparedQuery(array $preparedQuery)
971
    {
972
        /* If filter criteria exists for this class, prepare it and merge
973
         * over the existing query.
974
         *
975
         * @todo Consider recursive merging in case the filter criteria and
976
         * prepared query both contain top-level $and/$or operators.
977
         */
978 532
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
979 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
980 18
        }
981
982 532
        return $preparedQuery;
983
    }
984
985
    /**
986
     * Prepares the query criteria or new document object.
987
     *
988
     * PHP field names and types will be converted to those used by MongoDB.
989
     *
990
     * @param array $query
991
     * @param bool $isNewObj
992
     * @return array
993
     */
994 557
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
995
    {
996 557
        $preparedQuery = array();
997
998 557
        foreach ($query as $key => $value) {
999
            // Recursively prepare logical query clauses
1000 517
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1001 20
                foreach ($value as $k2 => $v2) {
1002 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1003 20
                }
1004 20
                continue;
1005
            }
1006
1007 517
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1008 26
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1009 26
                continue;
1010
            }
1011
1012 517
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1013 517
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1014 517
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1015 517
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1016 517
                    : Type::convertPHPToDatabaseValue($preparedValue);
1017 517
            }
1018 557
        }
1019
1020 557
        return $preparedQuery;
1021
    }
1022
1023
    /**
1024
     * Prepares a query value and converts the PHP value to the database value
1025
     * if it is an identifier.
1026
     *
1027
     * It also handles converting $fieldName to the database name if they are different.
1028
     *
1029
     * @param string $fieldName
1030
     * @param mixed $value
1031
     * @param ClassMetadata $class        Defaults to $this->class
1032
     * @param bool $prepareValue Whether or not to prepare the value
1033
     * @param bool $inNewObj Whether or not newObj is being prepared
1034
     * @return array An array of tuples containing prepared field names and values
1035
     */
1036 551
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1037
    {
1038 551
        $class = isset($class) ? $class : $this->class;
1039
1040
        // @todo Consider inlining calls to ClassMetadataInfo methods
1041
1042
        // Process all non-identifier fields by translating field names
1043 551
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1044 261
            $mapping = $class->fieldMappings[$fieldName];
1045 261
            $fieldName = $mapping['name'];
1046
1047 261
            if ( ! $prepareValue) {
1048 69
                return [[$fieldName, $value]];
1049
            }
1050
1051
            // Prepare mapped, embedded objects
1052 215
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1053 215
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1054 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1055
            }
1056
1057 213
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1058
                try {
1059 13
                    return $this->prepareDbRefElement($fieldName, $value, $mapping, $inNewObj);
1060 1
                } catch (MappingException $e) {
1061
                    // do nothing in case passed object is not mapped document
1062
                }
1063 1
            }
1064
1065
            // No further preparation unless we're dealing with a simple reference
1066
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1067 201
            $arrayValue = (array) $value;
1068 201
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1069 124
                return [[$fieldName, $value]];
1070
            }
1071
1072
            // Additional preparation for one or more simple reference values
1073 105
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1074
1075 105
            if ( ! is_array($value)) {
1076 101
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1077
            }
1078
1079
            // Objects without operators or with DBRef fields can be converted immediately
1080 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...
1081 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1082
            }
1083
1084 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1085
        }
1086
1087
        // Process identifier fields
1088 407
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1089 337
            $fieldName = '_id';
1090
1091 337
            if ( ! $prepareValue) {
1092 20
                return [[$fieldName, $value]];
1093
            }
1094
1095 320
            if ( ! is_array($value)) {
1096 297
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1097
            }
1098
1099
            // Objects without operators or with DBRef fields can be converted immediately
1100 58 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...
1101 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1102
            }
1103
1104 53
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1105
        }
1106
1107
        // No processing for unmapped, non-identifier, non-dotted field names
1108 110
        if (strpos($fieldName, '.') === false) {
1109 49
            return [[$fieldName, $value]];
1110
        }
1111
1112
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1113
         *
1114
         * We can limit parsing here, since at most three segments are
1115
         * significant: "fieldName.objectProperty" with an optional index or key
1116
         * for collections stored as either BSON arrays or objects.
1117
         */
1118 67
        $e = explode('.', $fieldName, 4);
1119
1120
        // No further processing for unmapped fields
1121 67
        if ( ! isset($class->fieldMappings[$e[0]])) {
1122 4
            return [[$fieldName, $value]];
1123
        }
1124
1125 64
        $mapping = $class->fieldMappings[$e[0]];
1126 64
        $e[0] = $mapping['name'];
1127
1128
        // Hash and raw fields will not be prepared beyond the field name
1129 64
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1130 1
            $fieldName = implode('.', $e);
1131
1132 1
            return [[$fieldName, $value]];
1133
        }
1134
1135 63
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1136 63
                && isset($e[2])) {
1137 1
            $objectProperty = $e[2];
1138 1
            $objectPropertyPrefix = $e[1] . '.';
1139 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1140 63
        } elseif ($e[1] != '$') {
1141 61
            $fieldName = $e[0] . '.' . $e[1];
1142 61
            $objectProperty = $e[1];
1143 61
            $objectPropertyPrefix = '';
1144 61
            $nextObjectProperty = implode('.', array_slice($e, 2));
1145 62
        } elseif (isset($e[2])) {
1146 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1147 1
            $objectProperty = $e[2];
1148 1
            $objectPropertyPrefix = $e[1] . '.';
1149 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1150 1
        } else {
1151 1
            $fieldName = $e[0] . '.' . $e[1];
1152
1153 1
            return [[$fieldName, $value]];
1154
        }
1155
1156
        // No further processing for fields without a targetDocument mapping
1157 63
        if ( ! isset($mapping['targetDocument'])) {
1158 3
            if ($nextObjectProperty) {
1159
                $fieldName .= '.'.$nextObjectProperty;
1160
            }
1161
1162 3
            return [[$fieldName, $value]];
1163
        }
1164
1165 60
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1166
1167
        // No further processing for unmapped targetDocument fields
1168 60
        if ( ! $targetClass->hasField($objectProperty)) {
1169 28
            if ($nextObjectProperty) {
1170
                $fieldName .= '.'.$nextObjectProperty;
1171
            }
1172
1173 28
            return [[$fieldName, $value]];
1174
        }
1175
1176 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1177 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1178
1179
        // Prepare DBRef identifiers or the mapped field's property path
1180 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1181 35
            ? $e[0] . '.$id'
1182 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1183
1184
        // Process targetDocument identifier fields
1185 35
        if ($objectPropertyIsId) {
1186 14
            if ( ! $prepareValue) {
1187 1
                return [[$fieldName, $value]];
1188
            }
1189
1190 13
            if ( ! is_array($value)) {
1191 2
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1192
            }
1193
1194
            // Objects without operators or with DBRef fields can be converted immediately
1195 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...
1196 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1197
            }
1198
1199 12
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1200
        }
1201
1202
        /* The property path may include a third field segment, excluding the
1203
         * collection item pointer. If present, this next object property must
1204
         * be processed recursively.
1205
         */
1206 21
        if ($nextObjectProperty) {
1207
            // Respect the targetDocument's class metadata when recursing
1208 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1209 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1210 14
                : null;
1211
1212 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1213
1214
            return array_map(function ($preparedTuple) use ($fieldName) {
1215 14
                list($key, $value) = $preparedTuple;
1216
1217 14
                return [$fieldName . '.' . $key, $value];
1218 14
            }, $fieldNames);
1219
        }
1220
1221 9
        return [[$fieldName, $value]];
1222
    }
1223
1224
    /**
1225
     * Prepares a query expression.
1226
     *
1227
     * @param array|object  $expression
1228
     * @param ClassMetadata $class
1229
     * @return array
1230
     */
1231 71
    private function prepareQueryExpression($expression, $class)
1232
    {
1233 71
        foreach ($expression as $k => $v) {
1234
            // Ignore query operators whose arguments need no type conversion
1235 71
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1236 12
                continue;
1237
            }
1238
1239
            // Process query operators whose argument arrays need type conversion
1240 71
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1241 69
                foreach ($v as $k2 => $v2) {
1242 69
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1243 69
                }
1244 69
                continue;
1245
            }
1246
1247
            // Recursively process expressions within a $not operator
1248 14
            if ($k === '$not' && is_array($v)) {
1249 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1250 11
                continue;
1251
            }
1252
1253 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1254 71
        }
1255
1256 71
        return $expression;
1257
    }
1258
1259
    /**
1260
     * Checks whether the value has DBRef fields.
1261
     *
1262
     * This method doesn't check if the the value is a complete DBRef object,
1263
     * although it should return true for a DBRef. Rather, we're checking that
1264
     * the value has one or more fields for a DBref. In practice, this could be
1265
     * $elemMatch criteria for matching a DBRef.
1266
     *
1267
     * @param mixed $value
1268
     * @return boolean
1269
     */
1270 72
    private function hasDBRefFields($value)
1271
    {
1272 72
        if ( ! is_array($value) && ! is_object($value)) {
1273
            return false;
1274
        }
1275
1276 72
        if (is_object($value)) {
1277
            $value = get_object_vars($value);
1278
        }
1279
1280 72
        foreach ($value as $key => $_) {
1281 72
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1282 3
                return true;
1283
            }
1284 71
        }
1285
1286 71
        return false;
1287
    }
1288
1289
    /**
1290
     * Checks whether the value has query operators.
1291
     *
1292
     * @param mixed $value
1293
     * @return boolean
1294
     */
1295 76
    private function hasQueryOperators($value)
1296
    {
1297 76
        if ( ! is_array($value) && ! is_object($value)) {
1298
            return false;
1299
        }
1300
1301 76
        if (is_object($value)) {
1302
            $value = get_object_vars($value);
1303
        }
1304
1305 76
        foreach ($value as $key => $_) {
1306 76
            if (isset($key[0]) && $key[0] === '$') {
1307 72
                return true;
1308
            }
1309 9
        }
1310
1311 9
        return false;
1312
    }
1313
1314
    /**
1315
     * Gets the array of discriminator values for the given ClassMetadata
1316
     *
1317
     * @param ClassMetadata $metadata
1318
     * @return array
1319
     */
1320 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1321
    {
1322 29
        $discriminatorValues = array($metadata->discriminatorValue);
1323 29
        foreach ($metadata->subClasses as $className) {
1324 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1325 8
                $discriminatorValues[] = $key;
1326 8
            }
1327 29
        }
1328
1329
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1330 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...
1331 2
            $discriminatorValues[] = null;
1332 2
        }
1333
1334 29
        return $discriminatorValues;
1335
    }
1336
1337 611
    private function handleCollections($document, $options)
1338
    {
1339
        // Collection deletions (deletions of complete collections)
1340 611
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1341 107
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1342 31
                $this->cp->delete($coll, $options);
1343 31
            }
1344 611
        }
1345
        // Collection updates (deleteRows, updateRows, insertRows)
1346 611
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1347 107
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1348 99
                $this->cp->update($coll, $options);
1349 99
            }
1350 611
        }
1351
        // Take new snapshots from visited collections
1352 611
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1353 257
            $coll->takeSnapshot();
1354 611
        }
1355 611
    }
1356
1357
    /**
1358
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1359
     * Also, shard key field should be present in actual document data.
1360
     *
1361
     * @param object $document
1362
     * @param string $shardKeyField
1363
     * @param array  $actualDocumentData
1364
     *
1365
     * @throws MongoDBException
1366
     */
1367 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1368
    {
1369 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1370 9
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1371
1372 9
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1373 9
        $fieldName = $fieldMapping['fieldName'];
1374
1375 9
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1376 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1377
        }
1378
1379 7
        if (!isset($actualDocumentData[$fieldName])) {
1380
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1381
        }
1382 7
    }
1383
1384
    /**
1385
     * Get shard key aware query for single document.
1386
     *
1387
     * @param object $document
1388
     *
1389
     * @return array
1390
     */
1391 302
    private function getQueryForDocument($document)
1392
    {
1393 302
        $id = $this->uow->getDocumentIdentifier($document);
1394 302
        $id = $this->class->getDatabaseIdentifierValue($id);
1395
1396 302
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1397 300
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1398
1399 300
        return $query;
1400
    }
1401
1402
    /**
1403
     * @param array $options
1404
     *
1405
     * @return array
1406
     */
1407 613
    private function getWriteOptions(array $options = array())
1408
    {
1409 613
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1410 613
        $documentOptions = [];
1411 613
        if ($this->class->hasWriteConcern()) {
1412 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1413 9
        }
1414
1415 613
        return array_merge($defaultOptions, $documentOptions, $options);
1416
    }
1417
1418
    /**
1419
     * @param string $fieldName
1420
     * @param mixed $value
1421
     * @param array $mapping
1422
     * @param bool $inNewObj
1423
     * @return array
1424
     */
1425 13
    private function prepareDbRefElement($fieldName, $value, array $mapping, $inNewObj)
1426
    {
1427 13
        $dbRef = $this->dm->createDBRef($value, $mapping);
1428 12
        if ($inNewObj) {
1429 6
            return [[$fieldName, $dbRef]];
1430
        }
1431 6
        $keys = ['$ref' => true, '$id' => true, '$db' => true];
1432 6
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1433 2
            unset($keys['$db']);
1434 2
        }
1435 6
        if (isset($mapping['targetDocument'])) {
1436 5
            unset($keys['$ref'], $keys['$db']);
1437 5
        }
1438
1439 6
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1440 2
            return [[$fieldName, $dbRef]];
1441 4
        } elseif ($mapping['type'] === 'many') {
1442 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($dbRef, $keys)]]];
1443
        } else {
1444 2
            return array_map(
1445 2
                function ($key) use ($dbRef, $fieldName) {
1446 2
                    return [$fieldName . '.' . $key, $dbRef[$key]];
1447 2
                },
1448 2
                array_keys($keys)
1449 2
            );
1450
        }
1451
    }
1452
}
1453