Completed
Pull Request — master (#1668)
by Andreas
04:58
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 87
CRAP Score 48.0262

Importance

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

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 769
    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 769
        $this->pb = $pb;
141 769
        $this->dm = $dm;
142 769
        $this->evm = $evm;
143 769
        $this->cm = $cm ?: new CriteriaMerger();
144 769
        $this->uow = $uow;
145 769
        $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 769
        $this->class = $class;
147 769
        $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 769
        $this->cp = $this->uow->getCollectionPersister();
149 769
    }
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 551
    public function addInsert($document)
175
    {
176 551
        $this->queuedInserts[spl_object_hash($document)] = $document;
177 551
    }
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 88
    public function addUpsert($document)
203
    {
204 88
        $this->queuedUpserts[spl_object_hash($document)] = $document;
205 88
    }
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 551
    public function executeInserts(array $options = array())
228
    {
229 551
        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 551
        $inserts = array();
234 551
        $options = $this->getWriteOptions($options);
235 551
        foreach ($this->queuedInserts as $oid => $document) {
236 551
            $data = $this->pb->prepareInsertData($document);
237
238
            // Set the initial version for each insert
239 550 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 550
            $inserts[$oid] = $data;
253
        }
254
255 550
        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 550
                $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 550
        foreach ($this->queuedInserts as $document) {
269 550
            $this->handleCollections($document, $options);
270
        }
271
272 550
        $this->queuedInserts = array();
273 550
    }
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 88
    public function executeUpserts(array $options = array())
285
    {
286 88
        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 88
        $options = $this->getWriteOptions($options);
291 88
        foreach ($this->queuedUpserts as $oid => $document) {
292
            try {
293 88
                $this->executeUpsert($document, $options);
294 88
                $this->handleCollections($document, $options);
295 88
                unset($this->queuedUpserts[$oid]);
296
            } catch (\MongoException $e) {
297
                unset($this->queuedUpserts[$oid]);
298 88
                throw $e;
299
            }
300
        }
301 88
    }
302
303
    /**
304
     * Executes a single upsert in {@link executeUpserts}
305
     *
306
     * @param object $document
307
     * @param array  $options
308
     */
309 88
    private function executeUpsert($document, array $options)
310
    {
311 88
        $options['upsert'] = true;
312 88
        $criteria = $this->getQueryForDocument($document);
313
314 88
        $data = $this->pb->prepareUpsertData($document);
315
316
        // Set the initial version for each upsert
317 88 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 88
        foreach (array_keys($criteria) as $field) {
331 88
            unset($data['$set'][$field]);
332
        }
333
334
        // Do not send an empty $set modifier
335 88
        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 88
        if (empty($data)) {
353 17
            $retry = true;
354 17
            $data = array('$set' => array('_id' => $criteria['_id']));
355
        }
356
357
        try {
358 88
            $this->collection->update($criteria, $data, $options);
359 88
            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
        }
385
386 225
        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 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 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
            }
407
        }
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
                } else {
418 9
                    $query[$lockMapping['name']] = array('$exists' => false);
419
                }
420
            }
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
            }
431
        }
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 35
    public function delete($document, array $options = array())
444
    {
445 35
        $query = $this->getQueryForDocument($document);
446
447 35
        if ($this->class->isLockable) {
448 2
            $query[$this->class->lockField] = array('$exists' => false);
449
        }
450
451 35
        $options = $this->getWriteOptions($options);
452
453 35
        $result = $this->collection->remove($query, $options);
454
455 35
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
456 2
            throw LockException::lockFailed($document);
457
        }
458 33
    }
459
460
    /**
461
     * Refreshes a managed document.
462
     *
463
     * @param object $document The document to refresh.
464
     */
465 22
    public function refresh($document)
466
    {
467 22
        $query = $this->getQueryForDocument($document);
468 22
        $data = $this->collection->findOne($query);
469 22
        $data = $this->hydratorFactory->hydrate($document, $data);
470 22
        $this->uow->setOriginalDocumentData($document, $data);
471 22
    }
