Completed
Pull Request — master (#1623)
by Andreas
09:54
created

DocumentPersister::addFilterToPreparedQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2
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 764
    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 764
        $this->pb = $pb;
141 764
        $this->dm = $dm;
142 764
        $this->evm = $evm;
143 764
        $this->cm = $cm ?: new CriteriaMerger();
144 764
        $this->uow = $uow;
145 764
        $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 764
        $this->class = $class;
147 764
        $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 764
        $this->cp = $this->uow->getCollectionPersister();
149 764
    }
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 533
    public function addInsert($document)
175
    {
176 533
        $this->queuedInserts[spl_object_hash($document)] = $document;
177 533
    }
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 86
    public function addUpsert($document)
203
    {
204 86
        $this->queuedUpserts[spl_object_hash($document)] = $document;
205 86
    }
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 533
    public function executeInserts(array $options = array())
228
    {
229 533
        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 533
        $inserts = array();
234 533
        $options = $this->getWriteOptions($options);
235 533
        foreach ($this->queuedInserts as $oid => $document) {
236 533
            $data = $this->pb->prepareInsertData($document);
237
238
            // Set the initial version for each insert
239 532 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 2
                } 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
                }
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
            }
251
252 532
            $inserts[$oid] = $data;
253
        }
254
255 532
        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 532
                $this->collection->batchInsert($inserts, $options);
258 7
            } catch (\MongoException $e) {
259 7
                $this->queuedInserts = array();
260 7
                throw $e;
261
            }
262
        }
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 532
        foreach ($this->queuedInserts as $document) {
269 532
            $this->handleCollections($document, $options);
270
        }
271
272 532
        $this->queuedInserts = array();
273 532
    }
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 86
    public function executeUpserts(array $options = array())
285
    {
286 86
        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 86
        $options = $this->getWriteOptions($options);
291 86
        foreach ($this->queuedUpserts as $oid => $document) {
292
            try {
293 86
                $this->executeUpsert($document, $options);
294 86
                $this->handleCollections($document, $options);
295 86
                unset($this->queuedUpserts[$oid]);
296
            } catch (\MongoException $e) {
297
                unset($this->queuedUpserts[$oid]);
298 86
                throw $e;
299
            }
300
        }
301 86
    }
302
303
    /**
304
     * Executes a single upsert in {@link executeUpserts}
305
     *
306
     * @param object $document
307
     * @param array  $options
308
     */
309 86
    private function executeUpsert($document, array $options)
310
    {
311 86
        $options['upsert'] = true;
312 86
        $criteria = $this->getQueryForDocument($document);
313
314 86
        $data = $this->pb->prepareUpsertData($document);
315
316
        // Set the initial version for each upsert
317 86 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 1
            } 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
            }
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
        }
329
330 86
        foreach (array_keys($criteria) as $field) {
331 86
            unset($data['$set'][$field]);
332
        }
333
334
        // Do not send an empty $set modifier
335 86
        if (empty($data['$set'])) {
336 17
            unset($data['$set']);
337
        }
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 86
        if (empty($data)) {
353 17
            $retry = true;
354 17
            $data = array('$set' => array('_id' => $criteria['_id']));
355
        }
356
357
        try {
358 86
            $this->collection->update($criteria, $data, $options);
359 86
            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 222
    public function update($document, array $options = array())
377
    {
378 222
        $update = $this->pb->prepareUpdateData($document);
379
380 222
        $query = $this->getQueryForDocument($document);
381
382 222
        foreach (array_keys($query) as $field) {
383 222
            unset($update['$set'][$field]);
384
        }
385
386 222
        if (empty($update['$set'])) {
387 94
            unset($update['$set']);
388
        }
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 222
        if ($this->class->isVersioned) {
395 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
396 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
397 31
            if ($versionMapping['type'] === 'int') {
398 28
                $nextVersion = $currentVersion + 1;
399 28
                $update['$inc'][$versionMapping['name']] = 1;
400 28
                $query[$versionMapping['name']] = $currentVersion;
401 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
402 3
            } 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
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
407
            }
408
        }
409
410 222
        if ( ! empty($update)) {
411
            // Include locking logic so that if the document object in memory is currently
412
            // locked then it will remove it, otherwise it ensures the document is not locked.
413 154
            if ($this->class->isLockable) {
414 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
415 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
416 11
                if ($isLocked) {
417 2
                    $update['$unset'] = array($lockMapping['name'] => true);
418
                } else {
419 9
                    $query[$lockMapping['name']] = array('$exists' => false);
420
                }
421
            }
422
423 154
            $options = $this->getWriteOptions($options);
424
425 154
            $result = $this->collection->update($query, $update, $options);
426
427 154 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...
428 5
                throw LockException::lockFailed($document);
429
            }
430
        }
