Completed
Pull Request — master (#1419)
by Robert
42:35 queued 17:39
created

DocumentPersister::getWriteOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 0
cp 0
rs 9.4285
cc 2
eloc 6
nc 2
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\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 711
    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 711
        $this->pb = $pb;
139 711
        $this->dm = $dm;
140 711
        $this->evm = $evm;
141 711
        $this->cm = $cm ?: new CriteriaMerger();
142 711
        $this->uow = $uow;
143 711
        $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 711
        $this->class = $class;
145 711
        $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 711
        $this->cp = $this->uow->getCollectionPersister();
147 711
    }
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 507
    public function addInsert($document)
173
    {
174 507
        $this->queuedInserts[spl_object_hash($document)] = $document;
175 507
    }
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 77
    public function addUpsert($document)
201
    {
202 77
        $this->queuedUpserts[spl_object_hash($document)] = $document;
203 77
    }
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 507
    public function executeInserts(array $options = array())
226
    {
227 507
        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 507
        $inserts = array();
232 507
        $options = $this->getWriteOptions($options);
233 507
        foreach ($this->queuedInserts as $oid => $document) {
234
            $data = $this->pb->prepareInsertData($document);
235
236 506
            // Set the initial version for each insert
237 39 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 37
                if ($versionMapping['type'] === 'int') {
240 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
241 2
                    $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
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
246 39
                }
247
                $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 506
250
            $inserts[$oid] = $data;
251
        }
252 506
253
        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 506
            try {
255 8
                $this->collection->batchInsert($inserts, $options);
256 8
            } catch (\MongoException $e) {
257 8
                $this->queuedInserts = array();
258
                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 506
         */
266 506
        foreach ($this->queuedInserts as $document) {
267
            $this->handleCollections($document, $options);
268
        }
269 506
270 506
        $this->queuedInserts = array();
271
    }
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 77
     */
282
    public function executeUpserts(array $options = array())
283 77
    {
284
        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 77
288
        $options = $this->getWriteOptions($options);
289 77
        foreach ($this->queuedUpserts as $oid => $document) {
290 77
            try {
291 77
                $this->executeUpsert($document, $options);
292
                $this->handleCollections($document, $options);
293
                unset($this->queuedUpserts[$oid]);
294 77
            } catch (\MongoException $e) {
295
                unset($this->queuedUpserts[$oid]);
296
                throw $e;
297 77
            }
298
        }
299
    }
300
301
    /**
302
     * Executes a single upsert in {@link executeUpserts}
303
     *
304
     * @param object $document
305 77
     * @param array  $options
306
     */
307 77
    private function executeUpsert($document, array $options)
308 77
    {
309
        $options['upsert'] = true;
310 77
        $criteria = $this->getQueryForDocument($document);
311
312
        $data = $this->pb->prepareUpsertData($document);
313 77
314 3
        // Set the initial version for each upsert
315 3 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 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
317 2
            if ($versionMapping['type'] === 'int') {
318 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
319 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
320 1
            } elseif ($versionMapping['type'] === 'date') {
321 1
                $nextVersionDateTime = new \DateTime();
322
                $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
323 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
324
            }
325
            $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 77
        }
327 77
328
        foreach (array_keys($criteria) as $field) {
329
            unset($data['$set'][$field]);
330
        }
331 77
332 13
        // Do not send an empty $set modifier
333
        if (empty($data['$set'])) {
334
            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 77
         * See: https://jira.mongodb.org/browse/SERVER-12266
349 13
         */
350 13
        if (empty($data)) {
351
            $retry = true;
352
            $data = array('$set' => array('_id' => $criteria['_id']));
353
        }
354 77
355 65
        try {
356 13
            $this->collection->update($criteria, $data, $options);
357 13
            return;
358
        } catch (\MongoCursorException $e) {
359
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
360
                throw $e;
361
            }
362 13
        }
363 13
364
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
365
    }
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 221
     * @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 219
        $query = $this->getQueryForDocument($document);
379 219
380
        foreach (array_keys($query) as $field) {
381
            unset($update['$set'][$field]);
382 219
        }
383 91
384
        if (empty($update['$set'])) {
385
            unset($update['$set']);
386
        }
387
388
389
        // Include versioning logic to set the new version value in the database