472
473
    /**
474
     * Finds a document by a set of criteria.
475
     *
476
     * If a scalar or MongoId is provided for $criteria, it will be used to
477
     * match an _id value.
478
     *
479
     * @param mixed   $criteria Query criteria
480
     * @param object  $document Document to load the data into. If not specified, a new document is created.
481
     * @param array   $hints    Hints for document creation
482
     * @param integer $lockMode
483
     * @param array   $sort     Sort array for Cursor::sort()
484
     * @throws \Doctrine\ODM\MongoDB\LockException
485
     * @return object|null The loaded and managed document instance or null if no document was found
486
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
487
     */
488 384
    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...
489
    {
490
        // TODO: remove this
491 384
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
492
            $criteria = array('_id' => $criteria);
493
        }
494
495 384
        $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...
496 384
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
497 384
        $criteria = $this->addFilterToPreparedQuery($criteria);
498
499 384
        $cursor = $this->collection->find($criteria);
500
501 384
        if (null !== $sort) {
502 105
            $cursor->sort($this->prepareSortOrProjection($sort));
503
        }
504
505 384
        $result = $cursor->getSingleResult();
506
507 384
        if ($this->class->isLockable) {
508 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
509 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
510 1
                throw LockException::lockFailed($result);
511
            }
512
        }
513
514 383
        return $this->createDocument($result, $document, $hints);
515
    }
516
517
    /**
518
     * Finds documents by a set of criteria.
519
     *
520
     * @param array        $criteria Query criteria
521
     * @param array        $sort     Sort array for Cursor::sort()
522
     * @param integer|null $limit    Limit for Cursor::limit()
523
     * @param integer|null $skip     Skip for Cursor::skip()
524
     * @return Cursor
525
     */
526 26
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
527
    {
528 26
        $criteria = $this->prepareQueryOrNewObj($criteria);
529 26
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
530 26
        $criteria = $this->addFilterToPreparedQuery($criteria);
531
532 26
        $baseCursor = $this->collection->find($criteria);
533 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...
534
535 26
        if (null !== $sort) {
536 3
            $cursor->sort($sort);
537
        }
538
539 26
        if (null !== $limit) {
540 2
            $cursor->limit($limit);
541
        }
542
543 26
        if (null !== $skip) {
544 2
            $cursor->skip($skip);
545
        }
546
547 26
        return $cursor;
548
    }
549
550
    /**
551
     * @param object $document
552
     *
553
     * @return array
554
     * @throws MongoDBException
555
     */
556 307
    private function getShardKeyQuery($document)
557
    {
558 307
        if ( ! $this->class->isSharded()) {
559 298
            return array();
560
        }
561
562 9
        $shardKey = $this->class->getShardKey();
563 9
        $keys = array_keys($shardKey['keys']);
564 9
        $data = $this->uow->getDocumentActualData($document);
565
566 9
        $shardKeyQueryPart = array();
567 9
        foreach ($keys as $key) {
568 9
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
569 9
            $this->guardMissingShardKey($document, $key, $data);
570 7
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
571 7
            $shardKeyQueryPart[$key] = $value;
572
        }
573
574 7
        return $shardKeyQueryPart;
575
    }
576
577
    /**
578
     * Wraps the supplied base cursor in the corresponding ODM class.
579
     *
580
     * @param CursorInterface $baseCursor
581
     * @return Cursor
582
     */
583 26
    private function wrapCursor(CursorInterface $baseCursor)
584
    {
585 26
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
586
    }
587
588
    /**
589
     * Checks whether the given managed document exists in the database.
590
     *
591
     * @param object $document
592
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
593
     */
594 3
    public function exists($document)
595
    {
596 3
        $id = $this->class->getIdentifierObject($document);
597 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
598
    }
599
600
    /**
601
     * Locks document by storing the lock mode on the mapped lock field.
602
     *
603
     * @param object $document
604
     * @param int $lockMode
605
     */
606 5
    public function lock($document, $lockMode)