431
432 218
        $this->handleCollections($document, $options);
433 218
    }
434
435
    /**
436
     * Removes document from mongo
437
     *
438
     * @param mixed $document
439
     * @param array $options Array of options to be used with remove()
440
     * @throws \Doctrine\ODM\MongoDB\LockException
441
     */
442 33
    public function delete($document, array $options = array())
443
    {
444 33
        $query = $this->getQueryForDocument($document);
445
446 33
        if ($this->class->isLockable) {
447 2
            $query[$this->class->lockField] = array('$exists' => false);
448
        }
449
450 33
        $options = $this->getWriteOptions($options);
451
452 33
        $result = $this->collection->remove($query, $options);
453
454 33 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...
455 2
            throw LockException::lockFailed($document);
456
        }
457 31
    }
458
459
    /**
460
     * Refreshes a managed document.
461
     *
462
     * @param string $id
463
     * @param object $document The document to refresh.
464
     *
465
     * @deprecated The first argument is deprecated.
466
     */
467 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...
468
    {
469 21
        $query = $this->getQueryForDocument($document);
470 21
        $data = $this->collection->findOne($query);
471 21
        $data = $this->hydratorFactory->hydrate($document, $data);
472 21
        $this->uow->setOriginalDocumentData($document, $data);
473 21
    }
474
475
    /**
476
     * Finds a document by a set of criteria.
477
     *
478
     * If a scalar or MongoId is provided for $criteria, it will be used to
479
     * match an _id value.
480
     *
481
     * @param mixed   $criteria Query criteria
482
     * @param object  $document Document to load the data into. If not specified, a new document is created.
483
     * @param array   $hints    Hints for document creation
484
     * @param integer $lockMode
485
     * @param array   $sort     Sort array for Cursor::sort()
486
     * @throws \Doctrine\ODM\MongoDB\LockException
487
     * @return object|null The loaded and managed document instance or null if no document was found
488
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
489
     */
490 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...
491
    {
492
        // TODO: remove this
493 379
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
494
            $criteria = array('_id' => $criteria);
495
        }
496
497 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...
498 379
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
499 379
        $criteria = $this->addFilterToPreparedQuery($criteria);
500
501 379
        $cursor = $this->collection->find($criteria);
502
503 379
        if (null !== $sort) {
504 105
            $cursor->sort($this->prepareSortOrProjection($sort));
505
        }
506
507 379
        $result = $cursor->getSingleResult();
508
509 379
        if ($this->class->isLockable) {
510 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
511 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
512 1
                throw LockException::lockFailed($result);
513
            }
514
        }
515
516 378
        return $this->createDocument($result, $document, $hints);
517
    }
518
519
    /**
520
     * Finds documents by a set of criteria.
521
     *
522
     * @param array        $criteria Query criteria
523
     * @param array        $sort     Sort array for Cursor::sort()
524
     * @param integer|null $limit    Limit for Cursor::limit()
525
     * @param integer|null $skip     Skip for Cursor::skip()
526
     * @return Cursor
527
     */
528 26
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
529
    {
530 26
        $criteria = $this->prepareQueryOrNewObj($criteria);
531 26
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
532 26
        $criteria = $this->addFilterToPreparedQuery($criteria);
533
534 26
        $baseCursor = $this->collection->find($criteria);
535 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...
536
537 26
        if (null !== $sort) {
538 3
            $cursor->sort($sort);
539
        }
540
541 26
        if (null !== $limit) {
542 2
            $cursor->limit($limit);
543
        }
544
545 26
        if (null !== $skip) {
546 2
            $cursor->skip($skip);
547
        }
548
549 26
        return $cursor;
550
    }
551
552
    /**
553
     * @param object $document
554
     *
555
     * @return array
556
     * @throws MongoDBException
557
     */
558 297
    private function getShardKeyQuery($document)
