Completed
Pull Request — master (#1403)
by Maciej
10:46
created

DocumentPersister::loadEmbedManyCollection()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 29
ccs 20
cts 20
cp 1
rs 6.7272
cc 7
eloc 20
nc 18
nop 1
crap 7
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\Utility\CollectionHelper;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\LockException;
30
use Doctrine\ODM\MongoDB\LockMode;
31
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
32
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
33
use Doctrine\ODM\MongoDB\Proxy\Proxy;
34
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
35
use Doctrine\ODM\MongoDB\Query\Query;
36
use Doctrine\ODM\MongoDB\Types\Type;
37
use Doctrine\ODM\MongoDB\UnitOfWork;
38
39
/**
40
 * The DocumentPersister is responsible for persisting documents.
41
 *
42
 * @since       1.0
43
 */
44
class DocumentPersister
45
{
46
    /**
47
     * The PersistenceBuilder instance.
48
     *
49
     * @var PersistenceBuilder
50
     */
51
    private $pb;
52
53
    /**
54
     * The DocumentManager instance.
55
     *
56
     * @var DocumentManager
57
     */
58
    private $dm;
59
60
    /**
61
     * The EventManager instance
62
     *
63
     * @var EventManager
64
     */
65
    private $evm;
66
67
    /**
68
     * The UnitOfWork instance.
69
     *
70
     * @var UnitOfWork
71
     */
72
    private $uow;
73
74
    /**
75
     * The ClassMetadata instance for the document type being persisted.
76
     *
77
     * @var ClassMetadata
78
     */
79
    private $class;
80
81
    /**
82
     * The MongoCollection instance for this document.
83
     *
84
     * @var \MongoCollection
85
     */
86
    private $collection;
87
88
    /**
89
     * Array of queued inserts for the persister to insert.
90
     *
91
     * @var array
92
     */
93
    private $queuedInserts = array();
94
95
    /**
96
     * Array of queued inserts for the persister to insert.
97
     *
98
     * @var array
99
     */
100
    private $queuedUpserts = array();
101
102
    /**
103
     * The CriteriaMerger instance.
104
     *
105
     * @var CriteriaMerger
106
     */
107
    private $cm;
108
109
    /**
110
     * The CollectionPersister instance.
111
     *
112
     * @var CollectionPersister
113
     */
114
    private $cp;
115
116
    /**
117
     * Initializes this instance.
118
     *
119
     * @param PersistenceBuilder $pb
120
     * @param DocumentManager $dm
121
     * @param EventManager $evm
122
     * @param UnitOfWork $uow
123
     * @param HydratorFactory $hydratorFactory
124
     * @param ClassMetadata $class
125
     * @param CriteriaMerger $cm
126
     */
127 693
    public function __construct(
128
        PersistenceBuilder $pb,
129
        DocumentManager $dm,
130
        EventManager $evm,
131
        UnitOfWork $uow,
132
        HydratorFactory $hydratorFactory,
133
        ClassMetadata $class,
134
        CriteriaMerger $cm = null
135
    ) {
136 693
        $this->pb = $pb;
137 693
        $this->dm = $dm;
138 693
        $this->evm = $evm;
139 693
        $this->cm = $cm ?: new CriteriaMerger();
140 693
        $this->uow = $uow;
141 693
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
142 693
        $this->class = $class;
143 693
        $this->collection = $dm->getDocumentCollection($class->name);
0 ignored issues
show
Documentation Bug introduced by
It seems like $dm->getDocumentCollection($class->name) of type object<Doctrine\MongoDB\Collection> is incompatible with the declared type object<MongoCollection> of property $collection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
144 693
        $this->cp = $this->uow->getCollectionPersister();
145 693
    }
146
147
    /**
148
     * @return array
149
     */
150
    public function getInserts()
151
    {
152
        return $this->queuedInserts;
153
    }
154
155
    /**
156
     * @param object $document
157
     * @return bool
158
     */
159
    public function isQueuedForInsert($document)
160
    {
161
        return isset($this->queuedInserts[spl_object_hash($document)]);
162
    }
163
164
    /**
165
     * Adds a document to the queued insertions.
166
     * The document remains queued until {@link executeInserts} is invoked.
167
     *
168
     * @param object $document The document to queue for insertion.
169
     */
170 493
    public function addInsert($document)
171
    {
172 493
        $this->queuedInserts[spl_object_hash($document)] = $document;
173 493
    }
174
175
    /**
176
     * @return array
177
     */
178
    public function getUpserts()
179
    {
180
        return $this->queuedUpserts;
181
    }
182
183
    /**
184
     * @param object $document
185
     * @return boolean
186
     */
187
    public function isQueuedForUpsert($document)
188
    {
189
        return isset($this->queuedUpserts[spl_object_hash($document)]);
190
    }
191
192
    /**
193
     * Adds a document to the queued upserts.
194
     * The document remains queued until {@link executeUpserts} is invoked.
195
     *
196
     * @param object $document The document to queue for insertion.
197
     */
198 76
    public function addUpsert($document)
199
    {
200 76
        $this->queuedUpserts[spl_object_hash($document)] = $document;
201 76
    }
202
203
    /**
204
     * Gets the ClassMetadata instance of the document class this persister is used for.
205
     *
206
     * @return ClassMetadata
207
     */
208
    public function getClassMetadata()
209
    {
210
        return $this->class;
211
    }
212
213
    /**
214
     * Executes all queued document insertions.
215
     *
216
     * Queued documents without an ID will inserted in a batch and queued
217
     * documents with an ID will be upserted individually.
218
     *
219
     * If no inserts are queued, invoking this method is a NOOP.
220
     *
221
     * @param array $options Options for batchInsert() and update() driver methods
222
     */
223 493
    public function executeInserts(array $options = array())
224
    {
225 493
        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...
226
            return;
227
        }
228
229 493
        $inserts = array();
230 493
        foreach ($this->queuedInserts as $oid => $document) {
231 493
            $data = $this->pb->prepareInsertData($document);
232
233
            // Set the initial version for each insert
234 492 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...
235 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
236 39
                if ($versionMapping['type'] === 'int') {
237 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
238 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
239 2
                } elseif ($versionMapping['type'] === 'date') {
240 2
                    $nextVersionDateTime = new \DateTime();
241 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
242 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
243
                }
244 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...
245
            }
246
247 492
            $inserts[$oid] = $data;
248
        }
249
250 492
        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...
251
            try {
252 492
                $this->collection->batchInsert($inserts, $options);
253 7
            } catch (\MongoException $e) {
254 7
                $this->queuedInserts = array();
255 7
                throw $e;
256
            }
257
        }
