Completed
Pull Request — master (#1385)
by Andreas
08:02
created

DocumentPersister::prepareQueryOrNewObj()   D

Complexity

Conditions 9
Paths 6

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 9
Metric Value
dl 0
loc 27
ccs 15
cts 15
cp 1
rs 4.909
cc 9
eloc 15
nc 6
nop 1
crap 9
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\Common\Persistence\Mapping\MappingException;
24
use Doctrine\MongoDB\CursorInterface;
25
use Doctrine\ODM\MongoDB\Cursor;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
28
use Doctrine\ODM\MongoDB\LockException;
29
use Doctrine\ODM\MongoDB\LockMode;
30
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
31
use Doctrine\ODM\MongoDB\MongoDBException;
32
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
33
use Doctrine\ODM\MongoDB\Proxy\Proxy;
34
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
35
use Doctrine\ODM\MongoDB\Query\Query;
36
use Doctrine\ODM\MongoDB\Types\Type;
37
use Doctrine\ODM\MongoDB\UnitOfWork;
38
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
39
40
/**
41
 * The DocumentPersister is responsible for persisting documents.
42
 *
43
 * @since       1.0
44
 */
45
class DocumentPersister
46
{
47
    /**
48
     * The PersistenceBuilder instance.
49
     *
50
     * @var PersistenceBuilder
51
     */
52
    private $pb;
53
54
    /**
55
     * The DocumentManager instance.
56
     *
57
     * @var DocumentManager
58
     */
59
    private $dm;
60
61
    /**
62
     * The EventManager instance
63
     *
64
     * @var EventManager
65
     */
66
    private $evm;
67
68
    /**
69
     * The UnitOfWork instance.
70
     *
71
     * @var UnitOfWork
72
     */
73
    private $uow;
74
75
    /**
76
     * The ClassMetadata instance for the document type being persisted.
77
     *
78
     * @var ClassMetadata
79
     */
80
    private $class;
81
82
    /**
83
     * The MongoCollection instance for this document.
84
     *
85
     * @var \MongoCollection
86
     */
87
    private $collection;
88
89
    /**
90
     * Array of queued inserts for the persister to insert.
91
     *
92
     * @var array
93
     */
94
    private $queuedInserts = array();
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedUpserts = array();
102
103
    /**
104
     * The CriteriaMerger instance.
105
     *
106
     * @var CriteriaMerger
107
     */
108
    private $cm;
109
110
    /**
111
     * The CollectionPersister instance.
112
     *
113
     * @var CollectionPersister
114
     */
115
    private $cp;
116
117
    /**
118
     * Initializes this instance.
119
     *
120
     * @param PersistenceBuilder $pb
121
     * @param DocumentManager $dm
122
     * @param EventManager $evm
123
     * @param UnitOfWork $uow
124
     * @param HydratorFactory $hydratorFactory
125
     * @param ClassMetadata $class
126
     * @param CriteriaMerger $cm
127
     */
128 693
    public function __construct(
129
        PersistenceBuilder $pb,
130
        DocumentManager $dm,
131
        EventManager $evm,
132
        UnitOfWork $uow,
133
        HydratorFactory $hydratorFactory,
134
        ClassMetadata $class,
135
        CriteriaMerger $cm = null
136
    ) {
137 693
        $this->pb = $pb;
138 693
        $this->dm = $dm;
139 693
        $this->evm = $evm;
140 693
        $this->cm = $cm ?: new CriteriaMerger();
141 693
        $this->uow = $uow;
142 693
        $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...
143 693
        $this->class = $class;
144 693
        $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...
145 693
        $this->cp = $this->uow->getCollectionPersister();
146 693
    }
147
148
    /**
149
     * @return array
150
     */
151
    public function getInserts()
152
    {
153
        return $this->queuedInserts;
154
    }
155
156
    /**
157
     * @param object $document
158
     * @return bool
159
     */
160
    public function isQueuedForInsert($document)
161
    {
162
        return isset($this->queuedInserts[spl_object_hash($document)]);
163
    }
164
165
    /**
166
     * Adds a document to the queued insertions.
167
     * The document remains queued until {@link executeInserts} is invoked.
168
     *
169
     * @param object $document The document to queue for insertion.
170
     */
171 492
    public function addInsert($document)
172
    {
173 492
        $this->queuedInserts[spl_object_hash($document)] = $document;
174 492
    }
175
176
    /**
177
     * @return array
178
     */
179
    public function getUpserts()
180
    {
181
        return $this->queuedUpserts;
182
    }
183
184
    /**
185
     * @param object $document
186
     * @return boolean
187
     */
188
    public function isQueuedForUpsert($document)
189
    {
190
        return isset($this->queuedUpserts[spl_object_hash($document)]);
191
    }
192
193
    /**
194
     * Adds a document to the queued upserts.
195
     * The document remains queued until {@link executeUpserts} is invoked.
196
     *
197
     * @param object $document The document to queue for insertion.
198
     */
199 76
    public function addUpsert($document)
200
    {
201 76
        $this->queuedUpserts[spl_object_hash($document)] = $document;
202 76
    }
203
204
    /**
205
     * Gets the ClassMetadata instance of the document class this persister is used for.
206
     *
207
     * @return ClassMetadata
208
     */
209
    public function getClassMetadata()
210
    {
211
        return $this->class;
212
    }
213
214
    /**
215
     * Executes all queued document insertions.
216
     *
217
     * Queued documents without an ID will inserted in a batch and queued
218
     * documents with an ID will be upserted individually.
219
     *
220
     * If no inserts are queued, invoking this method is a NOOP.
221
     *
222
     * @param array $options Options for batchInsert() and update() driver methods
223
     */
224 492
    public function executeInserts(array $options = array())
225
    {
226 492
        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...
227
            return;
228
        }
229
230 492
        $inserts = array();
231 492
        foreach ($this->queuedInserts as $oid => $document) {
232 492
            $data = $this->pb->prepareInsertData($document);
233
234
            // Set the initial version for each insert
235 491 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...
236 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
237 39
                if ($versionMapping['type'] === 'int') {
238 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
239 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
240 2
                } elseif ($versionMapping['type'] === 'date') {
241 2
                    $nextVersionDateTime = new \DateTime();
242 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
243 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
244
                }
245 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...
246
            }
247
248 491
            $inserts[$oid] = $data;
249
        }
250
251 491
        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...
252
            try {
253 491
                $this->collection->batchInsert($inserts, $options);
254 7
            } catch (\MongoException $e) {
255 7
                $this->queuedInserts = array();
256 7
                throw $e;
257
            }
258
        }