390 219
        // and to ensure the version has not changed since this document object instance
391 31
        // was fetched from the database
392 31
        if ($this->class->isVersioned) {
393 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
394 28
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
395 28
            if ($versionMapping['type'] === 'int') {
396 28
                $nextVersion = $currentVersion + 1;
397 28
                $update['$inc'][$versionMapping['name']] = 1;
398 3
                $query[$versionMapping['name']] = $currentVersion;
399 3
                $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
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
404
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
405
            }
406 219
        }
407
408
        if ( ! empty($update)) {
409 153
            // Include locking logic so that if the document object in memory is currently
410 11
            // locked then it will remove it, otherwise it ensures the document is not locked.
411 11
            if ($this->class->isLockable) {
412 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
413 2
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
414
                if ($isLocked) {
415 9
                    $update['$unset'] = array($lockMapping['name'] => true);
416
                } else {
417
                    $query[$lockMapping['name']] = array('$exists' => false);
418
                }
419 153
            }
420
421 153
            $options = $this->getWriteOptions($options);
422 5
423
            $result = $this->collection->update($query, $update, $options);
424
425 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 215
                throw LockException::lockFailed($document);
427 215
            }
428
        }
429
430
        $this->handleCollections($document, $options);
431
    }
432
433
    /**
434
     * Removes document from mongo
435
     *
436 29
     * @param mixed $document
437
     * @param array $options Array of options to be used with remove()
438 29
     * @throws \Doctrine\ODM\MongoDB\LockException
439
     */
440 29
    public function delete($document, array $options = array())
441 2
    {
442
        $query = $this->getQueryForDocument($document);
443
444 29
        if ($this->class->isLockable) {
445
            $query[$this->class->lockField] = array('$exists' => false);
446 29
        }
447 2
448
        $options = $this->getWriteOptions($options);
449 27
450
        $result = $this->collection->remove($query, $options);
451
452 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
            throw LockException::lockFailed($document);
454
        }
455
    }
456
457
    /**
458
     * Refreshes a managed document.
459 21
     *
460
     * @param string $id
461 21
     * @param object $document The document to refresh.
462 21
     *
463 21
     * @deprecated The first argument is deprecated.
464 21
     */
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
        $query = $this->getQueryForDocument($document);
468
        $data = $this->collection->findOne($query);
469
        $data = $this->hydratorFactory->hydrate($document, $data);
470
        $this->uow->setOriginalDocumentData($document, $data);
471
    }
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 364
     * @param integer $lockMode
483
     * @param array   $sort     Sort array for Cursor::sort()
484
     * @throws \Doctrine\ODM\MongoDB\LockException
485 364
     * @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
    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 364
    {
490 364
        // TODO: remove this
491 364
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
492
            $criteria = array('_id' => $criteria);
493 364
        }
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 102
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
497
        $criteria = $this->addFilterToPreparedQuery($criteria);
498
499 364
        $cursor = $this->collection->find($criteria);
500
501 364
        if (null !== $sort) {
502 1
            $cursor->sort($this->prepareSortOrProjection($sort));
503 1
        }
504 1
505
        $result = $cursor->getSingleResult();
506
507
        if ($this->class->isLockable) {
508 363
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
509
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
510
                throw LockException::lockFailed($result);
511
            }
512
        }
513
514
        return $this->createDocument($result, $document, $hints);
515
    }
516
517
    /**
518
     * Finds documents by a set of criteria.
519
     *
520 23
     * @param array        $criteria Query criteria
521
     * @param array        $sort     Sort array for Cursor::sort()
522 23
     * @param integer|null $limit    Limit for Cursor::limit()
523 23
     * @param integer|null $skip     Skip for Cursor::skip()
524 23
     * @return Cursor
525
     */
526 23
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
527 23
    {
528
        $criteria = $this->prepareQueryOrNewObj($criteria);
529 23
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
530 3
        $criteria = $this->addFilterToPreparedQuery($criteria);
531
532
        $baseCursor = $this->collection->find($criteria);
533 23
        $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 2
535
        if (null !== $sort) {
536
            $cursor->sort($sort);
537 23
        }
538 2
539
        if (null !== $limit) {
540
            $cursor->limit($limit);
541 23
        }
542
543
        if (null !== $skip) {
544
            $cursor->skip($skip);
545
        }
546
547
        return $cursor;
548
    }