258
259
        /* All collections except for ones using addToSet have already been
260
         * saved. We have left these to be handled separately to avoid checking
261
         * collection for uniqueness on PHP side.
262
         */
263 492
        foreach ($this->queuedInserts as $document) {
264 492
            $this->handleCollections($document, $options);
265
        }
266
267 492
        $this->queuedInserts = array();
268 492
    }
269
270
    /**
271
     * Executes all queued document upserts.
272
     *
273
     * Queued documents with an ID are upserted individually.
274
     *
275
     * If no upserts are queued, invoking this method is a NOOP.
276
     *
277
     * @param array $options Options for batchInsert() and update() driver methods
278
     */
279 76
    public function executeUpserts(array $options = array())
280
    {
281 76
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
282
            return;
283
        }
284
285 76
        foreach ($this->queuedUpserts as $oid => $document) {
286 76
            $data = $this->pb->prepareUpsertData($document);
287
288
            // Set the initial version for each upsert
289 76 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
290 3
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
291 3
                if ($versionMapping['type'] === 'int') {
292 2
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
293 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
294 1
                } elseif ($versionMapping['type'] === 'date') {
295 1
                    $nextVersionDateTime = new \DateTime();
296 1
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
297 1
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
298
                }
299 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...
300
            }
301
302
            try {
303 76
                $this->executeUpsert($data, $options);
304 76
                $this->handleCollections($document, $options);
305 76
                unset($this->queuedUpserts[$oid]);
306
            } catch (\MongoException $e) {
307
                unset($this->queuedUpserts[$oid]);
308 76
                throw $e;
309
            }
310
        }
311 76
    }
312
313
    /**
314
     * Executes a single upsert in {@link executeInserts}
315
     *
316
     * @param array $data
317
     * @param array $options
318
     */
319 76
    private function executeUpsert(array $data, array $options)
