Completed
Pull Request — master (#1419)
by Robert
11:09 queued 01:09
created

DocumentPersister::getWriteOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

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

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

Loading history...
453 2
            throw LockException::lockFailed($document);
454
        }
455 31
    }
456
457
    /**
458
     * Refreshes a managed document.
459
     *
460
     * @param string $id
461
     * @param object $document The document to refresh.
462
     *
463
     * @deprecated The first argument is deprecated.
464
     */
465 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...
466
    {
467 21
        $query = $this->getQueryForDocument($document);
468 21
        $data = $this->collection->findOne($query);
469 21
        $data = $this->hydratorFactory->hydrate($document, $data);
470 21
        $this->uow->setOriginalDocumentData($document, $data);
471 21
    }
472
473
    /**
474
     * Finds a document by a set of criteria.
475
     *
476
     * If a scalar or MongoId is provided for $criteria, it will be used to
477
     * match an _id value.
478
     *
479
     * @param mixed   $criteria Query criteria
480
     * @param object  $document Document to load the data into. If not specified, a new document is created.
481
     * @param array   $hints    Hints for document creation
482
     * @param integer $lockMode
483
     * @param array   $sort     Sort array for Cursor::sort()
484
     * @throws \Doctrine\ODM\MongoDB\LockException
485
     * @return object|null The loaded and managed document instance or null if no document was found
486
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
487
     */
488 364
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

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

Loading history...
489
    {
490
        // TODO: remove this
491 364
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
492
            $criteria = array('_id' => $criteria);
493
        }
494
495 364
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
496 364
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
497 364
        $criteria = $this->addFilterToPreparedQuery($criteria);
498
499 364
        $cursor = $this->collection->find($criteria);
500
501 364
        if (null !== $sort) {
502 102
            $cursor->sort($this->prepareSortOrProjection($sort));
503
        }
504
505 364
        $result = $cursor->getSingleResult();
506
507 364
        if ($this->class->isLockable) {
508 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
509 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
510 1
                throw LockException::lockFailed($result);
511
            }
512
        }
513
514 363
        return $this->createDocument($result, $document, $hints);
515
    }
516
517
    /**
518
     * Finds documents by a set of criteria.
519
     *
520
     * @param array        $criteria Query criteria
521
     * @param array        $sort     Sort array for Cursor::sort()
522
     * @param integer|null $limit    Limit for Cursor::limit()
523
     * @param integer|null $skip     Skip for Cursor::skip()
524
     * @return Cursor
525
     */
526 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
527
    {
528 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
529 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
530 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
531
532 22
        $baseCursor = $this->collection->find($criteria);
533 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...
534
535 22
        if (null !== $sort) {
536 3
            $cursor->sort($sort);
537
        }
538
539 22
        if (null !== $limit) {
540 2
            $cursor->limit($limit);
541
        }
542
543 22
        if (null !== $skip) {
544 2
            $cursor->skip($skip);
545
        }
546
547 22
        return $cursor;
548
    }
549
550
    /**
551
     * @param object $document
552
     *
553
     * @return array
554
     * @throws MongoDBException
555
     */
556 293
    private function getShardKeyQuery($document)
557
    {
558 293
        if ( ! $this->class->isSharded()) {
559 284
            return array();
560
        }
561
562 9
        $shardKey = $this->class->getShardKey();
563 9
        $keys = array_keys($shardKey['keys']);
564 9
        $data = $this->uow->getDocumentActualData($document);
565
566 9
        $shardKeyQueryPart = array();
567 9
        foreach ($keys as $key) {
568 9
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
569 9
            $this->guardMissingShardKey($document, $key, $data);
570 7
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
571 7
            $shardKeyQueryPart[$key] = $value;
572
        }
573
574 7
        return $shardKeyQueryPart;
575
    }
576
577
    /**
578
     * Wraps the supplied base cursor in the corresponding ODM class.
579
     *
580
     * @param CursorInterface $baseCursor
581
     * @return Cursor
582
     */
583 22
    private function wrapCursor(CursorInterface $baseCursor)
584
    {
585 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
586
    }
587
588
    /**
589
     * Checks whether the given managed document exists in the database.
590
     *
591
     * @param object $document
592
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
593
     */
594 3
    public function exists($document)
595
    {
596 3
        $id = $this->class->getIdentifierObject($document);
597 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
598
    }