559
    {
560 297
        if ( ! $this->class->isSharded()) {
561 294
            return array();
562
        }
563
564 3
        $shardKey = $this->class->getShardKey();
565 3
        $keys = array_keys($shardKey['keys']);
566 3
        $data = $this->uow->getDocumentActualData($document);
567
568 3
        $shardKeyQueryPart = array();
569 3
        foreach ($keys as $key) {
570 3
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
571 3
            $this->guardMissingShardKey($document, $key, $data);
572 3
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
573 3
            $shardKeyQueryPart[$key] = $value;
574
        }
575
576 3
        return $shardKeyQueryPart;
577
    }
578
579
    /**
580
     * Wraps the supplied base cursor in the corresponding ODM class.
581
     *
582
     * @param CursorInterface $baseCursor
583
     * @return Cursor
584
     */
585 26
    private function wrapCursor(CursorInterface $baseCursor)
586
    {
587 26
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
588
    }
589
590
    /**
591
     * Checks whether the given managed document exists in the database.
592
     *
593
     * @param object $document
594
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
595
     */
596 3
    public function exists($document)
597
    {
598 3
        $id = $this->class->getIdentifierObject($document);
599 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
600
    }
601
602
    /**
603
     * Locks document by storing the lock mode on the mapped lock field.
604
     *
605
     * @param object $document
606
     * @param int $lockMode
607
     */
608 5
    public function lock($document, $lockMode)
609
    {
610 5
        $id = $this->uow->getDocumentIdentifier($document);
611 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
612 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
613 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
614 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
615 5
    }
616
617
    /**
618
     * Releases any lock that exists on this document.
619
     *
620
     * @param object $document
621
     */
622 1
    public function unlock($document)
623
    {
624 1
        $id = $this->uow->getDocumentIdentifier($document);
625 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
626 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
627 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
628 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
629 1
    }
630
631
    /**
632
     * Creates or fills a single document object from an query result.
633
     *
634
     * @param object $result The query result.
635
     * @param object $document The document object to fill, if any.
636
     * @param array $hints Hints for document creation.
637
     * @return object The filled and managed document object or NULL, if the query result is empty.
638
     */
639 378
    private function createDocument($result, $document = null, array $hints = array())
640
    {
641 378
        if ($result === null) {
642 125
            return null;
643
        }
644
645 325
        if ($document !== null) {
646 38
            $hints[Query::HINT_REFRESH] = true;
647 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
648 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...
649
        }
650
651 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...
652
    }
653
654
    /**
655
     * Loads a PersistentCollection data. Used in the initialize() method.
656
     *
657
     * @param PersistentCollectionInterface $collection
658
     */
659 173
    public function loadCollection(PersistentCollectionInterface $collection)
660
    {
661 173
        $mapping = $collection->getMapping();
662 173
        switch ($mapping['association']) {
663 173
            case ClassMetadata::EMBED_MANY:
664 119
                $this->loadEmbedManyCollection($collection);
665 119
                break;
666
667 71
            case ClassMetadata::REFERENCE_MANY:
668 71
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
669 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
670
                } else {
671 66
                    if ($mapping['isOwningSide']) {
672 55
                        $this->loadReferenceManyCollectionOwningSide($collection);
673
                    } else {
674 15
                        $this->loadReferenceManyCollectionInverseSide($collection);
675
                    }
676
                }
677 70
                break;
678
        }
679 172
    }
680
681 119
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
682
    {
683 119
        $embeddedDocuments = $collection->getMongoData();
684 119
        $mapping = $collection->getMapping();
685 119
        $owner = $collection->getOwner();
686 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...
687 90
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
688 90
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
689 90
                $embeddedMetadata = $this->dm->getClassMetadata($className);
690 90
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
691
692 90
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
693
694 90
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
695 90
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
696 23
                    ? $data[$embeddedMetadata->identifier]
697 90
                    : null;
698
                
699 90
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
700 89
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
701
                }
702 90
                if (CollectionHelper::isHash($mapping['strategy'])) {
703 25
                    $collection->set($key, $embeddedDocumentObject);
704
                } else {
705 90
                    $collection->add($embeddedDocumentObject);
706
                }
707
            }
708
        }
709 119
    }