320
    {
321 76
        $options['upsert'] = true;
322 76
        $criteria = array('_id' => $data['$set']['_id']);
323 76
        unset($data['$set']['_id']);
324
325
        // Do not send an empty $set modifier
326 76
        if (empty($data['$set'])) {
327 13
            unset($data['$set']);
328
        }
329
330
        /* If there are no modifiers remaining, we're upserting a document with
331
         * an identifier as its only field. Since a document with the identifier
332
         * may already exist, the desired behavior is "insert if not exists" and
333
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
334
         * the identifier to the same value in our criteria.
335
         *
336
         * This will fail for versions before MongoDB 2.6, which require an
337
         * empty $set modifier. The best we can do (without attempting to check
338
         * server versions in advance) is attempt the 2.6+ behavior and retry
339
         * after the relevant exception.
340
         *
341
         * See: https://jira.mongodb.org/browse/SERVER-12266
342
         */
343 76
        if (empty($data)) {
344 13
            $retry = true;
345 13
            $data = array('$set' => array('_id' => $criteria['_id']));
346
        }
347
348
        try {
349 76
            $this->collection->update($criteria, $data, $options);
350 64
            return;
351 13
        } catch (\MongoCursorException $e) {
352 13
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
353
                throw $e;
354
            }
355
        }
356
357 13
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
358 13
    }
359
360
    /**
361
     * Updates the already persisted document if it has any new changesets.
362
     *
363
     * @param object $document
364
     * @param array $options Array of options to be used with update()
365
     * @throws \Doctrine\ODM\MongoDB\LockException
366
     */
367 214
    public function update($document, array $options = array())
368
    {
369 214
        $id = $this->uow->getDocumentIdentifier($document);
370 214
        $update = $this->pb->prepareUpdateData($document);
371
372 214
        $id = $this->class->getDatabaseIdentifierValue($id);
373 214
        $query = array('_id' => $id);
374
375
        // Include versioning logic to set the new version value in the database
376
        // and to ensure the version has not changed since this document object instance
377
        // was fetched from the database
378 214
        if ($this->class->isVersioned) {
379 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
380 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
381 31
            if ($versionMapping['type'] === 'int') {
382 28
                $nextVersion = $currentVersion + 1;
383 28
                $update['$inc'][$versionMapping['name']] = 1;
384 28
                $query[$versionMapping['name']] = $currentVersion;
385 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
386 3
            } elseif ($versionMapping['type'] === 'date') {
387 3
                $nextVersion = new \DateTime();
388 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
389 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
390 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
391
            }
392
        }
393
394 214
        if ( ! empty($update)) {
395
            // Include locking logic so that if the document object in memory is currently
396
            // locked then it will remove it, otherwise it ensures the document is not locked.
397 150
            if ($this->class->isLockable) {
398 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
399 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
400 11
                if ($isLocked) {
401 2
                    $update['$unset'] = array($lockMapping['name'] => true);
402
                } else {
403 9
                    $query[$lockMapping['name']] = array('$exists' => false);
404
                }
405
            }
406
407 150
            $result = $this->collection->update($query, $update, $options);
408
409 150 View Code Duplication
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
410 5
                throw LockException::lockFailed($document);
411
            }
412
        }
413
414 210
        $this->handleCollections($document, $options);
415 210
    }
416
417
    /**
418
     * Removes document from mongo
419
     *
420
     * @param mixed $document
421
     * @param array $options Array of options to be used with remove()
422
     * @throws \Doctrine\ODM\MongoDB\LockException
423
     */
424 28
    public function delete($document, array $options = array())
425
    {
426 28
        $id = $this->uow->getDocumentIdentifier($document);
427 28
        $query = array('_id' => $this->class->getDatabaseIdentifierValue($id));
428
429 28
        if ($this->class->isLockable) {
430 2
            $query[$this->class->lockField] = array('$exists' => false);
431
        }
432
433 28
        $result = $this->collection->remove($query, $options);
434
435 28 View Code Duplication
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
436 2
            throw LockException::lockFailed($document);
437
        }
438 26
    }
439
440
    /**
441
     * Refreshes a managed document.
442
     *
443
     * @param array $id The identifier of the document.
444
     * @param object $document The document to refresh.
445
     */
446 20
    public function refresh($id, $document)
447
    {
448 20
        $data = $this->collection->findOne(array('_id' => $id));
449 20
        $data = $this->hydratorFactory->hydrate($document, $data);
450 20
        $this->uow->setOriginalDocumentData($document, $data);
451 20
    }
452
453
    /**
454
     * Finds a document by a set of criteria.
455
     *
456
     * If a scalar or MongoId is provided for $criteria, it will be used to
457
     * match an _id value.
458
     *
459
     * @param mixed   $criteria Query criteria
460
     * @param object  $document Document to load the data into. If not specified, a new document is created.
461
     * @param array   $hints    Hints for document creation
462
     * @param integer $lockMode
463
     * @param array   $sort     Sort array for Cursor::sort()
464
     * @throws \Doctrine\ODM\MongoDB\LockException
465
     * @return object|null The loaded and managed document instance or null if no document was found
466
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
467
     */