549
550 285
    /**
551
     * @param object $document
552 285
     *
553 276
     * @return array
554
     * @throws MongoDBException
555
     */
556 9
    private function getShardKeyQuery($document)
557 9
    {
558 9
        if ( ! $this->class->isSharded()) {
559
            return array();
560 9
        }
561 9
562 9
        $shardKey = $this->class->getShardKey();
563 9
        $keys = array_keys($shardKey['keys']);
564 7
        $data = $this->uow->getDocumentActualData($document);
565 7
566
        $shardKeyQueryPart = array();
567
        foreach ($keys as $key) {
568 7
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
569
            $this->guardMissingShardKey($document, $key, $data);
570
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
571
            $shardKeyQueryPart[$key] = $value;
572
        }
573
574
        return $shardKeyQueryPart;
575
    }
576
577 23
    /**
578
     * Wraps the supplied base cursor in the corresponding ODM class.
579 23
     *
580
     * @param CursorInterface $baseCursor
581
     * @return Cursor
582
     */
583
    private function wrapCursor(CursorInterface $baseCursor)
584
    {
585
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
586
    }
587
588 3
    /**
589
     * Checks whether the given managed document exists in the database.
590 3
     *
591 3
     * @param object $document
592
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
593
     */
594
    public function exists($document)
595
    {
596
        $id = $this->class->getIdentifierObject($document);
597
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
598
    }
599
600 5
    /**
601
     * Locks document by storing the lock mode on the mapped lock field.
602 5
     *
603 5
     * @param object $document
604 5
     * @param int $lockMode
605 5
     */
606 5
    public function lock($document, $lockMode)
607 5
    {
608
        $id = $this->uow->getDocumentIdentifier($document);
609
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
610
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
611
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
612
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
613
    }
614 1
615
    /**
616 1
     * Releases any lock that exists on this document.
617 1
     *
618 1
     * @param object $document
619 1
     */
620 1
    public function unlock($document)
621 1
    {
622
        $id = $this->uow->getDocumentIdentifier($document);
623
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
624
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
625
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
626
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
627
    }
628
629
    /**
630
     * Creates or fills a single document object from an query result.
631 363
     *
632
     * @param object $result The query result.
633 363
     * @param object $document The document object to fill, if any.
634 116
     * @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 310
    private function createDocument($result, $document = null, array $hints = array())
638 37
    {
639 37
        if ($result === null) {
640 37
            return null;
641
        }
642
643 310
        if ($document !== null) {
644
            $hints[Query::HINT_REFRESH] = true;
645
            $id = $this->class->getPHPIdentifierValue($result['_id']);
646
            $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
        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 166
652
    /**
653 166
     * Loads a PersistentCollection data. Used in the initialize() method.
654 166
     *
655 166
     * @param PersistentCollectionInterface $collection
656 116
     */
657 116
    public function loadCollection(PersistentCollectionInterface $collection)
658
    {
659 67
        $mapping = $collection->getMapping();
660 67
        switch ($mapping['association']) {
661 3
            case ClassMetadata::EMBED_MANY:
662
                $this->loadEmbedManyCollection($collection);
663 64
                break;
664 54
665
            case ClassMetadata::REFERENCE_MANY:
666 14
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
667
                    $this->loadReferenceManyWithRepositoryMethod($collection);
668
                } else {
669 67
                    if ($mapping['isOwningSide']) {
670
                        $this->loadReferenceManyCollectionOwningSide($collection);
671 166
                    } else {
672
                        $this->loadReferenceManyCollectionInverseSide($collection);
673 116
                    }
674
                }
675 116
                break;
676 116
        }
677 116
    }
678 116
679 87
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
680 87
    {
681 87
        $embeddedDocuments = $collection->getMongoData();
682 87
        $mapping = $collection->getMapping();
683
        $owner = $collection->getOwner();
684 87
        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
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
686 87
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
687 87
                $embeddedMetadata = $this->dm->getClassMetadata($className);
688 21
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
689 87
690
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
691 87
692 86
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
693
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
694 87
                    ? $data[$embeddedMetadata->identifier]
695 25
                    : null;
696
                
697 87
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
698
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
699
                }