710
711 55
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
712
    {
713 55
        $hints = $collection->getHints();
714 55
        $mapping = $collection->getMapping();
715 55
        $groupedIds = array();
716
717 55
        $sorted = isset($mapping['sort']) && $mapping['sort'];
718
719 55
        foreach ($collection->getMongoData() as $key => $reference) {
720 50
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
721 5
                $className = $mapping['targetDocument'];
722 5
                $mongoId = $reference;
723
            } else {
724 46
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
725 46
                $mongoId = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
726
            }
727 50
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
728
729
            // create a reference to the class and id
730 50
            $reference = $this->dm->getReference($className, $id);
731
732
            // no custom sort so add the references right now in the order they are embedded
733 50
            if ( ! $sorted) {
734 49
                if (CollectionHelper::isHash($mapping['strategy'])) {
735 2
                    $collection->set($key, $reference);
736
                } else {
737 47
                    $collection->add($reference);
738
                }
739
            }
740
741
            // only query for the referenced object if it is not already initialized or the collection is sorted
742 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...
743 50
                $groupedIds[$className][] = $mongoId;
744
            }
745
        }
746 55
        foreach ($groupedIds as $className => $ids) {
747 35
            $class = $this->dm->getClassMetadata($className);
748 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
749 35
            $criteria = $this->cm->merge(
750 35
                array('_id' => array('$in' => array_values($ids))),
751 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
752 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
753
            );
754 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
755 35
            $cursor = $mongoCollection->find($criteria);
756 35
            if (isset($mapping['sort'])) {
757 35
                $cursor->sort($mapping['sort']);
758
            }
759 35
            if (isset($mapping['limit'])) {
760
                $cursor->limit($mapping['limit']);
761
            }
762 35
            if (isset($mapping['skip'])) {
763
                $cursor->skip($mapping['skip']);
764
            }
765 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...
766
                $cursor->slaveOkay(true);
767
            }
768 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...
769
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
770
            }
771 35
            $documents = $cursor->toArray(false);
772 35
            foreach ($documents as $documentData) {
773 34
                $document = $this->uow->getById($documentData['_id'], $class);
774 34
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
775 34
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
776 34
                    $this->uow->setOriginalDocumentData($document, $data);
777 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...
778
                }
779 34
                if ($sorted) {
780 35
                    $collection->add($document);
781
                }
782
            }
783
        }
784 55
    }
785
786 15
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
787
    {
788 15
        $query = $this->createReferenceManyInverseSideQuery($collection);
789 15
        $documents = $query->execute()->toArray(false);
790 15
        foreach ($documents as $key => $document) {
791 14
            $collection->add($document);
792
        }
793 15
    }
794
795
    /**
796
     * @param PersistentCollectionInterface $collection
797
     *
798
     * @return Query
799
     */
800 18
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
801
    {
802 18
        $hints = $collection->getHints();
803 18
        $mapping = $collection->getMapping();
804 18
        $owner = $collection->getOwner();
805 18
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
806 18
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
807 18
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
808 18
        if (isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
809 3
            $mappedByFieldName = $mapping['mappedBy'];
810
        } else {
811 15
            $mappedByFieldName = $mapping['mappedBy'] . '.' . ClassMetadataInfo::getReferencePrefix(isset($mappedByMapping['storeAs']) ? $mappedByMapping['storeAs'] : null) . 'id';
812
        }
813
814 18
        $criteria = $this->cm->merge(
815 18
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
816 18
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
817 18
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
818
        );
819 18
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
820 18
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
821 18
            ->setQueryArray($criteria);
822
823 18
        if (isset($mapping['sort'])) {
824 18
            $qb->sort($mapping['sort']);
825
        }
826 18
        if (isset($mapping['limit'])) {
827 2
            $qb->limit($mapping['limit']);
828
        }
829 18
        if (isset($mapping['skip'])) {
830
            $qb->skip($mapping['skip']);
831
        }
832 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...
833
            $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...
834
        }
835 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...
836
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
837
        }
838 18
        foreach ($mapping['prime'] as $field) {
839 4
            $qb->field($field)->prime(true);
840
        }
841
842 18
        return $qb->getQuery();
843
    }
844
845 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
846
    {
847 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
848 4
        $mapping = $collection->getMapping();
849 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...
850 4
        foreach ($documents as $key => $obj) {
851 4
            if (CollectionHelper::isHash($mapping['strategy'])) {
852 1
                $collection->set($key, $obj);
853
            } else {
854 4
                $collection->add($obj);
855
            }
856
        }
857 4
    }