607
    {
608 5
        $id = $this->uow->getDocumentIdentifier($document);
609 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
610 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
611 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
612 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
613 5
    }
614
615
    /**
616
     * Releases any lock that exists on this document.
617
     *
618
     * @param object $document
619
     */
620 1
    public function unlock($document)
621
    {
622 1
        $id = $this->uow->getDocumentIdentifier($document);
623 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
624 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
625 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
626 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
627 1
    }
628
629
    /**
630
     * Creates or fills a single document object from an query result.
631
     *
632
     * @param object $result The query result.
633
     * @param object $document The document object to fill, if any.
634
     * @param array $hints Hints for document creation.
635
     * @return object The filled and managed document object or NULL, if the query result is empty.
636
     */
637 383
    private function createDocument($result, $document = null, array $hints = array())
638
    {
639 383
        if ($result === null) {
640 126
            return null;
641
        }
642
643 330
        if ($document !== null) {
644 38
            $hints[Query::HINT_REFRESH] = true;
645 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
646 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...
647
        }
648
649 330
        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...
650
    }
651
652
    /**
653
     * Loads a PersistentCollection data. Used in the initialize() method.
654
     *
655
     * @param PersistentCollectionInterface $collection
656
     */
657 173
    public function loadCollection(PersistentCollectionInterface $collection)
658
    {
659 173
        $mapping = $collection->getMapping();
660 173
        switch ($mapping['association']) {
661 173
            case ClassMetadata::EMBED_MANY:
662 119
                $this->loadEmbedManyCollection($collection);
663 119
                break;
664
665 71
            case ClassMetadata::REFERENCE_MANY:
666 71
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
667 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
668
                } else {
669 66
                    if ($mapping['isOwningSide']) {
670 55
                        $this->loadReferenceManyCollectionOwningSide($collection);
671
                    } else {
672 15
                        $this->loadReferenceManyCollectionInverseSide($collection);
673
                    }
674
                }
675 70
                break;
676
        }
677 172
    }
678
679 119
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
680
    {
681 119
        $embeddedDocuments = $collection->getMongoData();
682 119
        $mapping = $collection->getMapping();
683 119
        $owner = $collection->getOwner();
684 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...
685 90
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
686 90
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
687 90
                $embeddedMetadata = $this->dm->getClassMetadata($className);
688 90
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
689
690 90
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
691
692 90
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
693 90
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
694 23
                    ? $data[$embeddedMetadata->identifier]
695 90
                    : null;
696
697 90
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
698 89
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
699
                }
700 90
                if (CollectionHelper::isHash($mapping['strategy'])) {
701 25
                    $collection->set($key, $embeddedDocumentObject);
702
                } else {
703 90
                    $collection->add($embeddedDocumentObject);
704
                }
705
            }
706
        }
707 119
    }
708
709 55
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
710
    {
711 55
        $hints = $collection->getHints();
712 55
        $mapping = $collection->getMapping();
713 55
        $groupedIds = array();
714
715 55
        $sorted = isset($mapping['sort']) && $mapping['sort'];
716
717 55
        foreach ($collection->getMongoData() as $key => $reference) {
718 50
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
719 50
            $mongoId = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
720 50
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
721
722
            // create a reference to the class and id
723 50
            $reference = $this->dm->getReference($className, $id);
724
725
            // no custom sort so add the references right now in the order they are embedded
726 50
            if ( ! $sorted) {
727 49
                if (CollectionHelper::isHash($mapping['strategy'])) {
728 2
                    $collection->set($key, $reference);
729
                } else {
730 47
                    $collection->add($reference);
731
                }
732
            }
733
734
            // only query for the referenced object if it is not already initialized or the collection is sorted
735 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...
736 50
                $groupedIds[$className][] = $mongoId;
737
            }
738
        }