700
                if (CollectionHelper::isHash($mapping['strategy'])) {
701 116
                    $collection->set($key, $embeddedDocumentObject);
702
                } else {
703 54
                    $collection->add($embeddedDocumentObject);
704
                }
705 54
            }
706 54
        }
707 54
    }
708
709 54
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
710
    {
711 54
        $hints = $collection->getHints();
712 49
        $mapping = $collection->getMapping();
713 5
        $groupedIds = array();
714 5
715
        $sorted = isset($mapping['sort']) && $mapping['sort'];
716 45
717 45
        foreach ($collection->getMongoData() as $key => $reference) {
718
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
719 49
                $className = $mapping['targetDocument'];
720
                $mongoId = $reference;
721
            } else {
722 49
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
723
                $mongoId = $reference['$id'];
724
            }
725 49
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
726 48
727 2
            // create a reference to the class and id
728
            $reference = $this->dm->getReference($className, $id);
729 46
730
            // no custom sort so add the references right now in the order they are embedded
731
            if ( ! $sorted) {
732
                if (CollectionHelper::isHash($mapping['strategy'])) {
733
                    $collection->set($key, $reference);
734 49
                } else {
735 49
                    $collection->add($reference);
736
                }
737
            }
738 54
739 35
            // only query for the referenced object if it is not already initialized or the collection is sorted
740 35
            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 35
                $groupedIds[$className][] = $mongoId;
742 35
            }
743 35
        }
744 35
        foreach ($groupedIds as $className => $ids) {
745
            $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
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
751 35
            );
752
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
753
            $cursor = $mongoCollection->find($criteria);
754 35
            if (isset($mapping['sort'])) {
755
                $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 35
                $cursor->slaveOkay(true);
765 34
            }
766 34 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 34
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
768 34
            }
769 34
            $documents = $cursor->toArray(false);
770 35
            foreach ($documents as $documentData) {
771
                $document = $this->uow->getById($documentData['_id'], $class);
772
                $data = $this->hydratorFactory->hydrate($document, $documentData);
773
                $this->uow->setOriginalDocumentData($document, $data);
774 54
                $document->__isInitialized__ = true;
775
                if ($sorted) {
776 14
                    $collection->add($document);
777
                }
778 14
            }
779 14
        }
780 14
    }
781 13
782
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
783 14
    {
784
        $query = $this->createReferenceManyInverseSideQuery($collection);
785
        $documents = $query->execute()->toArray(false);
786
        foreach ($documents as $key => $document) {
787
            $collection->add($document);
788
        }
789
    }
790 16
791
    /**
792 16
     * @param PersistentCollectionInterface $collection
793 16
     *
794 16
     * @return Query
795 16
     */
796 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
797 16
    {
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
        $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
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
808 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
809 16
        );
810
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
811 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
812 1
            ->setQueryArray($criteria);
813
814 16
        if (isset($mapping['sort'])) {
815
            $qb->sort($mapping['sort']);
816
        }
817 16
        if (isset($mapping['limit'])) {
818
            $qb->limit($mapping['limit']);
819
        }
820 16
        if (isset($mapping['skip'])) {
821
            $qb->skip($mapping['skip']);
822
        }
823
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
824 16
            $qb->slaveOkay(true);
825
        }
826 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 3
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
828
        }
829 3
830 3
        return $qb->getQuery();
831 3
    }
832 3
833 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
834 1
    {
835
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
836 3
        $mapping = $collection->getMapping();
837
        $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
        foreach ($documents as $key => $obj) {
839 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
840
                $collection->set($key, $obj);
841
            } else {
842
                $collection->add($obj);
843
            }
844
        }
845
    }
846 3
847
    /**
848 3
     * @param PersistentCollectionInterface $collection
849 3
     *
850 3
     * @return CursorInterface
851 3
     */
852 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
853
    {
854 3
        $hints = $collection->getHints();
855
        $mapping = $collection->getMapping();
856
        $repositoryMethod = $mapping['repositoryMethod'];
857
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
858 3
            ->$repositoryMethod($collection->getOwner());
859 3
860
        if ( ! $cursor instanceof CursorInterface) {
861 3
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
862
        }
863
864 3
        if (isset($mapping['sort'])) {
865
            $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
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
874 3
            $cursor->slaveOkay(true);
875
        }
876 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
        return $cursor;
881
    }