858
859
    /**
860
     * @param PersistentCollectionInterface $collection
861
     *
862
     * @return CursorInterface
863
     */
864 6
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
865
    {
866 6
        $hints = $collection->getHints();
867 6
        $mapping = $collection->getMapping();
868 6
        $repositoryMethod = $mapping['repositoryMethod'];
869 6
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
870 6
            ->$repositoryMethod($collection->getOwner());
871
872 6
        if ( ! $cursor instanceof CursorInterface) {
873
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
874
        }
875
876 6
        if (!empty($mapping['prime'])) {
877 2
            if (!$cursor instanceof Cursor) {
878
                throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a Cursor to allow for priming");
879
            }
880
881 2
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
882 2
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
883
884 2
            $cursor->enableReferencePriming($primers, $referencePrimer);
885
        }
886
887 5
        if (isset($mapping['sort'])) {
888 5
            $cursor->sort($mapping['sort']);
889
        }
890 5
        if (isset($mapping['limit'])) {
891 1
            $cursor->limit($mapping['limit']);
892
        }
893 5
        if (isset($mapping['skip'])) {
894
            $cursor->skip($mapping['skip']);
895
        }
896 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...
897
            $cursor->slaveOkay(true);
898
        }
899 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...
900
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
901
        }
902
903 5
        return $cursor;
904
    }
905
906
    /**
907
     * Prepare a sort or projection array by converting keys, which are PHP
908
     * property names, to MongoDB field names.
909
     *
910
     * @param array $fields
911
     * @return array
912
     */
913 145
    public function prepareSortOrProjection(array $fields)
914
    {
915 145
        $preparedFields = array();
916
917 145
        foreach ($fields as $key => $value) {
918 39
            $preparedFields[$this->prepareFieldName($key)] = $value;
919
        }
920
921 145
        return $preparedFields;
922
    }
923
924
    /**
925
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
926
     *
927
     * @param string $fieldName
928
     * @return string
929
     */
930 96
    public function prepareFieldName($fieldName)
931
    {
932 96
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
933
934 96
        return $fieldNames[0][0];
935
    }
936
937
    /**
938
     * Adds discriminator criteria to an already-prepared query.
939
     *
940
     * This method should be used once for query criteria and not be used for
941
     * nested expressions. It should be called before
942
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
943
     *
944
     * @param array $preparedQuery
945
     * @return array
946
     */
947 534
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
948
    {
949
        /* If the class has a discriminator field, which is not already in the
950
         * criteria, inject it now. The field/values need no preparation.
951
         */
952 534
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
953 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
954 29
            if (count($discriminatorValues) === 1) {
955 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
956
            } else {
957 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
958
            }
959
        }
960
961 534
        return $preparedQuery;
962
    }
963
964
    /**
965
     * Adds filter criteria to an already-prepared query.
966
     *
967
     * This method should be used once for query criteria and not be used for
968
     * nested expressions. It should be called after
969
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
970
     *
971
     * @param array $preparedQuery
972
     * @return array
973
     */
974 535
    public function addFilterToPreparedQuery(array $preparedQuery)
975
    {
976
        /* If filter criteria exists for this class, prepare it and merge
977
         * over the existing query.
978
         *
979
         * @todo Consider recursive merging in case the filter criteria and
980
         * prepared query both contain top-level $and/$or operators.
981
         */
982 535
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
983 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
984
        }
985
986 535
        return $preparedQuery;
987
    }
988
989
    /**
990
     * Prepares the query criteria or new document object.
991
     *
992
     * PHP field names and types will be converted to those used by MongoDB.
993
     *
994
     * @param array $query
995
     * @param bool $isNewObj
996
     * @return array
997
     */
998 563
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
999
    {
1000 563
        $preparedQuery = array();
1001
1002 563
        foreach ($query as $key => $value) {
1003
            // Recursively prepare logical query clauses
1004 523
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
1005 20
                foreach ($value as $k2 => $v2) {
1006 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1007
                }
1008 20
                continue;
1009
            }
1010
1011 523
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1012 26
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1013 26
                continue;
1014
            }
1015
1016 523
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1017 523
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1018 523
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1019 135
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1020 523
                    : Type::convertPHPToDatabaseValue($preparedValue);
1021
            }
1022
        }
1023
1024 563
        return $preparedQuery;
1025
    }