599
600
    /**
601
     * Locks document by storing the lock mode on the mapped lock field.
602
     *
603
     * @param object $document
604
     * @param int $lockMode
605
     */
606 5
    public function lock($document, $lockMode)
607
    {
608 5
        $id = $this->uow->getDocumentIdentifier($document);
609 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
610 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
611 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
612 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
613 5
    }
614
615
    /**
616
     * Releases any lock that exists on this document.
617
     *
618
     * @param object $document
619
     */
620 1
    public function unlock($document)
621
    {
622 1
        $id = $this->uow->getDocumentIdentifier($document);
623 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
624 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
625 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
626 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
627 1
    }
628
629
    /**
630
     * Creates or fills a single document object from an query result.
631
     *
632
     * @param object $result The query result.
633
     * @param object $document The document object to fill, if any.
634
     * @param array $hints Hints for document creation.
635
     * @return object The filled and managed document object or NULL, if the query result is empty.
636
     */
637 363
    private function createDocument($result, $document = null, array $hints = array())
638
    {
639 363
        if ($result === null) {
640 116
            return null;
641
        }
642
643 310
        if ($document !== null) {
644 37
            $hints[Query::HINT_REFRESH] = true;
645 37
            $id = $this->class->getPHPIdentifierValue($result['_id']);
646 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...
647
        }
648
649 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...
650
    }
651
652
    /**
653
     * Loads a PersistentCollection data. Used in the initialize() method.
654
     *
655
     * @param PersistentCollectionInterface $collection
656
     */
657 166
    public function loadCollection(PersistentCollectionInterface $collection)
658
    {
659 166
        $mapping = $collection->getMapping();
660 166
        switch ($mapping['association']) {
661 166
            case ClassMetadata::EMBED_MANY:
662 116
                $this->loadEmbedManyCollection($collection);
663 116
                break;
664
665 67
            case ClassMetadata::REFERENCE_MANY:
666 67
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
667 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
668
                } else {
669 64
                    if ($mapping['isOwningSide']) {
670 54
                        $this->loadReferenceManyCollectionOwningSide($collection);
671
                    } else {
672 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
673
                    }
674
                }
675 67
                break;
676
        }
677 166
    }
678
679 116
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
680
    {
681 116
        $embeddedDocuments = $collection->getMongoData();
682 116
        $mapping = $collection->getMapping();
683 116
        $owner = $collection->getOwner();
684 116
        if ($embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
685 87
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
686 87
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
687 87
                $embeddedMetadata = $this->dm->getClassMetadata($className);
688 87
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
689
690 87
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
691
692 87
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
693 87
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
694 21
                    ? $data[$embeddedMetadata->identifier]
695 87
                    : null;
696
                
697 87
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
698 86
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
699
                }
700 87
                if (CollectionHelper::isHash($mapping['strategy'])) {
701 25
                    $collection->set($key, $embeddedDocumentObject);
702
                } else {
703 87
                    $collection->add($embeddedDocumentObject);
704
                }
705
            }
706
        }
707 116
    }
708
709 54
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
710
    {
711 54
        $hints = $collection->getHints();
712 54
        $mapping = $collection->getMapping();
713 54
        $groupedIds = array();
714
715 54
        $sorted = isset($mapping['sort']) && $mapping['sort'];
716
717 54
        foreach ($collection->getMongoData() as $key => $reference) {
718 49
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
719 5
                $className = $mapping['targetDocument'];
720 5
                $mongoId = $reference;
721
            } else {
722 45
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
723 45
                $mongoId = $reference['$id'];
724
            }
725 49
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
726
727
            // create a reference to the class and id
728 49
            $reference = $this->dm->getReference($className, $id);
729
730
            // no custom sort so add the references right now in the order they are embedded
731 49
            if ( ! $sorted) {
732 48
                if (CollectionHelper::isHash($mapping['strategy'])) {
733 2
                    $collection->set($key, $reference);
734
                } else {
735 46
                    $collection->add($reference);
736
                }
737
            }
738
739
            // only query for the referenced object if it is not already initialized or the collection is sorted
740 49
            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...
741 49
                $groupedIds[$className][] = $mongoId;
742
            }
743
        }
