Completed
Pull Request — master (#1393)
by Maciej
18:40
created

loadReferenceManyCollectionInverseSide()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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