1026
1027
    /**
1028
     * Prepares a query value and converts the PHP value to the database value
1029
     * if it is an identifier.
1030
     *
1031
     * It also handles converting $fieldName to the database name if they are different.
1032
     *
1033
     * @param string $fieldName
1034
     * @param mixed $value
1035
     * @param ClassMetadata $class        Defaults to $this->class
1036
     * @param bool $prepareValue Whether or not to prepare the value
1037
     * @param bool $inNewObj Whether or not newObj is being prepared
1038
     * @return array An array of tuples containing prepared field names and values
1039
     */
1040 558
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1041
    {
1042 558
        $class = isset($class) ? $class : $this->class;
1043
1044
        // @todo Consider inlining calls to ClassMetadataInfo methods
1045
1046
        // Process all non-identifier fields by translating field names
1047 558
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1048 261
            $mapping = $class->fieldMappings[$fieldName];
1049 261
            $fieldName = $mapping['name'];
1050
1051 261
            if ( ! $prepareValue) {
1052 67
                return [[$fieldName, $value]];
1053
            }
1054
1055
            // Prepare mapped, embedded objects
1056 217
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1057 217
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1058 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1059
            }
1060
1061 215
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1062
                try {
1063 13
                    return $this->prepareDbRefElement($fieldName, $value, $mapping, $inNewObj);
1064 1
                } catch (MappingException $e) {
1065
                    // do nothing in case passed object is not mapped document
1066
                }
1067
            }
1068
1069
            // No further preparation unless we're dealing with a simple reference
1070
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1071 203
            $arrayValue = (array) $value;
1072 203
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1073 126
                return [[$fieldName, $value]];
1074
            }
1075
1076
            // Additional preparation for one or more simple reference values
1077 105
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1078
1079 105
            if ( ! is_array($value)) {
1080 101
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1081
            }
1082
1083
            // Objects without operators or with DBRef fields can be converted immediately
1084 6 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1085 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1086
            }
1087
1088 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1089
        }
1090
1091
        // Process identifier fields
1092 456
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1093 337
            $fieldName = '_id';
1094
1095 337
            if ( ! $prepareValue) {
1096 20
                return [[$fieldName, $value]];
1097
            }
1098
1099 320
            if ( ! is_array($value)) {
1100 297
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1101
            }
1102
1103
            // Objects without operators or with DBRef fields can be converted immediately
1104 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...
1105 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1106
            }
1107
1108 53
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1109
        }
1110
1111
        // No processing for unmapped, non-identifier, non-dotted field names
1112 204
        if (strpos($fieldName, '.') === false) {
1113 50
            return [[$fieldName, $value]];
1114
        }
1115
1116
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1117
         *
1118
         * We can limit parsing here, since at most three segments are
1119
         * significant: "fieldName.objectProperty" with an optional index or key
1120
         * for collections stored as either BSON arrays or objects.
1121
         */
1122 163
        $e = explode('.', $fieldName, 4);
1123
1124
        // No further processing for unmapped fields
1125 163
        if ( ! isset($class->fieldMappings[$e[0]])) {
1126 4
            return [[$fieldName, $value]];
1127
        }
1128
1129 160
        $mapping = $class->fieldMappings[$e[0]];
1130 160
        $e[0] = $mapping['name'];
1131
1132
        // Hash and raw fields will not be prepared beyond the field name
1133 160
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1134 1
            $fieldName = implode('.', $e);
1135
1136 1
            return [[$fieldName, $value]];
1137
        }
1138
1139 159
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1140 159
                && isset($e[2])) {
1141 1
            $objectProperty = $e[2];
1142 1
            $objectPropertyPrefix = $e[1] . '.';
1143 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1144 158
        } elseif ($e[1] != '$') {
1145 157
            $fieldName = $e[0] . '.' . $e[1];
1146 157
            $objectProperty = $e[1];
1147 157
            $objectPropertyPrefix = '';
1148 157
            $nextObjectProperty = implode('.', array_slice($e, 2));
1149 1
        } elseif (isset($e[2])) {
1150 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1151 1
            $objectProperty = $e[2];
1152 1
            $objectPropertyPrefix = $e[1] . '.';
1153 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1154
        } else {
1155 1
            $fieldName = $e[0] . '.' . $e[1];
1156
1157 1
            return [[$fieldName, $value]];
1158
        }
1159
1160
        // No further processing for fields without a targetDocument mapping