259
260
        /* All collections except for ones using addToSet have already been
261
         * saved. We have left these to be handled separately to avoid checking
262
         * collection for uniqueness on PHP side.
263
         */
264 491
        foreach ($this->queuedInserts as $document) {
265 491
            $this->handleCollections($document, $options);
266
        }
267
268 491
        $this->queuedInserts = array();
269 491
    }
270
271
    /**
272
     * Executes all queued document upserts.
273
     *
274
     * Queued documents with an ID are upserted individually.
275
     *
276
     * If no upserts are queued, invoking this method is a NOOP.
277
     *
278
     * @param array $options Options for batchInsert() and update() driver methods
279
     */
280 76
    public function executeUpserts(array $options = array())
281
    {
282 76
        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...
283
            return;
284
        }
285
286 76
        foreach ($this->queuedUpserts as $oid => $document) {
287
            try {
288 76
                $this->executeUpsert($document, $options);
289 76
                $this->handleCollections($document, $options);
290 76
                unset($this->queuedUpserts[$oid]);
291
            } catch (\MongoException $e) {
292
                unset($this->queuedUpserts[$oid]);
293 76
                throw $e;
294
            }
295
        }
296 76
    }
297
298
    /**
299
     * Executes a single upsert in {@link executeUpserts}
300
     *
301
     * @param object $document
302
     * @param array  $options
303
     */
304 76
    private function executeUpsert($document, array $options)
305
    {
306 76
        $options['upsert'] = true;
307 76
        $criteria = $this->getQueryForDocument($document);
308
309 76
        $data = $this->pb->prepareUpsertData($document);
310
311
        // Set the initial version for each upsert
312 76 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...
313 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
314 3
            if ($versionMapping['type'] === 'int') {
315 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
316 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
317 1
            } elseif ($versionMapping['type'] === 'date') {
318 1
                $nextVersionDateTime = new \DateTime();
319 1
                $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
320 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
321
            }
322 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...
323
        }
324
325 76
        foreach (array_keys($criteria) as $field) {
326 76
            unset($data['$set'][$field]);
327
        }
328
329
        // Do not send an empty $set modifier
330 76
        if (empty($data['$set'])) {
331 13
            unset($data['$set']);
332
        }
333
334
        /* If there are no modifiers remaining, we're upserting a document with
335
         * an identifier as its only field. Since a document with the identifier
336
         * may already exist, the desired behavior is "insert if not exists" and
337
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
338
         * the identifier to the same value in our criteria.
339
         *
340
         * This will fail for versions before MongoDB 2.6, which require an
341
         * empty $set modifier. The best we can do (without attempting to check
342
         * server versions in advance) is attempt the 2.6+ behavior and retry
343
         * after the relevant exception.
344
         *
345
         * See: https://jira.mongodb.org/browse/SERVER-12266
346
         */
347 76
        if (empty($data)) {
348 13
            $retry = true;
349 13
            $data = array('$set' => array('_id' => $criteria['_id']));
350
        }