739 55
        foreach ($groupedIds as $className => $ids) {
740 35
            $class = $this->dm->getClassMetadata($className);
741 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
742 35
            $criteria = $this->cm->merge(
743 35
                array('_id' => array('$in' => array_values($ids))),
744 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
745 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
746
            );
747 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
748 35
            $cursor = $mongoCollection->find($criteria);
749 35
            if (isset($mapping['sort'])) {
750 35
                $cursor->sort($mapping['sort']);
751
            }
752 35
            if (isset($mapping['limit'])) {
753
                $cursor->limit($mapping['limit']);
754
            }
755 35
            if (isset($mapping['skip'])) {
756
                $cursor->skip($mapping['skip']);
757
            }
758 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...
759
                $cursor->slaveOkay(true);
760
            }
761 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...
762
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
763
            }
764 35
            $documents = $cursor->toArray(false);
765 35
            foreach ($documents as $documentData) {
766 34
                $document = $this->uow->getById($documentData['_id'], $class);
767 34
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
768 34
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
769 34
                    $this->uow->setOriginalDocumentData($document, $data);
770 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...
771
                }
772 34
                if ($sorted) {
773 35
                    $collection->add($document);
774
                }
775
            }
776
        }
777 55
    }
778
779 15
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
780
    {
781 15
        $query = $this->createReferenceManyInverseSideQuery($collection);
782 15
        $documents = $query->execute()->toArray(false);
783 15
        foreach ($documents as $key => $document) {
784 14
            $collection->add($document);
785
        }
786 15
    }
787
788
    /**
789
     * @param PersistentCollectionInterface $collection
790
     *
791
     * @return Query
792
     */
793 18
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
794
    {
795 18
        $hints = $collection->getHints();
796 18
        $mapping = $collection->getMapping();
797 18
        $owner = $collection->getOwner();
798 18
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
799 18
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
800 18
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
801 18
        $mappedByFieldName = ClassMetadataInfo::getReferenceFieldName(isset($mappedByMapping['storeAs']) ? $mappedByMapping['storeAs'] : ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
802
803 18
        $criteria = $this->cm->merge(
804 18
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
805 18
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
806 18
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
807
        );
808 18
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
809 18
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
810 18
            ->setQueryArray($criteria);
811
812 18
        if (isset($mapping['sort'])) {
813 18
            $qb->sort($mapping['sort']);
814
        }
815 18
        if (isset($mapping['limit'])) {
816 2
            $qb->limit($mapping['limit']);
817
        }
818 18
        if (isset($mapping['skip'])) {
819
            $qb->skip($mapping['skip']);
820
        }
821 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...
822
            $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...
823
        }
824 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...
825
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
826
        }
827 18
        foreach ($mapping['prime'] as $field) {
828 4
            $qb->field($field)->prime(true);
829
        }
830
831 18
        return $qb->getQuery();
832
    }
833
834 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
835
    {
836 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
837 4
        $mapping = $collection->getMapping();
838 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...
839 4
        foreach ($documents as $key => $obj) {
840 4
            if (CollectionHelper::isHash($mapping['strategy'])) {
841 1
                $collection->set($key, $obj);
842
            } else {
843 4
                $collection->add($obj);
844
            }
845
        }
846 4
    }
847
848
    /**
849
     * @param PersistentCollectionInterface $collection
850
     *
851
     * @return CursorInterface
852
     */
853 6
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
854
    {
855 6
        $hints = $collection->getHints();
856 6
        $mapping = $collection->getMapping();
857 6
        $repositoryMethod = $mapping['repositoryMethod'];
858 6
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
859 6
            ->$repositoryMethod($collection->getOwner());
860
861 6
        if ( ! $cursor instanceof CursorInterface) {
862
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
863
        }
864
865 6
        if (!empty($mapping['prime'])) {
866 2
            if (!$cursor instanceof Cursor) {
867
                throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a Cursor to allow for priming");
868
            }
869
870 2
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
871 2
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
872
873 2
            $cursor->enableReferencePriming($primers, $referencePrimer);
874
        }
875
876 5
        if (isset($mapping['sort'])) {
877 5
            $cursor->sort($mapping['sort']);
878
        }
879 5
        if (isset($mapping['limit'])) {
880 1
            $cursor->limit($mapping['limit']);
881
        }
882 5
        if (isset($mapping['skip'])) {
883
            $cursor->skip($mapping['skip']);
884
        }
885 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...
886
            $cursor->slaveOkay(true);
887
        }