744 54
        foreach ($groupedIds as $className => $ids) {
745 35
            $class = $this->dm->getClassMetadata($className);
746 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
747 35
            $criteria = $this->cm->merge(
748 35
                array('_id' => array('$in' => array_values($ids))),
749 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
750 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
751
            );
752 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
753 35
            $cursor = $mongoCollection->find($criteria);
754 35
            if (isset($mapping['sort'])) {
755 35
                $cursor->sort($mapping['sort']);
756
            }
757 35
            if (isset($mapping['limit'])) {
758
                $cursor->limit($mapping['limit']);
759
            }
760 35
            if (isset($mapping['skip'])) {
761
                $cursor->skip($mapping['skip']);
762
            }
763 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
764
                $cursor->slaveOkay(true);
765
            }
766 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...
767
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
768
            }
769 35
            $documents = $cursor->toArray(false);
770 35
            foreach ($documents as $documentData) {
771 34
                $document = $this->uow->getById($documentData['_id'], $class);
772 34
                $data = $this->hydratorFactory->hydrate($document, $documentData);
773 34
                $this->uow->setOriginalDocumentData($document, $data);
774 34
                $document->__isInitialized__ = true;
775 34
                if ($sorted) {
776 35
                    $collection->add($document);
777
                }
778
            }
779
        }
780 54
    }
781
782 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
783
    {
784 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
785 14
        $documents = $query->execute()->toArray(false);
786 14
        foreach ($documents as $key => $document) {
787 13
            $collection->add($document);
788
        }
789 14
    }
790
791
    /**
792
     * @param PersistentCollectionInterface $collection
793
     *
794
     * @return Query
795
     */
796 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
797
    {
798 16
        $hints = $collection->getHints();
799 16
        $mapping = $collection->getMapping();
800 16
        $owner = $collection->getOwner();
801 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
802 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
803 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
804 16
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
805 16
        $criteria = $this->cm->merge(
806 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
807 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
808 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
809
        );
810 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
811 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
812 16
            ->setQueryArray($criteria);
813
814 16
        if (isset($mapping['sort'])) {
815 16
            $qb->sort($mapping['sort']);
816
        }
817 16
        if (isset($mapping['limit'])) {
818 1
            $qb->limit($mapping['limit']);
819
        }
820 16
        if (isset($mapping['skip'])) {
821
            $qb->skip($mapping['skip']);
822
        }
823 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
824
            $qb->slaveOkay(true);
825
        }
826 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...
827
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
828
        }
829
830 16
        return $qb->getQuery();
831
    }
832
833 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
834
    {
835 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
836 3
        $mapping = $collection->getMapping();
837 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...
838 3
        foreach ($documents as $key => $obj) {
839 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
840 1
                $collection->set($key, $obj);
841
            } else {
842 3
                $collection->add($obj);
843
            }
844
        }
845 3
    }
846
847
    /**
848
     * @param PersistentCollectionInterface $collection
849
     *
850
     * @return CursorInterface
851
     */
852 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
853
    {
854 3
        $hints = $collection->getHints();
855 3
        $mapping = $collection->getMapping();
856 3
        $repositoryMethod = $mapping['repositoryMethod'];
857 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
858 3
            ->$repositoryMethod($collection->getOwner());
859
860 3
        if ( ! $cursor instanceof CursorInterface) {
861
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
862
        }
863
864 3
        if (isset($mapping['sort'])) {
865 3
            $cursor->sort($mapping['sort']);
866
        }
867 3
        if (isset($mapping['limit'])) {
868
            $cursor->limit($mapping['limit']);
869
        }
870 3
        if (isset($mapping['skip'])) {
871
            $cursor->skip($mapping['skip']);
872
        }
873 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
874
            $cursor->slaveOkay(true);
875
        }
876 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...
877
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
878
        }
879
880 3
        return $cursor;
881
    }
882
883
    /**
884
     * Prepare a sort or projection array by converting keys, which are PHP
885
     * property names, to MongoDB field names.
886
     *
887
     * @param array $fields
888
     * @return array
889
     */
890 139
    public function prepareSortOrProjection(array $fields)
891
    {
892 139
        $preparedFields = array();
893
894 139
        foreach ($fields as $key => $value) {
895 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
896
        }
897
898 139
        return $preparedFields;
899
    }
900
901
    /**
902
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
903
     *
904
     * @param string $fieldName
905
     * @return string
906
     */
907 85
    public function prepareFieldName($fieldName)
908
    {
909 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
910
911 85
        return $fieldName;
912
    }