882
883
    /**
884 139
     * Prepare a sort or projection array by converting keys, which are PHP
885
     * property names, to MongoDB field names.
886 139
     *
887
     * @param array $fields
888 139
     * @return array
889 33
     */
890
    public function prepareSortOrProjection(array $fields)
891
    {
892 139
        $preparedFields = array();
893
894
        foreach ($fields as $key => $value) {
895
            $preparedFields[$this->prepareFieldName($key)] = $value;
896
        }
897
898
        return $preparedFields;
899
    }
900
901 85
    /**
902
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
903 85
     *
904
     * @param string $fieldName
905 85
     * @return string
906
     */
907
    public function prepareFieldName($fieldName)
908
    {
909
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
910
911
        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 495
     * nested expressions. It should be called before
919
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
920
     *
921
     * @param array $preparedQuery
922
     * @return array
923 495
     */
924 21
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
925 21
    {
926 13
        /* 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 10
         */
929
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
930
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
931
            if (count($discriminatorValues) === 1) {
932 495
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
933
            } else {
934
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
935
            }
936
        }
937
938
        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 496
     * nested expressions. It should be called after
946
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
947
     *
948
     * @param array $preparedQuery
949
     * @return array
950
     */
951
    public function addFilterToPreparedQuery(array $preparedQuery)
952
    {
953 496
        /* If filter criteria exists for this class, prepare it and merge
954 16
         * over the existing query.
955
         *
956
         * @todo Consider recursive merging in case the filter criteria and
957 496
         * prepared query both contain top-level $and/$or operators.
958
         */
959
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
960
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
961
        }
962
963
        return $preparedQuery;
964
    }
965
966
    /**
967
     * Prepares the query criteria or new document object.
968 529
     *
969
     * PHP field names and types will be converted to those used by MongoDB.
970 529
     *
971
     * @param array $query
972 529
     * @return array
973
     */
974 490
    public function prepareQueryOrNewObj(array $query)
975 20
    {
976 20
        $preparedQuery = array();
977
978 20
        foreach ($query as $key => $value) {
979
            // Recursively prepare logical query clauses
980
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
981 490
                foreach ($value as $k2 => $v2) {
982 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
983 20
                }
984
                continue;
985
            }
986 490
987
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
988 490
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
989 123
                continue;
990 490
            }
991
992
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
993 529
994
            $preparedQuery[$key] = is_array($value)
995
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
996
                : Type::convertPHPToDatabaseValue($value);
997
        }
998
999
        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 521
     * @param string $fieldName
1009
     * @param mixed $value
1010 521
     * @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
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
1015 521
    {
1016 240
        $class = isset($class) ? $class : $this->class;
1017 240
1018
        // @todo Consider inlining calls to ClassMetadataInfo methods
1019 240
1020 62
        // Process all non-identifier fields by translating field names
1021
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1022
            $mapping = $class->fieldMappings[$fieldName];
1023
            $fieldName = $mapping['name'];
1024 198
1025 198
            if ( ! $prepareValue) {
1026 3
                return array($fieldName, $value);
1027
            }
1028
1029 196
            // Prepare mapped, embedded objects
1030
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1031 5
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1032 1
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
1033
            }
1034
1035
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1036
                try {
1037
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
1038
                } catch (MappingException $e) {
1039 192
                    // do nothing in case passed object is not mapped document
1040 192
                }
1041 117
            }
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 103
            $arrayValue = (array) $value;
1046
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1047 103
                return array($fieldName, $value);
1048 99
            }
1049
1050
            // Additional preparation for one or more simple reference values
1051
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1052 6
1053 3
            if ( ! is_array($value)) {
1054
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1055
            }
1056 6
1057
            // Objects without operators or with DBRef fields can be converted immediately
1058 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
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1060 393
            }
1061 325
1062
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1063 325
        }
1064 16
1065
        // Process identifier fields
1066
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1067 311
            $fieldName = '_id';
1068 288
1069
            if ( ! $prepareValue) {
1070
                return array($fieldName, $value);
1071
            }
1072 56
1073 6
            if ( ! is_array($value)) {
1074
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1075
            }
1076 51
1077
            // Objects without operators or with DBRef fields can be converted immediately
1078 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
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1080 101
            }