351
352
        try {
353 76
            $this->collection->update($criteria, $data, $options);
354 64
            return;
355 13
        } catch (\MongoCursorException $e) {
356 13
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
357
                throw $e;
358
            }
359
        }
360
361 13
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
362 13
    }
363
364
    /**
365
     * Updates the already persisted document if it has any new changesets.
366
     *
367
     * @param object $document
368
     * @param array $options Array of options to be used with update()
369
     * @throws \Doctrine\ODM\MongoDB\LockException
370
     */
371 214
    public function update($document, array $options = array())
372
    {
373 214
        $update = $this->pb->prepareUpdateData($document);
374
375 214
        $query = $this->getQueryForDocument($document);
376
377 214
        foreach (array_keys($query) as $field) {
378 214
            unset($update['$set'][$field]);
379
        }
380
381 214
        if (empty($update['$set'])) {
382 89
            unset($update['$set']);
383
        }
384
385
386
        // Include versioning logic to set the new version value in the database
387
        // and to ensure the version has not changed since this document object instance
388
        // was fetched from the database
389 214
        if ($this->class->isVersioned) {
390 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
391 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
392 31
            if ($versionMapping['type'] === 'int') {
393 28
                $nextVersion = $currentVersion + 1;
394 28
                $update['$inc'][$versionMapping['name']] = 1;
395 28
                $query[$versionMapping['name']] = $currentVersion;
396 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
397 3
            } elseif ($versionMapping['type'] === 'date') {
398 3
                $nextVersion = new \DateTime();
399 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
400 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
401 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
402
            }
403
        }
404
405 214
        if ( ! empty($update)) {
406
            // Include locking logic so that if the document object in memory is currently
407
            // locked then it will remove it, otherwise it ensures the document is not locked.
408 150
            if ($this->class->isLockable) {
409 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
410 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
411 11
                if ($isLocked) {
412 2
                    $update['$unset'] = array($lockMapping['name'] => true);
413
                } else {
414 9
                    $query[$lockMapping['name']] = array('$exists' => false);
415
                }
416
            }
417
418 150
            $result = $this->collection->update($query, $update, $options);
419
420 150 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...
421 5
                throw LockException::lockFailed($document);
422
            }
423
        }
424
425 210
        $this->handleCollections($document, $options);
426 210
    }
427
428
    /**
429
     * Removes document from mongo
430
     *
431
     * @param mixed $document
432
     * @param array $options Array of options to be used with remove()
433
     * @throws \Doctrine\ODM\MongoDB\LockException
434
     */
435 28
    public function delete($document, array $options = array())
436
    {
437 28
        $query = $this->getQueryForDocument($document);
438
439 28
        if ($this->class->isLockable) {
440 2
            $query[$this->class->lockField] = array('$exists' => false);
441
        }
442
443 28
        $result = $this->collection->remove($query, $options);
444
445 28 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...
446 2
            throw LockException::lockFailed($document);
447
        }
448 26
    }
449
450
    /**
451
     * Refreshes a managed document.
452
     *
453
     * @param string $id
454
     * @param object $document The document to refresh.
455
     *
456
     * @deprecated The first argument is deprecated.
457
     */
458 20
    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...
459
    {
460 20
        $query = $this->getQueryForDocument($document);
461 20
        $data = $this->collection->findOne($query);
462 20
        $data = $this->hydratorFactory->hydrate($document, $data);
463 20
        $this->uow->setOriginalDocumentData($document, $data);
464 20
    }
465
466
    /**
467
     * Finds a document by a set of criteria.
468
     *
469
     * If a scalar or MongoId is provided for $criteria, it will be used to
470
     * match an _id value.
471
     *
472
     * @param mixed   $criteria Query criteria
473
     * @param object  $document Document to load the data into. If not specified, a new document is created.
474
     * @param array   $hints    Hints for document creation
475
     * @param integer $lockMode
476
     * @param array   $sort     Sort array for Cursor::sort()
477
     * @throws \Doctrine\ODM\MongoDB\LockException
478
     * @return object|null The loaded and managed document instance or null if no document was found
479
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
480
     */
481 363
    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...
482
    {
483
        // TODO: remove this
484 363
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
485
            $criteria = array('_id' => $criteria);
486
        }
487
488 363
        $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...
489 363
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
490 363
        $criteria = $this->addFilterToPreparedQuery($criteria);
491
492 363
        $cursor = $this->collection->find($criteria);
493
494 363
        if (null !== $sort) {
495 101
            $cursor->sort($this->prepareSortOrProjection($sort));
496
        }
497
498 363
        $result = $cursor->getSingleResult();
499
500 363
        if ($this->class->isLockable) {
501 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
502 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
503 1
                throw LockException::lockFailed($result);
504
            }
505
        }
506
507 362
        return $this->createDocument($result, $document, $hints);
508
    }