913
914
    /**
915
     * Adds discriminator criteria to an already-prepared query.
916
     *
917
     * This method should be used once for query criteria and not be used for
918
     * nested expressions. It should be called before
919
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
920
     *
921
     * @param array $preparedQuery
922
     * @return array
923
     */
924 494
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
925
    {
926
        /* If the class has a discriminator field, which is not already in the
927
         * criteria, inject it now. The field/values need no preparation.
928
         */
929 494
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
930 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
931 21
            if (count($discriminatorValues) === 1) {
932 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
933
            } else {
934 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
935
            }
936
        }
937
938 494
        return $preparedQuery;
939
    }
940
941
    /**
942
     * Adds filter criteria to an already-prepared query.
943
     *
944
     * This method should be used once for query criteria and not be used for
945
     * nested expressions. It should be called after
946
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
947
     *
948
     * @param array $preparedQuery
949
     * @return array
950
     */
951 495
    public function addFilterToPreparedQuery(array $preparedQuery)
952
    {
953
        /* If filter criteria exists for this class, prepare it and merge
954
         * over the existing query.
955
         *
956
         * @todo Consider recursive merging in case the filter criteria and
957
         * prepared query both contain top-level $and/$or operators.
958
         */
959 495
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
960 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
961
        }
962
963 495
        return $preparedQuery;
964
    }
965
966
    /**
967
     * Prepares the query criteria or new document object.
968
     *
969
     * PHP field names and types will be converted to those used by MongoDB.
970
     *
971
     * @param array $query
972
     * @return array
973
     */
974 528
    public function prepareQueryOrNewObj(array $query)
975
    {
976 528
        $preparedQuery = array();
977
978 528
        foreach ($query as $key => $value) {
979
            // Recursively prepare logical query clauses
980 490
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
981 20
                foreach ($value as $k2 => $v2) {
982 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
983
                }
984 20
                continue;
985
            }
986
987 490
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
988 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
989 20
                continue;
990
            }
991
992 490
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
993
994 490
            $preparedQuery[$key] = is_array($value)
995 123
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
996 490
                : Type::convertPHPToDatabaseValue($value);
997
        }
998
999 528
        return $preparedQuery;
1000
    }
1001
1002
    /**
1003
     * Prepares a query value and converts the PHP value to the database value
1004
     * if it is an identifier.
1005
     *
1006
     * It also handles converting $fieldName to the database name if they are different.
1007
     *
1008
     * @param string $fieldName
1009
     * @param mixed $value
1010
     * @param ClassMetadata $class        Defaults to $this->class
1011
     * @param boolean $prepareValue Whether or not to prepare the value
1012
     * @return array        Prepared field name and value
1013
     */
1014 521
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
1015
    {
1016 521
        $class = isset($class) ? $class : $this->class;
1017
1018
        // @todo Consider inlining calls to ClassMetadataInfo methods
1019
1020
        // Process all non-identifier fields by translating field names
1021 521
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1022 240
            $mapping = $class->fieldMappings[$fieldName];
1023 240
            $fieldName = $mapping['name'];
1024
1025 240
            if ( ! $prepareValue) {
1026 62
                return array($fieldName, $value);
1027
            }
1028
1029
            // Prepare mapped, embedded objects
1030 198
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1031 198
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1032 3
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
1033
            }
1034
1035 196
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1036
                try {
1037 5
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
1038 1
                } catch (MappingException $e) {
1039
                    // do nothing in case passed object is not mapped document
1040
                }
1041
            }
1042
1043
            // No further preparation unless we're dealing with a simple reference
1044
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1045 192
            $arrayValue = (array) $value;
1046 192
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1047 117
                return array($fieldName, $value);
1048
            }
1049
1050
            // Additional preparation for one or more simple reference values
1051 103
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1052
1053 103
            if ( ! is_array($value)) {
1054 99
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1055
            }
1056
1057
            // Objects without operators or with DBRef fields can be converted immediately
1058 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...
1059 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1060
            }
1061
1062 6
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1063
        }
1064
1065
        // Process identifier fields
1066 393
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1067 325
            $fieldName = '_id';
1068
1069 325
            if ( ! $prepareValue) {
1070 16
                return array($fieldName, $value);
1071
            }
1072
1073 311
            if ( ! is_array($value)) {
1074 288
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1075
            }
1076
1077
            // Objects without operators or with DBRef fields can be converted immediately
1078 56 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...
1079 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1080
            }
1081
1082 51
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1083
        }
1084
1085
        // No processing for unmapped, non-identifier, non-dotted field names
