Completed
Push — master ( 8648c7...74350b )
by Andreas
17s
created

DocumentPersister::getShardKeyQuery()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 21
cts 21
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 22
nc 4
nop 1
crap 6
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 783
    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 783
        $this->pb = $pb;
141 783
        $this->dm = $dm;
142 783
        $this->evm = $evm;
143 783
        $this->cm = $cm ?: new CriteriaMerger();
144 783
        $this->uow = $uow;
145 783
        $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 783
        $this->class = $class;
147 783
        $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 783
        $this->cp = $this->uow->getCollectionPersister();
149 783
    }
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 545
    public function addInsert($document)
175
    {
176 545
        $this->queuedInserts[spl_object_hash($document)] = $document;
177 545
    }
178
179
    /**
180
     * @return array
181
     */
182
    public function getUpserts()
183
    {
184
        return $this->queuedUpserts;
185
    }
186
187
    /**
188
     * @param object $document
189
     * @return boolean
190
     */
191
    public function isQueuedForUpsert($document)
192
    {
193
        return isset($this->queuedUpserts[spl_object_hash($document)]);
194
    }
195
196
    /**
197
     * Adds a document to the queued upserts.
198
     * The document remains queued until {@link executeUpserts} is invoked.
199
     *
200
     * @param object $document The document to queue for insertion.
201
     */
202 87
    public function addUpsert($document)
203
    {
204 87
        $this->queuedUpserts[spl_object_hash($document)] = $document;
205 87
    }
206
207
    /**
208
     * Gets the ClassMetadata instance of the document class this persister is used for.
209
     *
210
     * @return ClassMetadata
211
     */
212
    public function getClassMetadata()
213
    {
214
        return $this->class;
215
    }
216
217
    /**
218
     * Executes all queued document insertions.
219
     *
220
     * Queued documents without an ID will inserted in a batch and queued
221
     * documents with an ID will be upserted individually.
222
     *
223
     * If no inserts are queued, invoking this method is a NOOP.
224
     *
225
     * @param array $options Options for batchInsert() and update() driver methods
226
     */
227 545
    public function executeInserts(array $options = array())
228
    {
229 545
        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 545
        $inserts = array();
234 545
        $options = $this->getWriteOptions($options);
235 545
        foreach ($this->queuedInserts as $oid => $document) {
236 545
            $data = $this->pb->prepareInsertData($document);
237
238
            // Set the initial version for each insert
239 544 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 38
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
241 38
                if ($versionMapping['type'] === 'int') {
242 36
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
243 36
                    $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 38
                $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 544
            $inserts[$oid] = $data;
253
        }
254
255 544
        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 544
                $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 544
        foreach ($this->queuedInserts as $document) {
269 544
            $this->handleCollections($document, $options);
270
        }
271
272 544
        $this->queuedInserts = array();
273 544
    }
274
275
    /**
276
     * Executes all queued document upserts.
277
     *
278
     * Queued documents with an ID are upserted individually.
279
     *
280
     * If no upserts are queued, invoking this method is a NOOP.
281
     *
282
     * @param array $options Options for batchInsert() and update() driver methods
283
     */
284 87
    public function executeUpserts(array $options = array())
285
    {
286 87
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
287
            return;
288
        }
289
290 87
        $options = $this->getWriteOptions($options);
291 87
        foreach ($this->queuedUpserts as $oid => $document) {
292
            try {
293 87
                $this->executeUpsert($document, $options);
294 87
                $this->handleCollections($document, $options);
295 87
                unset($this->queuedUpserts[$oid]);
296
            } catch (\MongoException $e) {
297
                unset($this->queuedUpserts[$oid]);
298 87
                throw $e;
299
            }
300
        }
301 87
    }
302
303
    /**
304
     * Executes a single upsert in {@link executeUpserts}
305
     *
306
     * @param object $document
307
     * @param array  $options
308
     */
309 87
    private function executeUpsert($document, array $options)
310
    {
311 87
        $options['upsert'] = true;
312 87
        $criteria = $this->getQueryForDocument($document);
313
314 87
        $data = $this->pb->prepareUpsertData($document);
315
316
        // Set the initial version for each upsert
317 87 View Code Duplication
        if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
318 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
319 3
            if ($versionMapping['type'] === 'int') {
320 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
321 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
322 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 87
        foreach (array_keys($criteria) as $field) {
331 87
            unset($data['$set'][$field]);
332
        }
333
334
        // Do not send an empty $set modifier
335 87
        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 87
        if (empty($data)) {
353 17
            $retry = true;
354 17
            $data = array('$set' => array('_id' => $criteria['_id']));
355
        }
356
357
        try {
358 87
            $this->collection->update($criteria, $data, $options);
359 87
            return;
360
        } catch (\MongoCursorException $e) {
361
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
362
                throw $e;
363
            }
364
        }
365
366
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
367
    }