888 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...
889
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
890
        }
891
892 5
        return $cursor;
893
    }
894
895
    /**
896
     * Prepare a sort or projection array by converting keys, which are PHP
897
     * property names, to MongoDB field names.
898
     *
899
     * @param array $fields
900
     * @return array
901
     */
902 140
    public function prepareSortOrProjection(array $fields)
903
    {
904 140
        $preparedFields = array();
905
906 140
        foreach ($fields as $key => $value) {
907 38
            $preparedFields[$this->prepareFieldName($key)] = $value;
908
        }
909
910 140
        return $preparedFields;
911
    }
912
913
    /**
914
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
915
     *
916
     * @param string $fieldName
917
     * @return string
918
     */
919 89
    public function prepareFieldName($fieldName)
920
    {
921 89
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
922
923 89
        return $fieldNames[0][0];
924
    }
925
926
    /**
927
     * Adds discriminator criteria to an already-prepared query.
928
     *
929
     * This method should be used once for query criteria and not be used for
930
     * nested expressions. It should be called before
931
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
932
     *
933
     * @param array $preparedQuery
934
     * @return array
935
     */
936 521
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
937
    {
938
        /* If the class has a discriminator field, which is not already in the
939
         * criteria, inject it now. The field/values need no preparation.
940
         */
941 521
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
942 29
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
943 29
            if (count($discriminatorValues) === 1) {
944 21
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
945
            } else {
946 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
947
            }
948
        }
949
950 521
        return $preparedQuery;
951
    }
952
953
    /**
954
     * Adds filter criteria to an already-prepared query.
955
     *
956
     * This method should be used once for query criteria and not be used for
957
     * nested expressions. It should be called after
958
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
959
     *
960
     * @param array $preparedQuery
961
     * @return array
962
     */
963 522
    public function addFilterToPreparedQuery(array $preparedQuery)
964
    {
965
        /* If filter criteria exists for this class, prepare it and merge
966
         * over the existing query.
967
         *
968
         * @todo Consider recursive merging in case the filter criteria and
969
         * prepared query both contain top-level $and/$or operators.
970
         */
971 522
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
972 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
973
        }
974
975 522
        return $preparedQuery;
976
    }
977
978
    /**
979
     * Prepares the query criteria or new document object.
980
     *
981
     * PHP field names and types will be converted to those used by MongoDB.
982
     *
983
     * @param array $query
984
     * @param bool $isNewObj
985
     * @return array
986
     */
987 546
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
988
    {
989 546
        $preparedQuery = array();
990
991 546
        foreach ($query as $key => $value) {
992
            // Recursively prepare logical query clauses
993 505
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
994 16
                foreach ($value as $k2 => $v2) {
995 16
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
996
                }
997 16
                continue;
998
            }
999
1000 505
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1001 26
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1002 26
                continue;
1003
            }
1004
1005 505
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj);
1006 505
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1007 505
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1008 125
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1009 505
                    : Type::convertPHPToDatabaseValue($preparedValue);
1010
            }
1011
        }
1012
1013 546
        return $preparedQuery;
1014
    }
1015
1016
    /**
1017
     * Prepares a query value and converts the PHP value to the database value
1018
     * if it is an identifier.
1019
     *
1020
     * It also handles converting $fieldName to the database name if they are different.
1021
     *
1022
     * @param string $fieldName
1023
     * @param mixed $value
1024
     * @param ClassMetadata $class        Defaults to $this->class
1025
     * @param bool $prepareValue Whether or not to prepare the value
1026
     * @param bool $inNewObj Whether or not newObj is being prepared
1027
     * @return array An array of tuples containing prepared field names and values
1028
     */