468 363
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

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

Loading history...
469
    {
470
        // TODO: remove this
471 363
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
472
            $criteria = array('_id' => $criteria);
473
        }
474
475 363
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
476 363
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
477 363
        $criteria = $this->addFilterToPreparedQuery($criteria);
478
479 363
        $cursor = $this->collection->find($criteria);
480
481 363
        if (null !== $sort) {
482 101
            $cursor->sort($this->prepareSortOrProjection($sort));
483
        }
484
485 363
        $result = $cursor->getSingleResult();
486
487 363
        if ($this->class->isLockable) {
488 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
489 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
490 1
                throw LockException::lockFailed($result);
491
            }
492
        }
493
494 362
        return $this->createDocument($result, $document, $hints);
495
    }
496
497
    /**
498
     * Finds documents by a set of criteria.
499
     *
500
     * @param array        $criteria Query criteria
501
     * @param array        $sort     Sort array for Cursor::sort()
502
     * @param integer|null $limit    Limit for Cursor::limit()
503
     * @param integer|null $skip     Skip for Cursor::skip()
504
     * @return Cursor
505
     */
506 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
507
    {
508 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
509 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
510 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
511
512 22
        $baseCursor = $this->collection->find($criteria);
513 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...
514
515 22
        if (null !== $sort) {
516 3
            $cursor->sort($sort);
517
        }
518
519 22
        if (null !== $limit) {
520 2
            $cursor->limit($limit);
521
        }
522
523 22
        if (null !== $skip) {
524 2
            $cursor->skip($skip);
525
        }
526
527 22
        return $cursor;
528
    }
529
530
    /**
531
     * Wraps the supplied base cursor in the corresponding ODM class.
532
     *
533
     * @param CursorInterface $baseCursor
534
     * @return Cursor
535
     */
536 22
    private function wrapCursor(CursorInterface $baseCursor)
537
    {
538 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
539
    }
540
541
    /**
542
     * Checks whether the given managed document exists in the database.
543
     *
544
     * @param object $document
545
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
546
     */
547 3
    public function exists($document)
548
    {
549 3
        $id = $this->class->getIdentifierObject($document);
550 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
551
    }
552
553
    /**
554
     * Locks document by storing the lock mode on the mapped lock field.
555
     *
556
     * @param object $document
557
     * @param int $lockMode
558
     */
559 5
    public function lock($document, $lockMode)
560
    {
561 5
        $id = $this->uow->getDocumentIdentifier($document);
562 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
563 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
564 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
565 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
566 5
    }
567
568
    /**
569
     * Releases any lock that exists on this document.
570
     *
571
     * @param object $document
572
     */
573 1
    public function unlock($document)
574
    {
575 1
        $id = $this->uow->getDocumentIdentifier($document);
576 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
577 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
578 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
579 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
580 1
    }
581
582
    /**
583
     * Creates or fills a single document object from an query result.
584
     *
585
     * @param object $result The query result.
586
     * @param object $document The document object to fill, if any.
587
     * @param array $hints Hints for document creation.
588
     * @return object The filled and managed document object or NULL, if the query result is empty.
589
     */
590 362
    private function createDocument($result, $document = null, array $hints = array())
591
    {
592 362
        if ($result === null) {
593 115
            return null;
594
        }
595
596 310
        if ($document !== null) {
597 37
            $hints[Query::HINT_REFRESH] = true;
598 37
            $id = $this->class->getPHPIdentifierValue($result['_id']);
599 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...
600
        }
601
602 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...
603
    }
604
605
    /**
606
     * Loads a PersistentCollection data. Used in the initialize() method.
607
     *
608
     * @param PersistentCollectionInterface $collection
609
     */
610 164
    public function loadCollection(PersistentCollectionInterface $collection)
611
    {
612 164
        $mapping = $collection->getMapping();
613 164
        switch ($mapping['association']) {
614 164
            case ClassMetadata::EMBED_MANY:
615 115
                $this->loadEmbedManyCollection($collection);
616 115
                break;
617
618 65
            case ClassMetadata::REFERENCE_MANY:
619 65
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
620 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
621
                } else {
622 62
                    if ($mapping['isOwningSide']) {
623 52
                        $this->loadReferenceManyCollectionOwningSide($collection);
624
                    } else {
625 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
626
                    }
627
                }
628 65
                break;
629
        }
630 164
    }