509
510
    /**
511
     * Finds documents by a set of criteria.
512
     *
513
     * @param array        $criteria Query criteria
514
     * @param array        $sort     Sort array for Cursor::sort()
515
     * @param integer|null $limit    Limit for Cursor::limit()
516
     * @param integer|null $skip     Skip for Cursor::skip()
517
     * @return Cursor
518
     */
519 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
520
    {
521 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
522 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
523 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
524
525 22
        $baseCursor = $this->collection->find($criteria);
526 22
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
527
528 22
        if (null !== $sort) {
529 3
            $cursor->sort($sort);
530
        }
531
532 22
        if (null !== $limit) {
533 2
            $cursor->limit($limit);
534
        }
535
536 22
        if (null !== $skip) {
537 2
            $cursor->skip($skip);
538
        }
539
540 22
        return $cursor;
541
    }
542
543
    /**
544
     * @param object $document
545
     *
546
     * @return array
547
     * @throws MongoDBException
548
     */
549 274
    public function getShardKeyQuery($document)
550
    {
551 274
        if ( ! $this->class->isSharded()) {
552 272
            return array();
553
        }
554
555 2
        $shardKey = $this->class->getShardKey();
556 2
        $keys = array_keys($shardKey['keys']);
557 2
        $data = $this->uow->getDocumentActualData($document);
558
559 2
        $shardKeyQueryPart = array();
560 2
        foreach ($keys as $key) {
561 2
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
562 2
            $this->guardMissingShardKey($document, $key, $data);
563 2
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
564 2
            $shardKeyQueryPart[$key] = $value;
565
        }
566
567 2
        return $shardKeyQueryPart;
568
    }
569
570
    /**
571
     * Wraps the supplied base cursor in the corresponding ODM class.
572
     *
573
     * @param CursorInterface $baseCursor
574
     * @return Cursor
575
     */
576 22
    private function wrapCursor(CursorInterface $baseCursor)
577
    {
578 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
579
    }
580
581
    /**
582
     * Checks whether the given managed document exists in the database.
583
     *
584
     * @param object $document
585
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
586
     */
587 3
    public function exists($document)
588
    {
589 3
        $id = $this->class->getIdentifierObject($document);
590 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
591
    }
592
593
    /**
594
     * Locks document by storing the lock mode on the mapped lock field.
595
     *
596
     * @param object $document
597
     * @param int $lockMode
598
     */
599 5
    public function lock($document, $lockMode)
600
    {
601 5
        $id = $this->uow->getDocumentIdentifier($document);
602 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
603 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
604 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
605 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
606 5
    }
607
608
    /**
609
     * Releases any lock that exists on this document.
610
     *
611
     * @param object $document
612
     */
613 1
    public function unlock($document)
614
    {
615 1
        $id = $this->uow->getDocumentIdentifier($document);
616 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
617 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
618 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
619 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
620 1
    }
621
622
    /**
623
     * Creates or fills a single document object from an query result.
624
     *
625
     * @param object $result The query result.
626
     * @param object $document The document object to fill, if any.
627
     * @param array $hints Hints for document creation.
628
     * @return object The filled and managed document object or NULL, if the query result is empty.
629
     */
630 362
    private function createDocument($result, $document = null, array $hints = array())
631
    {
632 362
        if ($result === null) {
633 115
            return null;
634
        }
635
636 310
        if ($document !== null) {
637 37
            $hints[Query::HINT_REFRESH] = true;
638 37
            $id = $this->class->getPHPIdentifierValue($result['_id']);
639 37
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
640
        }
641
642 310
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
643
    }
644
645
    /**
646
     * Loads a PersistentCollection data. Used in the initialize() method.
647
     *
648
     * @param PersistentCollectionInterface $collection
649
     */
650 163
    public function loadCollection(PersistentCollectionInterface $collection)
651
    {
652 163
        $mapping = $collection->getMapping();
653 163
        switch ($mapping['association']) {
654 163
            case ClassMetadata::EMBED_MANY:
655 114
                $this->loadEmbedManyCollection($collection);
656 114
                break;
657
658 65
            case ClassMetadata::REFERENCE_MANY:
659 65
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
660 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
661
                } else {
662 62
                    if ($mapping['isOwningSide']) {
663 52
                        $this->loadReferenceManyCollectionOwningSide($collection);
664
                    } else {
665 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
666
                    }
667
                }
668 65
                break;
669
        }
670 163
    }
671
672 114
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
673
    {
674 114
        $embeddedDocuments = $collection->getMongoData();
675 114
        $mapping = $collection->getMapping();
676 114
        $owner = $collection->getOwner();
677 114
        if ($embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
678 85
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
679 85
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
680 85
                $embeddedMetadata = $this->dm->getClassMetadata($className);
681 85
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
682
683 85
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
684
685 85
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
686 85
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
687 21
                    ? $data[$embeddedMetadata->identifier]
688 85
                    : null;
689
690 85
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
691 85
                if (CollectionHelper::isHash($mapping['strategy'])) {
692 25
                    $collection->set($key, $embeddedDocumentObject);
693
                } else {
694 85
                    $collection->add($embeddedDocumentObject);
695
                }
696
            }
697
        }