1029 551
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1030
    {
1031 551
        $class = isset($class) ? $class : $this->class;
1032
1033
        // @todo Consider inlining calls to ClassMetadataInfo methods
1034
1035
        // Process all non-identifier fields by translating field names
1036 551
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1037 250
            $mapping = $class->fieldMappings[$fieldName];
1038 250
            $fieldName = $mapping['name'];
1039
1040 250
            if ( ! $prepareValue) {
1041 57
                return [[$fieldName, $value]];
1042
            }
1043
1044
            // Prepare mapped, embedded objects
1045 207
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1046 207
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1047 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1048
            }
1049
1050 205
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1051
                try {
1052 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1053 1
                } catch (MappingException $e) {
1054
                    // do nothing in case passed object is not mapped document
1055
                }
1056
            }
1057
1058
            // No further preparation unless we're dealing with a simple reference
1059
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1060 192
            $arrayValue = (array) $value;
1061 192
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1062 116
                return [[$fieldName, $value]];
1063
            }
1064
1065
            // Additional preparation for one or more simple reference values
1066 103
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1067
1068 103
            if ( ! is_array($value)) {
1069 99
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1070
            }
1071
1072
            // Objects without operators or with DBRef fields can be converted immediately
1073 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...
1074 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1075
            }
1076
1077 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1078
        }
1079
1080
        // Process identifier fields
1081 459
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1082 351
            $fieldName = '_id';
1083
1084 351
            if ( ! $prepareValue) {
1085 30
                return [[$fieldName, $value]];
1086
            }
1087
1088 324
            if ( ! is_array($value)) {
1089 301
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1090
            }
1091
1092
            // Objects without operators or with DBRef fields can be converted immediately
1093 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...
1094 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1095
            }
1096
1097 53
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1098
        }
1099
1100
        // No processing for unmapped, non-identifier, non-dotted field names
1101 200
        if (strpos($fieldName, '.') === false) {
1102 49
            return [[$fieldName, $value]];
1103
        }
1104
1105
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1106
         *
1107
         * We can limit parsing here, since at most three segments are
1108
         * significant: "fieldName.objectProperty" with an optional index or key
1109
         * for collections stored as either BSON arrays or objects.
1110
         */
1111 160
        $e = explode('.', $fieldName, 4);
1112
1113
        // No further processing for unmapped fields
1114 160
        if ( ! isset($class->fieldMappings[$e[0]])) {
1115 3
            return [[$fieldName, $value]];
1116
        }
1117
1118 158
        $mapping = $class->fieldMappings[$e[0]];
1119 158
        $e[0] = $mapping['name'];
1120
1121
        // Hash and raw fields will not be prepared beyond the field name
1122 158
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1123 1
            $fieldName = implode('.', $e);
1124
1125 1
            return [[$fieldName, $value]];
1126
        }
1127
1128 157
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1129 157
                && isset($e[2])) {
1130 1
            $objectProperty = $e[2];
1131 1
            $objectPropertyPrefix = $e[1] . '.';
1132 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1133 156
        } elseif ($e[1] != '$') {
1134 155
            $fieldName = $e[0] . '.' . $e[1];
1135 155
            $objectProperty = $e[1];
1136 155
            $objectPropertyPrefix = '';
1137 155
            $nextObjectProperty = implode('.', array_slice($e, 2));
1138 1
        } elseif (isset($e[2])) {
1139 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1140 1
            $objectProperty = $e[2];
1141 1
            $objectPropertyPrefix = $e[1] . '.';
1142 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1143
        } else {
1144 1
            $fieldName = $e[0] . '.' . $e[1];
1145
1146 1
            return [[$fieldName, $value]];
1147
        }
1148
1149
        // No further processing for fields without a targetDocument mapping
1150 157
        if ( ! isset($mapping['targetDocument'])) {
1151 3
            if ($nextObjectProperty) {
1152
                $fieldName .= '.'.$nextObjectProperty;
1153
            }
1154
1155 3
            return [[$fieldName, $value]];
1156
        }
1157
1158 154
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1159
1160
        // No further processing for unmapped targetDocument fields
1161 154
        if ( ! $targetClass->hasField($objectProperty)) {
1162 27
            if ($nextObjectProperty) {
1163
                $fieldName .= '.'.$nextObjectProperty;
1164
            }
1165
1166 27
            return [[$fieldName, $value]];
1167
        }
