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