1081 44
1082
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1083
        }
1084
1085
        // No processing for unmapped, non-identifier, non-dotted field names
1086
        if (strpos($fieldName, '.') === false) {
1087
            return array($fieldName, $value);
1088
        }
1089
1090 63
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1091
         *
1092
         * We can limit parsing here, since at most three segments are
1093 63
         * significant: "fieldName.objectProperty" with an optional index or key
1094 4
         * for collections stored as either BSON arrays or objects.
1095
         */
1096
        $e = explode('.', $fieldName, 4);
1097 60
1098 60
        // No further processing for unmapped fields
1099
        if ( ! isset($class->fieldMappings[$e[0]])) {
1100
            return array($fieldName, $value);
1101 60
        }
1102 1
1103
        $mapping = $class->fieldMappings[$e[0]];
1104 1
        $e[0] = $mapping['name'];
1105
1106
        // Hash and raw fields will not be prepared beyond the field name
1107 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1108 59
            $fieldName = implode('.', $e);
1109 1
1110 1
            return array($fieldName, $value);
1111 1
        }
1112 58
1113 57
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1114 57
                && isset($e[2])) {
1115 57
            $objectProperty = $e[2];
1116 57
            $objectPropertyPrefix = $e[1] . '.';
1117 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1118 1
        } elseif ($e[1] != '$') {
1119 1
            $fieldName = $e[0] . '.' . $e[1];
1120 1
            $objectProperty = $e[1];
1121 1
            $objectPropertyPrefix = '';
1122
            $nextObjectProperty = implode('.', array_slice($e, 2));
1123 1
        } elseif (isset($e[2])) {
1124
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1125 1
            $objectProperty = $e[2];
1126
            $objectPropertyPrefix = $e[1] . '.';
1127
            $nextObjectProperty = implode('.', array_slice($e, 3));
1128
        } else {
1129 59
            $fieldName = $e[0] . '.' . $e[1];
1130 3
1131
            return array($fieldName, $value);
1132
        }
1133
1134 3
        // No further processing for fields without a targetDocument mapping
1135
        if ( ! isset($mapping['targetDocument'])) {
1136
            if ($nextObjectProperty) {
1137 56
                $fieldName .= '.'.$nextObjectProperty;
1138
            }
1139
1140 56
            return array($fieldName, $value);
1141 24
        }
1142
1143
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1144
1145 24
        // No further processing for unmapped targetDocument fields
1146
        if ( ! $targetClass->hasField($objectProperty)) {
1147
            if ($nextObjectProperty) {
1148 35
                $fieldName .= '.'.$nextObjectProperty;
1149 35
            }
1150
1151
            return array($fieldName, $value);
1152 35
        }
1153 13
1154 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1155
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1156
1157 35
        // Prepare DBRef identifiers or the mapped field's property path
1158 14
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1159 1
            ? $e[0] . '.$id'
1160
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1161
1162 13
        // Process targetDocument identifier fields
1163 2
        if ($objectPropertyIsId) {
1164
            if ( ! $prepareValue) {
1165
                return array($fieldName, $value);
1166
            }
1167 12
1168 3
            if ( ! is_array($value)) {
1169
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1170
            }
1171 12
1172
            // Objects without operators or with DBRef fields can be converted immediately
1173 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
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1175
            }
1176
1177
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1178 21
        }
1179
1180 14
        /* The property path may include a third field segment, excluding the
1181 8
         * collection item pointer. If present, this next object property must
1182 14
         * be processed recursively.
1183
         */
1184 14
        if ($nextObjectProperty) {
1185
            // Respect the targetDocument's class metadata when recursing
1186 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1187
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1188
                : null;
1189 21
1190
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1191
1192
            $fieldName .= '.' . $key;
1193
        }
1194
1195
        return array($fieldName, $value);
1196
    }
1197
1198
    /**
1199 69
     * Prepares a query expression.
1200
     *
1201 69
     * @param array|object  $expression
1202
     * @param ClassMetadata $class
1203 69
     * @return array
1204 12
     */
1205
    private function prepareQueryExpression($expression, $class)