368
369
    /**
370
     * Updates the already persisted document if it has any new changesets.
371
     *
372
     * @param object $document
373
     * @param array $options Array of options to be used with update()
374
     * @throws \Doctrine\ODM\MongoDB\LockException
375
     */
376 225
    public function update($document, array $options = array())
377
    {
378 225
        $update = $this->pb->prepareUpdateData($document);
379
380 225
        $query = $this->getQueryForDocument($document);
381
382 223
        foreach (array_keys($query) as $field) {
383 223
            unset($update['$set'][$field]);
384
        }
385
386 223
        if (empty($update['$set'])) {
387 93
            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 223
        $nextVersion = null;
395 223
        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 223
        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 156
            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 156
            $options = $this->getWriteOptions($options);
423
424 156
            $result = $this->collection->update($query, $update, $options);
425
426 156
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
427 6
                throw LockException::lockFailed($document);
428 151
            } elseif ($this->class->isVersioned) {
429 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
430
            }
431
        }
432
433 218
        $this->handleCollections($document, $options);
434 218
    }
435
436
    /**
437
     * Removes document from mongo
438
     *
439
     * @param mixed $document
440
     * @param array $options Array of options to be used with remove()
441
     * @throws \Doctrine\ODM\MongoDB\LockException
442
     */
443 34
    public function delete($document, array $options = array())
444
    {
445 34
        $query = $this->getQueryForDocument($document);
446
447 34
        if ($this->class->isLockable) {
448 2
            $query[$this->class->lockField] = array('$exists' => false);
449
        }
450
451 34
        $options = $this->getWriteOptions($options);
452
453 34
        $result = $this->collection->remove($query, $options);
454
455 34
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
456 2
            throw LockException::lockFailed($document);
457
        }
458 32
    }
459
460
    /**
461
     * Refreshes a managed document.
462
     *
463
     * @param string $id
464
     * @param object $document The document to refresh.
465
     *
466
     * @deprecated The first argument is deprecated.
467
     */
468 21
    public function refresh($id, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
469
    {
470 21
        $query = $this->getQueryForDocument($document);
471 21
        $data = $this->collection->findOne($query);
472 21
        $data = $this->hydratorFactory->hydrate($document, $data);
473 21
        $this->uow->setOriginalDocumentData($document, $data);
474 21
    }
475
476
    /**
477
     * Finds a document by a set of criteria.
478
     *
479
     * If a scalar or MongoId is provided for $criteria, it will be used to
480
     * match an _id value.
481
     *
482
     * @param mixed   $criteria Query criteria
483
     * @param object  $document Document to load the data into. If not specified, a new document is created.
484
     * @param array   $hints    Hints for document creation
485
     * @param integer $lockMode
486
     * @param array   $sort     Sort array for Cursor::sort()
487
     * @throws \Doctrine\ODM\MongoDB\LockException
488
     * @return object|null The loaded and managed document instance or null if no document was found
489
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
490
     */
491 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...
492
    {
493
        // TODO: remove this
494 384
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
495
            $criteria = array('_id' => $criteria);
496
        }
497
498 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...
499 384
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
500 384
        $criteria = $this->addFilterToPreparedQuery($criteria);
501
502 384
        $cursor = $this->collection->find($criteria);
503
504 384
        if (null !== $sort) {
505 105
            $cursor->sort($this->prepareSortOrProjection($sort));
506
        }
507
508 384
        $result = $cursor->getSingleResult();
509
510 384
        if ($this->class->isLockable) {
511 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
512 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
513 1
                throw LockException::lockFailed($result);
514
            }
515
        }
516
517 383
        return $this->createDocument($result, $document, $hints);
518
    }
519
520
    /**
521
     * Finds documents by a set of criteria.
522
     *
523
     * @param array        $criteria Query criteria
524
     * @param array        $sort     Sort array for Cursor::sort()
525
     * @param integer|null $limit    Limit for Cursor::limit()
526
     * @param integer|null $skip     Skip for Cursor::skip()
527
     * @return Cursor
528
     */
529 26
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
530
    {
531 26
        $criteria = $this->prepareQueryOrNewObj($criteria);
532 26
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
533 26
        $criteria = $this->addFilterToPreparedQuery($criteria);
534
535 26
        $baseCursor = $this->collection->find($criteria);
536 26
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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