1086 101
        if (strpos($fieldName, '.') === false) {
1087 44
            return array($fieldName, $value);
1088
        }
1089
1090
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1091
         *
1092
         * We can limit parsing here, since at most three segments are
1093
         * significant: "fieldName.objectProperty" with an optional index or key
1094
         * for collections stored as either BSON arrays or objects.
1095
         */
1096 63
        $e = explode('.', $fieldName, 4);
1097
1098
        // No further processing for unmapped fields
1099 63
        if ( ! isset($class->fieldMappings[$e[0]])) {
1100 4
            return array($fieldName, $value);
1101
        }
1102
1103 60
        $mapping = $class->fieldMappings[$e[0]];
1104 60
        $e[0] = $mapping['name'];
1105
1106
        // Hash and raw fields will not be prepared beyond the field name
1107 60
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1108 1
            $fieldName = implode('.', $e);
1109
1110 1
            return array($fieldName, $value);
1111
        }
1112
1113 59
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1114 59
                && isset($e[2])) {
1115 1
            $objectProperty = $e[2];
1116 1
            $objectPropertyPrefix = $e[1] . '.';
1117 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1118 58
        } elseif ($e[1] != '$') {
1119 57
            $fieldName = $e[0] . '.' . $e[1];
1120 57
            $objectProperty = $e[1];
1121 57
            $objectPropertyPrefix = '';
1122 57
            $nextObjectProperty = implode('.', array_slice($e, 2));
1123 1
        } elseif (isset($e[2])) {
1124 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1125 1
            $objectProperty = $e[2];
1126 1
            $objectPropertyPrefix = $e[1] . '.';
1127 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1128
        } else {
1129 1
            $fieldName = $e[0] . '.' . $e[1];
1130
1131 1
            return array($fieldName, $value);
1132
        }
1133
1134
        // No further processing for fields without a targetDocument mapping
1135 59
        if ( ! isset($mapping['targetDocument'])) {
1136 3
            if ($nextObjectProperty) {
1137
                $fieldName .= '.'.$nextObjectProperty;
1138
            }
1139
1140 3
            return array($fieldName, $value);
1141
        }
1142
1143 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1144
1145
        // No further processing for unmapped targetDocument fields
1146 56
        if ( ! $targetClass->hasField($objectProperty)) {
1147 24
            if ($nextObjectProperty) {
1148
                $fieldName .= '.'.$nextObjectProperty;
1149
            }
1150
1151 24
            return array($fieldName, $value);
1152
        }
1153
1154 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1155 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1156
1157
        // Prepare DBRef identifiers or the mapped field's property path
1158 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1159 13
            ? $e[0] . '.$id'
1160 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1161
1162
        // Process targetDocument identifier fields
1163 35
        if ($objectPropertyIsId) {
1164 14
            if ( ! $prepareValue) {
1165 1
                return array($fieldName, $value);
1166
            }
1167
1168 13
            if ( ! is_array($value)) {
1169 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1170
            }
1171
1172
            // Objects without operators or with DBRef fields can be converted immediately
1173 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...
1174 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1175
            }
1176
1177 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1178
        }
1179
1180
        /* The property path may include a third field segment, excluding the
1181
         * collection item pointer. If present, this next object property must
1182
         * be processed recursively.
1183
         */
1184 21
        if ($nextObjectProperty) {
1185
            // Respect the targetDocument's class metadata when recursing
1186 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1187 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1188 14
                : null;
1189
1190 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1191
1192 14
            $fieldName .= '.' . $key;
1193
        }
1194
1195 21
        return array($fieldName, $value);
1196
    }
1197
1198
    /**
1199
     * Prepares a query expression.
1200
     *
1201
     * @param array|object  $expression
1202
     * @param ClassMetadata $class
1203
     * @return array
1204
     */
1205 69
    private function prepareQueryExpression($expression, $class)
1206
    {
1207 69
        foreach ($expression as $k => $v) {
1208
            // Ignore query operators whose arguments need no type conversion
1209 69
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1210 12
                continue;
1211
            }
1212
1213
            // Process query operators whose argument arrays need type conversion
1214 69
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1215 67
                foreach ($v as $k2 => $v2) {
1216 67
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1217
                }
1218 67
                continue;
1219
            }
1220
1221
            // Recursively process expressions within a $not operator
1222 14
            if ($k === '$not' && is_array($v)) {
1223 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1224 11
                continue;
1225
            }