698 114
    }
699
700 52
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
701
    {
702 52
        $hints = $collection->getHints();
703 52
        $mapping = $collection->getMapping();
704 52
        $groupedIds = array();
705
706 52
        $sorted = isset($mapping['sort']) && $mapping['sort'];
707
708 52
        foreach ($collection->getMongoData() as $key => $reference) {
709 47
            if (isset($mapping['simple']) && $mapping['simple']) {
710 4
                $className = $mapping['targetDocument'];
711 4
                $mongoId = $reference;
712
            } else {
713 43
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
714 43
                $mongoId = $reference['$id'];
715
            }
716 47
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
717
718
            // create a reference to the class and id
719 47
            $reference = $this->dm->getReference($className, $id);
720
721
            // no custom sort so add the references right now in the order they are embedded
722 47
            if ( ! $sorted) {
723 46
                if (CollectionHelper::isHash($mapping['strategy'])) {
724 2
                    $collection->set($key, $reference);
725
                } else {
726 44
                    $collection->add($reference);
727
                }
728
            }
729
730
            // only query for the referenced object if it is not already initialized or the collection is sorted
731 47
            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...
732 47
                $groupedIds[$className][] = $mongoId;
733
            }
734
        }
735 52
        foreach ($groupedIds as $className => $ids) {
736 35
            $class = $this->dm->getClassMetadata($className);
737 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
738 35
            $criteria = $this->cm->merge(
739 35
                array('_id' => array('$in' => array_values($ids))),
740 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
741 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
742
            );
743 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
744 35
            $cursor = $mongoCollection->find($criteria);
745 35
            if (isset($mapping['sort'])) {
746 35
                $cursor->sort($mapping['sort']);
747
            }
748 35
            if (isset($mapping['limit'])) {
749
                $cursor->limit($mapping['limit']);
750
            }
751 35
            if (isset($mapping['skip'])) {
752
                $cursor->skip($mapping['skip']);
753
            }
754 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
755
                $cursor->slaveOkay(true);
756
            }
757 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...
758
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
759
            }
760 35
            $documents = $cursor->toArray(false);
761 35
            foreach ($documents as $documentData) {
762 34
                $document = $this->uow->getById($documentData['_id'], $class);
763 34
                $data = $this->hydratorFactory->hydrate($document, $documentData);
764 34
                $this->uow->setOriginalDocumentData($document, $data);
765 34
                $document->__isInitialized__ = true;
766 34
                if ($sorted) {
767 35
                    $collection->add($document);
768
                }
769
            }
770
        }
771 52
    }
772
773 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
774
    {
775 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
776 14
        $documents = $query->execute()->toArray(false);
777 14
        foreach ($documents as $key => $document) {
778 13
            $collection->add($document);
779
        }
780 14
    }
781
782
    /**
783
     * @param PersistentCollectionInterface $collection
784
     *
785
     * @return Query
786
     */
787 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
788
    {
789 16
        $hints = $collection->getHints();
790 16
        $mapping = $collection->getMapping();
791 16
        $owner = $collection->getOwner();
792 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
793 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
794 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
795 16
        $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
796 16
        $criteria = $this->cm->merge(
797 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
798 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
799 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
800
        );
801 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
802 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
803 16
            ->setQueryArray($criteria);
804
805 16
        if (isset($mapping['sort'])) {
806 16
            $qb->sort($mapping['sort']);
807
        }
808 16
        if (isset($mapping['limit'])) {
809 1
            $qb->limit($mapping['limit']);
810
        }
811 16
        if (isset($mapping['skip'])) {
812
            $qb->skip($mapping['skip']);
813
        }
814 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
815
            $qb->slaveOkay(true);
816
        }
817 16 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
818
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
819
        }
820
821 16
        return $qb->getQuery();
822
    }
823
824 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
825
    {
826 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
827 3
        $mapping = $collection->getMapping();
828 3
        $documents = $cursor->toArray(false);
0 ignored issues
show
Unused Code introduced by
The call to CursorInterface::toArray() has too many arguments starting with false.

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

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

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

Loading history...
829 3
        foreach ($documents as $key => $obj) {
830 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
831 1
                $collection->set($key, $obj);
832
            } else {
833 3
                $collection->add($obj);
834
            }
835
        }
836 3
    }
837
838
    /**
839
     * @param PersistentCollectionInterface $collection
840
     *
841
     * @return CursorInterface
842
     */