631
632 115
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
633
    {
634 115
        $embeddedDocuments = $collection->getMongoData();
635 115
        $mapping = $collection->getMapping();
636 115
        $owner = $collection->getOwner();
637 115
        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...
638 86
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
639 86
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
640 86
                $embeddedMetadata = $this->dm->getClassMetadata($className);
641 86
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
642
643 86
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
644
645 86
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
646 86
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
647 21
                    ? $data[$embeddedMetadata->identifier]
648 86
                    : null;
649
                
650 86
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
651 85
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
652
                }
653 86
                if (CollectionHelper::isHash($mapping['strategy'])) {
654 25
                    $collection->set($key, $embeddedDocumentObject);
655
                } else {
656 86
                    $collection->add($embeddedDocumentObject);
657
                }
658
            }
659
        }
660 115
    }
661
662 52
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
663
    {
664 52
        $hints = $collection->getHints();
665 52
        $mapping = $collection->getMapping();
666 52
        $groupedIds = array();
667
668 52
        $sorted = isset($mapping['sort']) && $mapping['sort'];
669
670 52
        foreach ($collection->getMongoData() as $key => $reference) {
671 47
            if (isset($mapping['simple']) && $mapping['simple']) {
672 4
                $className = $mapping['targetDocument'];
673 4
                $mongoId = $reference;
674
            } else {
675 43
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
676 43
                $mongoId = $reference['$id'];
677
            }
678 47
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
679
680
            // create a reference to the class and id
681 47
            $reference = $this->dm->getReference($className, $id);
682
683
            // no custom sort so add the references right now in the order they are embedded
684 47
            if ( ! $sorted) {
685 46
                if (CollectionHelper::isHash($mapping['strategy'])) {
686 2
                    $collection->set($key, $reference);
687
                } else {
688 44
                    $collection->add($reference);
689
                }
690
            }
691
692
            // only query for the referenced object if it is not already initialized or the collection is sorted
693 47
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
694 47
                $groupedIds[$className][] = $mongoId;
695
            }
696
        }
697 52
        foreach ($groupedIds as $className => $ids) {
698 35
            $class = $this->dm->getClassMetadata($className);
699 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
700 35
            $criteria = $this->cm->merge(
701 35
                array('_id' => array('$in' => array_values($ids))),
702 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
703 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
704
            );
705 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
706 35
            $cursor = $mongoCollection->find($criteria);
707 35
            if (isset($mapping['sort'])) {
708 35
                $cursor->sort($mapping['sort']);
709
            }
710 35
            if (isset($mapping['limit'])) {
711
                $cursor->limit($mapping['limit']);
712
            }
713 35
            if (isset($mapping['skip'])) {
714
                $cursor->skip($mapping['skip']);
715
            }
716 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
717
                $cursor->slaveOkay(true);
718
            }
719 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...
720
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
721
            }
722 35
            $documents = $cursor->toArray(false);
723 35
            foreach ($documents as $documentData) {
724 34
                $document = $this->uow->getById($documentData['_id'], $class);
725 34
                $data = $this->hydratorFactory->hydrate($document, $documentData);
726 34
                $this->uow->setOriginalDocumentData($document, $data);
727 34
                $document->__isInitialized__ = true;
728 34
                if ($sorted) {
729 35
                    $collection->add($document);
730
                }
731
            }
732
        }
733 52
    }
734
735 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
736
    {
737 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
738 14
        $documents = $query->execute()->toArray(false);
739 14
        foreach ($documents as $key => $document) {
740 13
            $collection->add($document);
741
        }
742 14
    }
743
744
    /**
745
     * @param PersistentCollectionInterface $collection
746
     *
747
     * @return Query
748
     */
749 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
750
    {
751 16
        $hints = $collection->getHints();
752 16
        $mapping = $collection->getMapping();
753 16
        $owner = $collection->getOwner();
754 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
755 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
756 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
757 16
        $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
758 16
        $criteria = $this->cm->merge(
759 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
760 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
761 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
762
        );
763 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
764 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
765 16
            ->setQueryArray($criteria);
766
767 16
        if (isset($mapping['sort'])) {
768 16
            $qb->sort($mapping['sort']);
769
        }
770 16
        if (isset($mapping['limit'])) {
771 1
            $qb->limit($mapping['limit']);
772
        }
773 16
        if (isset($mapping['skip'])) {
774
            $qb->skip($mapping['skip']);
775
        }
776 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
777
            $qb->slaveOkay(true);
778
        }
779 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...
780
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
781
        }
782
783 16
        return $qb->getQuery();
784
    }
785
786 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
787
    {
788 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
789 3
        $mapping = $collection->getMapping();
790 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...
791 3
        foreach ($documents as $key => $obj) {
792 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
793 1
                $collection->set($key, $obj);
794
            } else {
795 3
                $collection->add($obj);
796
            }
797
        }