1226
1227 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1228
        }
1229
1230 69
        return $expression;
1231
    }
1232
1233
    /**
1234
     * Checks whether the value has DBRef fields.
1235
     *
1236
     * This method doesn't check if the the value is a complete DBRef object,
1237
     * although it should return true for a DBRef. Rather, we're checking that
1238
     * the value has one or more fields for a DBref. In practice, this could be
1239
     * $elemMatch criteria for matching a DBRef.
1240
     *
1241
     * @param mixed $value
1242
     * @return boolean
1243
     */
1244 70
    private function hasDBRefFields($value)
1245
    {
1246 70
        if ( ! is_array($value) && ! is_object($value)) {
1247
            return false;
1248
        }
1249
1250 70
        if (is_object($value)) {
1251
            $value = get_object_vars($value);
1252
        }
1253
1254 70
        foreach ($value as $key => $_) {
1255 70
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1256 70
                return true;
1257
            }
1258
        }
1259
1260 69
        return false;
1261
    }
1262
1263
    /**
1264
     * Checks whether the value has query operators.
1265
     *
1266
     * @param mixed $value
1267
     * @return boolean
1268
     */
1269 74
    private function hasQueryOperators($value)
1270
    {
1271 74
        if ( ! is_array($value) && ! is_object($value)) {
1272
            return false;
1273
        }
1274
1275 74
        if (is_object($value)) {
1276
            $value = get_object_vars($value);
1277
        }
1278
1279 74
        foreach ($value as $key => $_) {
1280 74
            if (isset($key[0]) && $key[0] === '$') {
1281 74
                return true;
1282
            }
1283
        }
1284
1285 9
        return false;
1286
    }
1287
1288
    /**
1289
     * Gets the array of discriminator values for the given ClassMetadata
1290
     *
1291
     * @param ClassMetadata $metadata
1292
     * @return array
1293
     */
1294 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1295
    {
1296 21
        $discriminatorValues = array($metadata->discriminatorValue);
1297 21
        foreach ($metadata->subClasses as $className) {
1298 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1299 8
                $discriminatorValues[] = $key;
1300
            }
1301
        }
1302
1303
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1304 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...
1305 2
            $discriminatorValues[] = null;
1306
        }
1307
1308 21
        return $discriminatorValues;
1309
    }
1310
1311 582
    private function handleCollections($document, $options)
1312
    {
1313
        // Collection deletions (deletions of complete collections)
1314 582
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1315 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1316 103
                $this->cp->delete($coll, $options);
1317
            }
1318
        }
1319
        // Collection updates (deleteRows, updateRows, insertRows)
1320 582
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1321 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1322 103
                $this->cp->update($coll, $options);
1323
            }
1324
        }
1325
        // Take new snapshots from visited collections
1326 582
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1327 240
            $coll->takeSnapshot();
1328
        }
1329 582
    }
1330
1331
    /**
1332
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1333
     * Also, shard key field should be present in actual document data.
1334
     *
1335
     * @param object $document
1336
     * @param string $shardKeyField
1337
     * @param array  $actualDocumentData
1338
     *
1339
     * @throws MongoDBException
1340
     */
1341 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1342
    {
1343 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1344 9
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1345
1346 9
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1347 9
        $fieldName = $fieldMapping['fieldName'];
1348
1349 9
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1350 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1351
        }
1352
1353 7
        if (!isset($actualDocumentData[$fieldName])) {
1354
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1355
        }
1356 7
    }
1357
1358
    /**
1359
     * Get shard key aware query for single document.
1360
     *
1361
     * @param object $document
1362
     *
1363
     * @return array
1364
     */
1365 290
    private function getQueryForDocument($document)
1366
    {
1367 290
        $id = $this->uow->getDocumentIdentifier($document);
1368 290
        $id = $this->class->getDatabaseIdentifierValue($id);
1369
1370 290
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1371 288
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1372
1373 288
        return $query;
1374
    }
1375
1376
    /**
1377
     * @param array $options
1378
     *
1379
     * @return array
1380
     */
1381 583
    private function getWriteOptions(array $options = array())
1382
    {
1383 583
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1384 583
        $documentOptions = [];
1385 583
        if ($this->class->hasWriteConcern()) {
1386 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1387
        }
1388
1389 583
        return array_merge($defaultOptions, $documentOptions, $options);
1390
    }
1391
}
1392