1161 159
        if ( ! isset($mapping['targetDocument'])) {
1162 3
            if ($nextObjectProperty) {
1163
                $fieldName .= '.'.$nextObjectProperty;
1164
            }
1165
1166 3
            return [[$fieldName, $value]];
1167
        }
1168
1169 156
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1170
1171
        // No further processing for unmapped targetDocument fields
1172 156
        if ( ! $targetClass->hasField($objectProperty)) {
1173 28
            if ($nextObjectProperty) {
1174
                $fieldName .= '.'.$nextObjectProperty;
1175
            }
1176
1177 28
            return [[$fieldName, $value]];
1178
        }
1179
1180 133
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1181 133
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1182
1183
        // Prepare DBRef identifiers or the mapped field's property path
1184 133
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1185 111
            ? $e[0] . '.' . ClassMetadataInfo::getReferencePrefix($mapping['storeAs']) . 'id'
1186 133
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1187
1188
        // Process targetDocument identifier fields
1189 133
        if ($objectPropertyIsId) {
1190 112
            if ( ! $prepareValue) {
1191 1
                return [[$fieldName, $value]];
1192
            }
1193
1194 111
            if ( ! is_array($value)) {
1195 97
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1196
            }
1197
1198
            // Objects without operators or with DBRef fields can be converted immediately
1199 16 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1200 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1201
            }
1202
1203 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1204
        }
1205
1206
        /* The property path may include a third field segment, excluding the
1207
         * collection item pointer. If present, this next object property must
1208
         * be processed recursively.
1209
         */
1210 21
        if ($nextObjectProperty) {
1211
            // Respect the targetDocument's class metadata when recursing
1212 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1213 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1214 14
                : null;
1215
1216 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1217
1218
            return array_map(function ($preparedTuple) use ($fieldName) {
1219 14
                list($key, $value) = $preparedTuple;
1220
1221 14
                return [$fieldName . '.' . $key, $value];
1222 14
            }, $fieldNames);
1223
        }
1224
1225 9
        return [[$fieldName, $value]];
1226
    }
1227
1228
    /**
1229
     * Prepares a query expression.
1230
     *
1231
     * @param array|object  $expression
1232
     * @param ClassMetadata $class
1233
     * @return array
1234
     */
1235 75
    private function prepareQueryExpression($expression, $class)
1236
    {
1237 75
        foreach ($expression as $k => $v) {
1238
            // Ignore query operators whose arguments need no type conversion
1239 75
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1240 16
                continue;
1241
            }
1242
1243
            // Process query operators whose argument arrays need type conversion
1244 75
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1245 73
                foreach ($v as $k2 => $v2) {
1246 73
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1247
                }
1248 73
                continue;
1249
            }
1250
1251
            // Recursively process expressions within a $not operator
1252 18
            if ($k === '$not' && is_array($v)) {
1253 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1254 15
                continue;
1255
            }
1256
1257 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1258
        }
1259
1260 75
        return $expression;
1261
    }
1262
1263
    /**
1264
     * Checks whether the value has DBRef fields.
1265
     *
1266
     * This method doesn't check if the the value is a complete DBRef object,
1267
     * although it should return true for a DBRef. Rather, we're checking that
1268
     * the value has one or more fields for a DBref. In practice, this could be
1269
     * $elemMatch criteria for matching a DBRef.
1270
     *
1271
     * @param mixed $value
1272
     * @return boolean
1273
     */
1274 76
    private function hasDBRefFields($value)
1275
    {
1276 76
        if ( ! is_array($value) && ! is_object($value)) {
1277
            return false;
1278
        }
1279
1280 76
        if (is_object($value)) {
1281
            $value = get_object_vars($value);
1282
        }
1283
1284 76
        foreach ($value as $key => $_) {
1285 76
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1286 76
                return true;
1287
            }
1288
        }
1289
1290 75
        return false;
1291
    }
1292
1293
    /**
1294
     * Checks whether the value has query operators.
1295
     *
1296
     * @param mixed $value
1297
     * @return boolean
1298
     */
1299 80
    private function hasQueryOperators($value)
1300
    {
1301 80
        if ( ! is_array($value) && ! is_object($value)) {
1302
            return false;
1303
        }
1304
1305 80
        if (is_object($value)) {
1306
            $value = get_object_vars($value);
1307
        }
1308
1309 80
        foreach ($value as $key => $_) {
1310 80
            if (isset($key[0]) && $key[0] === '$') {
1311 80
                return true;
1312
            }
1313
        }
1314
1315 11
        return false;
1316
    }