798 3
    }
799
800
    /**
801
     * @param PersistentCollectionInterface $collection
802
     *
803
     * @return CursorInterface
804
     */
805 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
806
    {
807 3
        $hints = $collection->getHints();
808 3
        $mapping = $collection->getMapping();
809 3
        $repositoryMethod = $mapping['repositoryMethod'];
810 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
811 3
            ->$repositoryMethod($collection->getOwner());
812
813 3
        if ( ! $cursor instanceof CursorInterface) {
814
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
815
        }
816
817 3
        if (isset($mapping['sort'])) {
818 3
            $cursor->sort($mapping['sort']);
819
        }
820 3
        if (isset($mapping['limit'])) {
821
            $cursor->limit($mapping['limit']);
822
        }
823 3
        if (isset($mapping['skip'])) {
824
            $cursor->skip($mapping['skip']);
825
        }
826 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
827
            $cursor->slaveOkay(true);
828
        }
829 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...
830
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
831
        }
832
833 3
        return $cursor;
834
    }
835
836
    /**
837
     * Prepare a sort or projection array by converting keys, which are PHP
838
     * property names, to MongoDB field names.
839
     *
840
     * @param array $fields
841
     * @return array
842
     */
843 138
    public function prepareSortOrProjection(array $fields)
844
    {
845 138
        $preparedFields = array();
846
847 138
        foreach ($fields as $key => $value) {
848 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
849
        }
850
851 138
        return $preparedFields;
852
    }
853
854
    /**
855
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
856
     *
857
     * @param string $fieldName
858
     * @return string
859
     */
860 85
    public function prepareFieldName($fieldName)
861
    {
862 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
863
864 85
        return $fieldName;
865
    }
866
867
    /**
868
     * Adds discriminator criteria to an already-prepared query.
869
     *
870
     * This method should be used once for query criteria and not be used for
871
     * nested expressions. It should be called before
872
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
873
     *
874
     * @param array $preparedQuery
875
     * @return array
876
     */
877 488
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
878
    {
879
        /* If the class has a discriminator field, which is not already in the
880
         * criteria, inject it now. The field/values need no preparation.
881
         */
882 488
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
883 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
884 21
            if (count($discriminatorValues) === 1) {
885 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
886
            } else {
887 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
888
            }
889
        }
890
891 488
        return $preparedQuery;
892
    }
893
894
    /**
895
     * Adds filter criteria to an already-prepared query.
896
     *
897
     * This method should be used once for query criteria and not be used for
898
     * nested expressions. It should be called after
899
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
900
     *
901
     * @param array $preparedQuery
902
     * @return array
903
     */
904 489
    public function addFilterToPreparedQuery(array $preparedQuery)
905
    {
906
        /* If filter criteria exists for this class, prepare it and merge
907
         * over the existing query.
908
         *
909
         * @todo Consider recursive merging in case the filter criteria and
910
         * prepared query both contain top-level $and/$or operators.
911
         */
912 489
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
913 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
914
        }
915
916 489
        return $preparedQuery;
917
    }
918
919
    /**
920
     * Prepares the query criteria or new document object.
921
     *
922
     * PHP field names and types will be converted to those used by MongoDB.
923
     *
924
     * @param array $query
925
     * @return array
926
     */
927 522
    public function prepareQueryOrNewObj(array $query)
928
    {
929 522
        $preparedQuery = array();
930
931 522
        foreach ($query as $key => $value) {
932
            // Recursively prepare logical query clauses
933 484
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
934 20
                foreach ($value as $k2 => $v2) {
935 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
936
                }
937 20
                continue;
938
            }
939
940 484
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
941 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
942 20
                continue;
943
            }
944
945 484
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
946
947 484
            $preparedQuery[$key] = is_array($value)
948 120
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
949 484
                : Type::convertPHPToDatabaseValue($value);
950
        }
951
952 522
        return $preparedQuery;
953
    }
954
955
    /**
956
     * Prepares a query value and converts the PHP value to the database value
957
     * if it is an identifier.
958
     *
959
     * It also handles converting $fieldName to the database name if they are different.
960
     *
961
     * @param string $fieldName
962
     * @param mixed $value
963
     * @param ClassMetadata $class        Defaults to $this->class
964
     * @param boolean $prepareValue Whether or not to prepare the value
965
     * @return array        Prepared field name and value
966
     */