843 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
844
    {
845 3
        $hints = $collection->getHints();
846 3
        $mapping = $collection->getMapping();
847 3
        $repositoryMethod = $mapping['repositoryMethod'];
848 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
849 3
            ->$repositoryMethod($collection->getOwner());
850
851 3
        if ( ! $cursor instanceof CursorInterface) {
852
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
853
        }
854
855 3
        if (isset($mapping['sort'])) {
856 3
            $cursor->sort($mapping['sort']);
857
        }
858 3
        if (isset($mapping['limit'])) {
859
            $cursor->limit($mapping['limit']);
860
        }
861 3
        if (isset($mapping['skip'])) {
862
            $cursor->skip($mapping['skip']);
863
        }
864 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
865
            $cursor->slaveOkay(true);
866
        }
867 3 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
868
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
869
        }
870
871 3
        return $cursor;
872
    }
873
874
    /**
875
     * Prepare a sort or projection array by converting keys, which are PHP
876
     * property names, to MongoDB field names.
877
     *
878
     * @param array $fields
879
     * @return array
880
     */
881 138
    public function prepareSortOrProjection(array $fields)
882
    {
883 138
        $preparedFields = array();
884
885 138
        foreach ($fields as $key => $value) {
886 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
887
        }
888
889 138
        return $preparedFields;
890
    }
891
892
    /**
893
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
894
     *
895
     * @param string $fieldName
896
     * @return string
897
     */
898 85
    public function prepareFieldName($fieldName)
899
    {
900 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
901
902 85
        return $fieldName;
903
    }
904
905
    /**
906
     * Adds discriminator criteria to an already-prepared query.
907
     *
908
     * This method should be used once for query criteria and not be used for
909
     * nested expressions. It should be called before
910
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
911
     *
912
     * @param array $preparedQuery
913
     * @return array
914
     */
915 487
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
916
    {
917
        /* If the class has a discriminator field, which is not already in the
918
         * criteria, inject it now. The field/values need no preparation.
919
         */
920 487
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
921 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
922 21
            if (count($discriminatorValues) === 1) {
923 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
924
            } else {
925 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
926
            }
927
        }
928
929 487
        return $preparedQuery;
930
    }
931
932
    /**
933
     * Adds filter criteria to an already-prepared query.
934
     *
935
     * This method should be used once for query criteria and not be used for
936
     * nested expressions. It should be called after
937
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
938
     *
939
     * @param array $preparedQuery
940
     * @return array
941
     */
942 488
    public function addFilterToPreparedQuery(array $preparedQuery)
943
    {
944
        /* If filter criteria exists for this class, prepare it and merge
945
         * over the existing query.
946
         *
947
         * @todo Consider recursive merging in case the filter criteria and
948
         * prepared query both contain top-level $and/$or operators.
949
         */
950 488
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
951 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
952
        }
953
954 488
        return $preparedQuery;
955
    }
956
957
    /**
958
     * Prepares the query criteria or new document object.
959
     *
960
     * PHP field names and types will be converted to those used by MongoDB.
961
     *
962
     * @param array $query
963
     * @return array
964
     */
965 521
    public function prepareQueryOrNewObj(array $query)
966
    {
967 521
        $preparedQuery = array();
968
969 521
        foreach ($query as $key => $value) {
970
            // Recursively prepare logical query clauses
971 483
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
972 20
                foreach ($value as $k2 => $v2) {
973 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
974
                }
975 20
                continue;
976
            }
977
978 483
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
979 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
980 20
                continue;
981
            }
982
983 483
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
984
985 483
            $preparedQuery[$key] = is_array($value)
986 120
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
987 483
                : Type::convertPHPToDatabaseValue($value);
988
        }
989
990 521
        return $preparedQuery;
991
    }
992
993
    /**
994
     * Prepares a query value and converts the PHP value to the database value
995
     * if it is an identifier.
996
     *
997
     * It also handles converting $fieldName to the database name if they are different.
998
     *
999
     * @param string $fieldName
1000
     * @param mixed $value
1001
     * @param ClassMetadata $class        Defaults to $this->class
1002
     * @param boolean $prepareValue Whether or not to prepare the value
1003
     * @return array        Prepared field name and value
1004
     */
1005 514
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
1006
    {
1007 514
        $class = isset($class) ? $class : $this->class;
1008
1009
        // @todo Consider inlining calls to ClassMetadataInfo methods
1010
1011
        // Process all non-identifier fields by translating field names
1012 514
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1013 236
            $mapping = $class->fieldMappings[$fieldName];
1014 236
            $fieldName = $mapping['name'];
1015
1016 236
            if ( ! $prepareValue) {
1017 62
                return array($fieldName, $value);
1018
            }
1019
1020
            // Prepare mapped, embedded objects
1021 194
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1022 194
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1023 3
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
1024
            }