1168
1169 132
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1170 132
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1171
1172
        // Prepare DBRef identifiers or the mapped field's property path
1173 132
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1174 114
            ? ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $e[0])
1175 132
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1176
1177
        // Process targetDocument identifier fields
1178 132
        if ($objectPropertyIsId) {
1179 115
            if ( ! $prepareValue) {
1180 4
                return [[$fieldName, $value]];
1181
            }
1182
1183 111
            if ( ! is_array($value)) {
1184 97
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1185
            }
1186
1187
            // Objects without operators or with DBRef fields can be converted immediately
1188 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...
1189 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1190
            }
1191
1192 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1193
        }
1194
1195
        /* The property path may include a third field segment, excluding the
1196
         * collection item pointer. If present, this next object property must
1197
         * be processed recursively.
1198
         */
1199 17
        if ($nextObjectProperty) {
1200
            // Respect the targetDocument's class metadata when recursing
1201 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1202 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1203 14
                : null;
1204
1205 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1206
1207
            return array_map(function ($preparedTuple) use ($fieldName) {
1208 14
                list($key, $value) = $preparedTuple;
1209
1210 14
                return [$fieldName . '.' . $key, $value];
1211 14
            }, $fieldNames);
1212
        }
1213
1214 5
        return [[$fieldName, $value]];
1215
    }
1216
1217
    /**
1218
     * Prepares a query expression.
1219
     *
1220
     * @param array|object  $expression
1221
     * @param ClassMetadata $class
1222
     * @return array
1223
     */
1224 75
    private function prepareQueryExpression($expression, $class)
1225
    {
1226 75
        foreach ($expression as $k => $v) {
1227
            // Ignore query operators whose arguments need no type conversion
1228 75
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1229 16
                continue;
1230
            }
1231
1232
            // Process query operators whose argument arrays need type conversion
1233 75
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1234 73
                foreach ($v as $k2 => $v2) {
1235 73
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1236
                }
1237 73
                continue;
1238
            }
1239
1240
            // Recursively process expressions within a $not operator
1241 18
            if ($k === '$not' && is_array($v)) {
1242 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1243 15
                continue;
1244
            }
1245
1246 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1247
        }
1248
1249 75
        return $expression;
1250
    }
1251
1252
    /**
1253
     * Checks whether the value has DBRef fields.
1254
     *
1255
     * This method doesn't check if the the value is a complete DBRef object,
1256
     * although it should return true for a DBRef. Rather, we're checking that
1257
     * the value has one or more fields for a DBref. In practice, this could be
1258
     * $elemMatch criteria for matching a DBRef.
1259
     *
1260
     * @param mixed $value
1261
     * @return boolean
1262
     */
1263 76
    private function hasDBRefFields($value)
1264
    {
1265 76
        if ( ! is_array($value) && ! is_object($value)) {
1266
            return false;
1267
        }
1268
1269 76
        if (is_object($value)) {
1270
            $value = get_object_vars($value);
1271
        }
1272
1273 76
        foreach ($value as $key => $_) {
1274 76
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1275 76
                return true;
1276
            }
1277
        }
1278
1279 75
        return false;
1280
    }
1281
1282
    /**
1283
     * Checks whether the value has query operators.
1284
     *
1285
     * @param mixed $value
1286
     * @return boolean
1287
     */
1288 80
    private function hasQueryOperators($value)
1289
    {
1290 80
        if ( ! is_array($value) && ! is_object($value)) {
1291
            return false;
1292
        }
1293
1294 80
        if (is_object($value)) {
1295
            $value = get_object_vars($value);
1296
        }
1297
1298 80
        foreach ($value as $key => $_) {
1299 80
            if (isset($key[0]) && $key[0] === '$') {
1300 80
                return true;
1301
            }
1302
        }
1303
1304 11
        return false;
1305
    }