1206
    {
1207
        foreach ($expression as $k => $v) {
1208 69
            // Ignore query operators whose arguments need no type conversion
1209 67
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1210 67
                continue;
1211
            }
1212 67
1213
            // Process query operators whose argument arrays need type conversion
1214
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1215
                foreach ($v as $k2 => $v2) {
1216 14
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1217 11
                }
1218 11
                continue;
1219
            }
1220
1221 14
            // Recursively process expressions within a $not operator
1222
            if ($k === '$not' && is_array($v)) {
1223
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1224 69
                continue;
1225
            }
1226
1227
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1228
        }
1229
1230
        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 70
     * the value has one or more fields for a DBref. In practice, this could be
1239
     * $elemMatch criteria for matching a DBRef.
1240 70
     *
1241
     * @param mixed $value
1242
     * @return boolean
1243
     */
1244 70
    private function hasDBRefFields($value)
1245
    {
1246
        if ( ! is_array($value) && ! is_object($value)) {
1247
            return false;
1248 70
        }
1249 70
1250 70
        if (is_object($value)) {
1251
            $value = get_object_vars($value);
1252
        }
1253
1254 69
        foreach ($value as $key => $_) {
1255
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1256
                return true;
1257
            }
1258
        }
1259
1260
        return false;
1261
    }
1262
1263 74
    /**
1264
     * Checks whether the value has query operators.
1265 74
     *
1266
     * @param mixed $value
1267
     * @return boolean
1268
     */
1269 74
    private function hasQueryOperators($value)
1270
    {
1271
        if ( ! is_array($value) && ! is_object($value)) {
1272
            return false;
1273 74
        }
1274 74
1275 74
        if (is_object($value)) {
1276
            $value = get_object_vars($value);
1277
        }
1278
1279 9
        foreach ($value as $key => $_) {
1280
            if (isset($key[0]) && $key[0] === '$') {
1281
                return true;
1282
            }
1283
        }
1284
1285
        return false;
1286
    }
1287
1288 21
    /**
1289
     * Gets the array of discriminator values for the given ClassMetadata
1290 21
     *
1291 21
     * @param ClassMetadata $metadata
1292 8
     * @return array
1293 8
     */
1294
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1295
    {
1296
        $discriminatorValues = array($metadata->discriminatorValue);
1297
        foreach ($metadata->subClasses as $className) {
1298 21
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1299 2
                $discriminatorValues[] = $key;
1300
            }
1301
        }
1302 21
1303
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1304 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 571
            $discriminatorValues[] = null;
1306
        }
1307
1308 571
        return $discriminatorValues;
1309 103
    }
1310 103
1311
    private function handleCollections($document, $options)
1312
    {
1313
        // Collection deletions (deletions of complete collections)
1314 571
        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 571
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1321 240
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1322
                $this->cp->update($coll, $options);
1323 571
            }
1324
        }
1325
        // Take new snapshots from visited collections
1326
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1327
            $coll->takeSnapshot();
1328
        }
1329
    }
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 9
     * @param object $document
1336
     * @param string $shardKeyField
1337 9
     * @param array  $actualDocumentData
1338 9
     *
1339
     * @throws MongoDBException
1340 9
     */
1341 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1342
    {
1343 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1344 2
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1345
1346
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1347 7
        $fieldName = $fieldMapping['fieldName'];
1348
1349
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1350 7
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1351
        }
1352
1353
        if (!isset($actualDocumentData[$fieldName])) {
1354
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1355
        }
1356
    }
1357
1358
    /**
1359 282
     * Get shard key aware query for single document.
1360
     *
1361 282
     * @param object $document
1362 282
     *
1363
     * @return array
1364 282
     */
1365 280
    private function getQueryForDocument($document)
1366
    {
1367 280
        $id = $this->uow->getDocumentIdentifier($document);
1368
        $id = $this->class->getDatabaseIdentifierValue($id);
1369
1370
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1371
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1372
1373
        return $query;
1374
    }
1375
1376
    /**
1377
     * @param array $options
1378
     *
1379
     * @return array
1380
     */
1381
    private function getWriteOptions(array $options = array())
1382
    {
1383
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1384
        $documentOptions = [];
1385
        if ($this->class->hasWriteConcern()) {
1386
            $documentOptions['w'] = $this->class->getWriteConcern();
1387
        }
1388
1389
        return array_merge($defaultOptions, $documentOptions, $options);
1390
    }
1391
}
1392