1025
1026 192
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1027
                try {
1028 5
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
1029 1
                } catch (MappingException $e) {
1030
                    // do nothing in case passed object is not mapped document
1031
                }
1032
            }
1033
1034
            // No further preparation unless we're dealing with a simple reference
1035
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1036 188
            $arrayValue = (array) $value;
1037 188
            if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) {
1038 116
                return array($fieldName, $value);
1039
            }
1040
1041
            // Additional preparation for one or more simple reference values
1042 100
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1043
1044 100
            if ( ! is_array($value)) {
1045 95
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1046
            }
1047
1048
            // Objects without operators or with DBRef fields can be converted immediately
1049 7 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...
1050 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1051
            }
1052
1053 7
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1054
        }
1055
1056
        // Process identifier fields
1057 389
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1058 322
            $fieldName = '_id';
1059
1060 322
            if ( ! $prepareValue) {
1061 16
                return array($fieldName, $value);
1062
            }
1063
1064 308
            if ( ! is_array($value)) {
1065 287
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1066
            }
1067
1068
            // Objects without operators or with DBRef fields can be converted immediately
1069 54 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...
1070 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1071
            }
1072
1073 49
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1074
        }
1075
1076
        // No processing for unmapped, non-identifier, non-dotted field names
1077 100
        if (strpos($fieldName, '.') === false) {
1078 44
            return array($fieldName, $value);
1079
        }
1080
1081
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1082
         *
1083
         * We can limit parsing here, since at most three segments are
1084
         * significant: "fieldName.objectProperty" with an optional index or key
1085
         * for collections stored as either BSON arrays or objects.
1086
         */
1087 62
        $e = explode('.', $fieldName, 4);
1088
1089
        // No further processing for unmapped fields
1090 62
        if ( ! isset($class->fieldMappings[$e[0]])) {
1091 4
            return array($fieldName, $value);
1092
        }
1093
1094 59
        $mapping = $class->fieldMappings[$e[0]];
1095 59
        $e[0] = $mapping['name'];
1096
1097
        // Hash and raw fields will not be prepared beyond the field name
1098 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1099 1
            $fieldName = implode('.', $e);
1100
1101 1
            return array($fieldName, $value);
1102
        }
1103
1104 58
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1105 58
                && isset($e[2])) {
1106 1
            $objectProperty = $e[2];
1107 1
            $objectPropertyPrefix = $e[1] . '.';
1108 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1109 57
        } elseif ($e[1] != '$') {
1110 56
            $fieldName = $e[0] . '.' . $e[1];
1111 56
            $objectProperty = $e[1];
1112 56
            $objectPropertyPrefix = '';
1113 56
            $nextObjectProperty = implode('.', array_slice($e, 2));
1114 1
        } elseif (isset($e[2])) {
1115 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1116 1
            $objectProperty = $e[2];
1117 1
            $objectPropertyPrefix = $e[1] . '.';
1118 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1119
        } else {
1120 1
            $fieldName = $e[0] . '.' . $e[1];
1121
1122 1
            return array($fieldName, $value);
1123
        }
1124
1125
        // No further processing for fields without a targetDocument mapping
1126 58
        if ( ! isset($mapping['targetDocument'])) {
1127 2
            if ($nextObjectProperty) {
1128
                $fieldName .= '.'.$nextObjectProperty;
1129
            }
1130
1131 2
            return array($fieldName, $value);
1132
        }
1133
1134 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1135
1136
        // No further processing for unmapped targetDocument fields
1137 56
        if ( ! $targetClass->hasField($objectProperty)) {
1138 24
            if ($nextObjectProperty) {
1139
                $fieldName .= '.'.$nextObjectProperty;
1140
            }
1141
1142 24
            return array($fieldName, $value);
1143
        }
1144
1145 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1146 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1147
1148
        // Prepare DBRef identifiers or the mapped field's property path
1149 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1150 13
            ? $e[0] . '.$id'
1151 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1152
1153
        // Process targetDocument identifier fields
1154 35
        if ($objectPropertyIsId) {
1155 14
            if ( ! $prepareValue) {
1156 1
                return array($fieldName, $value);
1157
            }
1158
1159 13
            if ( ! is_array($value)) {
1160 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1161
            }
1162
1163
            // Objects without operators or with DBRef fields can be converted immediately
1164 12 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1165 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1166
            }
1167
1168 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1169
        }
1170
1171
        /* The property path may include a third field segment, excluding the
1172
         * collection item pointer. If present, this next object property must
1173
         * be processed recursively.
1174
         */
1175 21
        if ($nextObjectProperty) {
1176
            // Respect the targetDocument's class metadata when recursing
1177 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1178 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1179 14
                : null;
1180
1181 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1182
1183 14
            $fieldName .= '.' . $key;
1184
        }
1185
1186 21
        return array($fieldName, $value);
1187
    }