967 515
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
968
    {
969 515
        $class = isset($class) ? $class : $this->class;
970
971
        // @todo Consider inlining calls to ClassMetadataInfo methods
972
973
        // Process all non-identifier fields by translating field names
974 515
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
975 236
            $mapping = $class->fieldMappings[$fieldName];
976 236
            $fieldName = $mapping['name'];
977
978 236
            if ( ! $prepareValue) {
979 62
                return array($fieldName, $value);
980
            }
981
982
            // Prepare mapped, embedded objects
983 194
            if ( ! empty($mapping['embedded']) && is_object($value) &&
984 194
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
985 3
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
986
            }
987
988 192
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
989
                try {
990 5
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
991 1
                } catch (MappingException $e) {
992
                    // do nothing in case passed object is not mapped document
993
                }
994
            }
995
996
            // No further preparation unless we're dealing with a simple reference
997
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
998 188
            $arrayValue = (array) $value;
999 188
            if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) {
1000 116
                return array($fieldName, $value);
1001
            }
1002
1003
            // Additional preparation for one or more simple reference values
1004 100
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1005
1006 100
            if ( ! is_array($value)) {
1007 95
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1008
            }
1009
1010
            // Objects without operators or with DBRef fields can be converted immediately
1011 7 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1012 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1013
            }
1014
1015 7
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1016
        }
1017
1018
        // Process identifier fields
1019 390
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1020 323
            $fieldName = '_id';
1021
1022 323
            if ( ! $prepareValue) {
1023 16
                return array($fieldName, $value);
1024
            }
1025
1026 309
            if ( ! is_array($value)) {
1027 288
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1028
            }
1029
1030
            // Objects without operators or with DBRef fields can be converted immediately
1031 54 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1032 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1033
            }
1034
1035 49
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1036
        }
1037
1038
        // No processing for unmapped, non-identifier, non-dotted field names
1039 100
        if (strpos($fieldName, '.') === false) {
1040 44
            return array($fieldName, $value);
1041
        }
1042
1043
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1044
         *
1045
         * We can limit parsing here, since at most three segments are
1046
         * significant: "fieldName.objectProperty" with an optional index or key
1047
         * for collections stored as either BSON arrays or objects.
1048
         */
1049 62
        $e = explode('.', $fieldName, 4);
1050
1051
        // No further processing for unmapped fields
1052 62
        if ( ! isset($class->fieldMappings[$e[0]])) {
1053 4
            return array($fieldName, $value);
1054
        }
1055
1056 59
        $mapping = $class->fieldMappings[$e[0]];
1057 59
        $e[0] = $mapping['name'];
1058
1059
        // Hash and raw fields will not be prepared beyond the field name
1060 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1061 1
            $fieldName = implode('.', $e);
1062
1063 1
            return array($fieldName, $value);
1064
        }
1065
1066 58
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1067 58
                && isset($e[2])) {
1068 1
            $objectProperty = $e[2];
1069 1
            $objectPropertyPrefix = $e[1] . '.';
1070 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1071 57
        } elseif ($e[1] != '$') {
1072 56
            $fieldName = $e[0] . '.' . $e[1];
1073 56
            $objectProperty = $e[1];
1074 56
            $objectPropertyPrefix = '';
1075 56
            $nextObjectProperty = implode('.', array_slice($e, 2));
1076 1
        } elseif (isset($e[2])) {
1077 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1078 1
            $objectProperty = $e[2];
1079 1
            $objectPropertyPrefix = $e[1] . '.';
1080 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1081
        } else {
1082 1
            $fieldName = $e[0] . '.' . $e[1];
1083
1084 1
            return array($fieldName, $value);
1085
        }
1086
1087
        // No further processing for fields without a targetDocument mapping
1088 58
        if ( ! isset($mapping['targetDocument'])) {
1089 2
            if ($nextObjectProperty) {
1090
                $fieldName .= '.'.$nextObjectProperty;
1091
            }
1092
1093 2
            return array($fieldName, $value);
1094
        }
1095
1096 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1097
1098
        // No further processing for unmapped targetDocument fields
1099 56
        if ( ! $targetClass->hasField($objectProperty)) {
1100 24
            if ($nextObjectProperty) {
1101
                $fieldName .= '.'.$nextObjectProperty;
1102
            }
1103
1104 24
            return array($fieldName, $value);
1105
        }
1106
1107 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1108 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1109
1110
        // Prepare DBRef identifiers or the mapped field's property path
1111 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1112 13
            ? $e[0] . '.$id'
1113 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1114
1115
        // Process targetDocument identifier fields