1317
1318
    /**
1319
     * Gets the array of discriminator values for the given ClassMetadata
1320
     *
1321
     * @param ClassMetadata $metadata
1322
     * @return array
1323
     */
1324 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1325
    {
1326 29
        $discriminatorValues = array($metadata->discriminatorValue);
1327 29
        foreach ($metadata->subClasses as $className) {
1328 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1329 8
                $discriminatorValues[] = $key;
1330
            }
1331
        }
1332
1333
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1334 29 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1335 2
            $discriminatorValues[] = null;
1336
        }
1337
1338 29
        return $discriminatorValues;
1339
    }
1340
1341 606
    private function handleCollections($document, $options)
1342
    {
1343
        // Collection deletions (deletions of complete collections)
1344 606
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1345 107
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1346 107
                $this->cp->delete($coll, $options);
1347
            }
1348
        }
1349
        // Collection updates (deleteRows, updateRows, insertRows)
1350 606
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1351 107
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1352 107
                $this->cp->update($coll, $options);
1353
            }
1354
        }
1355
        // Take new snapshots from visited collections
1356 606
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1357 260
            $coll->takeSnapshot();
1358
        }
1359 606
    }
1360
1361
    /**
1362
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1363
     * Also, shard key field should be present in actual document data.
1364
     *
1365
     * @param object $document
1366
     * @param string $shardKeyField
1367
     * @param array  $actualDocumentData
1368
     *
1369
     * @throws MongoDBException
1370
     */
1371 3
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1372
    {
1373 3
        $dcs = $this->uow->getDocumentChangeSet($document);
1374 3
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1375
1376 3
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1377 3
        $fieldName = $fieldMapping['fieldName'];
1378
1379 3
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1380
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1381
        }
1382
1383 3
        if (!isset($actualDocumentData[$fieldName])) {
1384
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1385
        }
1386 3
    }
1387
1388
    /**
1389
     * Get shard key aware query for single document.
1390
     *
1391
     * @param object $document
1392
     *
1393
     * @return array
1394
     */
1395 294
    private function getQueryForDocument($document)
1396
    {
1397 294
        $id = $this->uow->getDocumentIdentifier($document);
1398 294
        $id = $this->class->getDatabaseIdentifierValue($id);
1399
1400 294
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1401 294
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1402
1403 294
        return $query;
1404
    }
1405
1406
    /**
1407
     * @param array $options
1408
     *
1409
     * @return array
1410
     */
1411 607
    private function getWriteOptions(array $options = array())
1412
    {
1413 607
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1414 607
        $documentOptions = [];
1415 607
        if ($this->class->hasWriteConcern()) {
1416 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1417
        }
1418
1419 607
        return array_merge($defaultOptions, $documentOptions, $options);
1420
    }
1421
1422
    /**
1423
     * @param string $fieldName
1424
     * @param mixed $value
1425
     * @param array $mapping
1426
     * @param bool $inNewObj
1427
     * @return array
1428
     */
1429 13
    private function prepareDbRefElement($fieldName, $value, array $mapping, $inNewObj)
1430
    {
1431 13
        $dbRef = $this->dm->createDBRef($value, $mapping);
1432 12
        if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1433 8
            return [[$fieldName, $dbRef]];
1434
        }
1435
1436 4
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_REF) {
1437
            $keys = ['id' => true];
1438
        } else {
1439 4
            $keys = ['$ref' => true, '$id' => true, '$db' => true];
1440
1441 4
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF) {
1442
                unset($keys['$db']);
1443
            }
1444
1445 4
            if (isset($mapping['targetDocument'])) {
1446 3
                unset($keys['$ref'], $keys['$db']);
1447
            }
1448
        }
1449
1450 4
        if ($mapping['type'] === 'many') {
1451 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($dbRef, $keys)]]];
1452
        } else {
1453 2
            return array_map(
1454 2
                function ($key) use ($dbRef, $fieldName) {
1455 2
                    return [$fieldName . '.' . $key, $dbRef[$key]];
1456 2
                },
1457 2
                array_keys($keys)
1458
            );
1459
        }
1460
    }
1461
}
1462