1306
1307
    /**
1308
     * Gets the array of discriminator values for the given ClassMetadata
1309
     *
1310
     * @param ClassMetadata $metadata
1311
     * @return array
1312
     */
1313 29
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1314
    {
1315 29
        $discriminatorValues = array($metadata->discriminatorValue);
1316 29
        foreach ($metadata->subClasses as $className) {
1317 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1318 8
                $discriminatorValues[] = $key;
1319
            }
1320
        }
1321
1322
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1323 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...
1324 2
            $discriminatorValues[] = null;
1325
        }
1326
1327 29
        return $discriminatorValues;
1328
    }
1329
1330 627
    private function handleCollections($document, $options)
1331
    {
1332
        // Collection deletions (deletions of complete collections)
1333 627
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1334 107
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1335 107
                $this->cp->delete($coll, $options);
1336
            }
1337
        }
1338
        // Collection updates (deleteRows, updateRows, insertRows)
1339 627
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1340 107
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1341 107
                $this->cp->update($coll, $options);
1342
            }
1343
        }
1344
        // Take new snapshots from visited collections
1345 627
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1346 266
            $coll->takeSnapshot();
1347
        }
1348 627
    }
1349
1350
    /**
1351
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1352
     * Also, shard key field should be present in actual document data.
1353
     *
1354
     * @param object $document
1355
     * @param string $shardKeyField
1356
     * @param array  $actualDocumentData
1357
     *
1358
     * @throws MongoDBException
1359
     */
1360 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1361
    {
1362 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1363 9
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1364
1365 9
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1366 9
        $fieldName = $fieldMapping['fieldName'];
1367
1368 9
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1369 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1370
        }
1371
1372 7
        if (!isset($actualDocumentData[$fieldName])) {
1373
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1374
        }
1375 7
    }
1376
1377
    /**
1378
     * Get shard key aware query for single document.
1379
     *
1380
     * @param object $document
1381
     *
1382
     * @return array
1383
     */
1384 304
    private function getQueryForDocument($document)
1385
    {
1386 304
        $id = $this->uow->getDocumentIdentifier($document);
1387 304
        $id = $this->class->getDatabaseIdentifierValue($id);
1388
1389 304
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1390 302
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1391
1392 302
        return $query;
1393
    }
1394
1395
    /**
1396
     * @param array $options
1397
     *
1398
     * @return array
1399
     */
1400 629
    private function getWriteOptions(array $options = array())
1401
    {
1402 629
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1403 629
        $documentOptions = [];
1404 629
        if ($this->class->hasWriteConcern()) {
1405 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1406
        }
1407
1408 629
        return array_merge($defaultOptions, $documentOptions, $options);
1409
    }
1410
1411
    /**
1412
     * @param string $fieldName
1413
     * @param mixed $value
1414
     * @param array $mapping
1415
     * @param bool $inNewObj
1416
     * @return array
1417
     */
1418 14
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1419
    {
1420 14
        $reference = $this->dm->createReference($value, $mapping);
1421 13
        if ($inNewObj || $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1422 8
            return [[$fieldName, $reference]];
1423
        }
1424
1425 5
        switch ($mapping['storeAs']) {
1426 5
            case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
1427
                $keys = ['id' => true];
1428
                break;
1429
1430 5
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF:
1431 1
            case ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1432 5
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1433
1434 5
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_DB_REF) {
1435 4
                    unset($keys['$db']);
1436
                }
1437
1438 5
                if (isset($mapping['targetDocument'])) {
1439 3
                    unset($keys['$ref'], $keys['$db']);
1440
                }
1441 5
                break;
1442
1443
            default:
1444
                throw new \InvalidArgumentException("Reference type {$mapping['storeAs']} is invalid.");
1445
        }
1446
1447 5
        if ($mapping['type'] === 'many') {
1448 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1449
        } else {
1450 3
            return array_map(
1451 3
                function ($key) use ($reference, $fieldName) {
1452 3
                    return [$fieldName . '.' . $key, $reference[$key]];
1453 3
                },
1454 3
                array_keys($keys)
1455
            );
1456
        }
1457
    }
1458
}
1459