1116 35
        if ($objectPropertyIsId) {
1117 14
            if ( ! $prepareValue) {
1118 1
                return array($fieldName, $value);
1119
            }
1120
1121 13
            if ( ! is_array($value)) {
1122 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1123
            }
1124
1125
            // Objects without operators or with DBRef fields can be converted immediately
1126 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...
1127 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1128
            }
1129
1130 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1131
        }
1132
1133
        /* The property path may include a third field segment, excluding the
1134
         * collection item pointer. If present, this next object property must
1135
         * be processed recursively.
1136
         */
1137 21
        if ($nextObjectProperty) {
1138
            // Respect the targetDocument's class metadata when recursing
1139 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1140 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1141 14
                : null;
1142
1143 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1144
1145 14
            $fieldName .= '.' . $key;
1146
        }
1147
1148 21
        return array($fieldName, $value);
1149
    }
1150
1151
    /**
1152
     * Prepares a query expression.
1153
     *
1154
     * @param array|object  $expression
1155
     * @param ClassMetadata $class
1156
     * @return array
1157
     */
1158 68
    private function prepareQueryExpression($expression, $class)
1159
    {
1160 68
        foreach ($expression as $k => $v) {
1161
            // Ignore query operators whose arguments need no type conversion
1162 68
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1163 12
                continue;
1164
            }
1165
1166
            // Process query operators whose argument arrays need type conversion
1167 68
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1168 65
                foreach ($v as $k2 => $v2) {
1169 65
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1170
                }
1171 65
                continue;
1172
            }
1173
1174
            // Recursively process expressions within a $not operator
1175 15
            if ($k === '$not' && is_array($v)) {
1176 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1177 11
                continue;
1178
            }
1179
1180 15
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1181
        }
1182
1183 68
        return $expression;
1184
    }
1185
1186
    /**
1187
     * Checks whether the value has DBRef fields.
1188
     *
1189
     * This method doesn't check if the the value is a complete DBRef object,
1190
     * although it should return true for a DBRef. Rather, we're checking that
1191
     * the value has one or more fields for a DBref. In practice, this could be
1192
     * $elemMatch criteria for matching a DBRef.
1193
     *
1194
     * @param mixed $value
1195
     * @return boolean
1196
     */
1197 69
    private function hasDBRefFields($value)
1198
    {
1199 69
        if ( ! is_array($value) && ! is_object($value)) {
1200
            return false;
1201
        }
1202
1203 69
        if (is_object($value)) {
1204
            $value = get_object_vars($value);
1205
        }
1206
1207 69
        foreach ($value as $key => $_) {
1208 69
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1209 69
                return true;
1210
            }
1211
        }
1212
1213 68
        return false;
1214
    }
1215
1216
    /**
1217
     * Checks whether the value has query operators.
1218
     *
1219
     * @param mixed $value
1220
     * @return boolean
1221
     */
1222 73
    private function hasQueryOperators($value)
1223
    {
1224 73
        if ( ! is_array($value) && ! is_object($value)) {
1225
            return false;
1226
        }
1227
1228 73
        if (is_object($value)) {
1229
            $value = get_object_vars($value);
1230
        }
1231
1232 73
        foreach ($value as $key => $_) {
1233 73
            if (isset($key[0]) && $key[0] === '$') {
1234 73
                return true;
1235
            }
1236
        }
1237
1238 9
        return false;
1239
    }
1240
1241
    /**
1242
     * Gets the array of discriminator values for the given ClassMetadata
1243
     *
1244
     * @param ClassMetadata $metadata
1245
     * @return array
1246
     */
1247 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1248
    {
1249 21
        $discriminatorValues = array($metadata->discriminatorValue);
1250 21
        foreach ($metadata->subClasses as $className) {
1251 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1252 8
                $discriminatorValues[] = $key;
1253
            }
1254
        }
1255
1256
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1257 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...
1258 2
            $discriminatorValues[] = null;
1259
        }
1260
1261 21
        return $discriminatorValues;
1262
    }
1263
1264 556
    private function handleCollections($document, $options)
1265
    {
1266
        // Collection deletions (deletions of complete collections)
1267 556
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1268 101
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1269 101
                $this->cp->delete($coll, $options);
1270
            }
1271
        }
1272
        // Collection updates (deleteRows, updateRows, insertRows)
1273 556
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1274 101
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1275 101
                $this->cp->update($coll, $options);
1276
            }
1277
        }
1278
        // Take new snapshots from visited collections
1279 556
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1280 236
            $coll->takeSnapshot();
1281
        }
1282 556
    }
1283
}
1284