1188
1189
    /**
1190
     * Prepares a query expression.
1191
     *
1192
     * @param array|object  $expression
1193
     * @param ClassMetadata $class
1194
     * @return array
1195
     */
1196 68
    private function prepareQueryExpression($expression, $class)
1197
    {
1198 68
        foreach ($expression as $k => $v) {
1199
            // Ignore query operators whose arguments need no type conversion
1200 68
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1201 12
                continue;
1202
            }
1203
1204
            // Process query operators whose argument arrays need type conversion
1205 68
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1206 65
                foreach ($v as $k2 => $v2) {
1207 65
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1208
                }
1209 65
                continue;
1210
            }
1211
1212
            // Recursively process expressions within a $not operator
1213 15
            if ($k === '$not' && is_array($v)) {
1214 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1215 11
                continue;
1216
            }
1217
1218 15
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1219
        }
1220
1221 68
        return $expression;
1222
    }
1223
1224
    /**
1225
     * Checks whether the value has DBRef fields.
1226
     *
1227
     * This method doesn't check if the the value is a complete DBRef object,
1228
     * although it should return true for a DBRef. Rather, we're checking that
1229
     * the value has one or more fields for a DBref. In practice, this could be
1230
     * $elemMatch criteria for matching a DBRef.
1231
     *
1232
     * @param mixed $value
1233
     * @return boolean
1234
     */
1235 69
    private function hasDBRefFields($value)
1236
    {
1237 69
        if ( ! is_array($value) && ! is_object($value)) {
1238
            return false;
1239
        }
1240
1241 69
        if (is_object($value)) {
1242
            $value = get_object_vars($value);
1243
        }
1244
1245 69
        foreach ($value as $key => $_) {
1246 69
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1247 69
                return true;
1248
            }
1249
        }
1250
1251 68
        return false;
1252
    }
1253
1254
    /**
1255
     * Checks whether the value has query operators.
1256
     *
1257
     * @param mixed $value
1258
     * @return boolean
1259
     */
1260 73
    private function hasQueryOperators($value)
1261
    {
1262 73
        if ( ! is_array($value) && ! is_object($value)) {
1263
            return false;
1264
        }
1265
1266 73
        if (is_object($value)) {
1267
            $value = get_object_vars($value);
1268
        }
1269
1270 73
        foreach ($value as $key => $_) {
1271 73
            if (isset($key[0]) && $key[0] === '$') {
1272 73
                return true;
1273
            }
1274
        }
1275
1276 9
        return false;
1277
    }
1278
1279
    /**
1280
     * Gets the array of discriminator values for the given ClassMetadata
1281
     *
1282
     * @param ClassMetadata $metadata
1283
     * @return array
1284
     */
1285 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1286
    {
1287 21
        $discriminatorValues = array($metadata->discriminatorValue);
1288 21
        foreach ($metadata->subClasses as $className) {
1289 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1290 8
                $discriminatorValues[] = $key;
1291
            }
1292
        }
1293
1294
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1295 21 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1296 2
            $discriminatorValues[] = null;
1297
        }
1298
1299 21
        return $discriminatorValues;
1300
    }
1301
1302 555
    private function handleCollections($document, $options)
1303
    {
1304
        // Collection deletions (deletions of complete collections)
1305 555
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1306 101
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1307 101
                $this->cp->delete($coll, $options);
1308
            }
1309
        }
1310
        // Collection updates (deleteRows, updateRows, insertRows)
1311 555
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1312 101
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1313 101
                $this->cp->update($coll, $options);
1314
            }
1315
        }
1316
        // Take new snapshots from visited collections
1317 555
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1318 236
            $coll->takeSnapshot();
1319
        }
1320 555
    }
1321
1322
    /**
1323
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1324
     * Also, shard key field should be presented in actual document data.
1325
     *
1326
     * @param object $document
1327
     * @param string $shardKeyField
1328
     * @param array  $actualDocumentData
1329
     *
1330
     * @throws MongoDBException
1331
     */
1332 2
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1333
    {
1334 2
        $dcs = $this->uow->getDocumentChangeSet($document);
1335 2
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1336
1337 2
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1338 2
        $fieldName = $fieldMapping['fieldName'];
1339
1340 2
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1341
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1342
        }
1343
1344 2
        if (!isset($actualDocumentData[$fieldName])) {
1345
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1346
        }
1347 2
    }
1348
1349
    /**
1350
     * Get shard key aware query for single document.
1351
     *
1352
     * @param object $document
1353
     *
1354
     * @return array
1355
     */
1356 272
    private function getQueryForDocument($document)
1357
    {
1358 272
        $id = $this->uow->getDocumentIdentifier($document);
1359 272
        $id = $this->class->getDatabaseIdentifierValue($id);
1360
1361 272
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1362 272
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1363
1364 272
        return $query;
1365
    }
1366
}
1367