Completed
Pull Request — master (#1410)
by Piotr
09:50
created

DocumentPersister::load()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8.0189
Metric Value
dl 0
loc 28
ccs 14
cts 15
cp 0.9333
rs 5.3846
cc 8
eloc 15
nc 12
nop 5
crap 8.0189
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 696
    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 696
        $this->pb = $pb;
138 696
        $this->dm = $dm;
139 696
        $this->evm = $evm;
140 696
        $this->cm = $cm ?: new CriteriaMerger();
141 696
        $this->uow = $uow;
142 696
        $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 696
        $this->class = $class;
144 696
        $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 696
        $this->cp = $this->uow->getCollectionPersister();
146 696
    }
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 497
    public function addInsert($document)
172
    {
173 497
        $this->queuedInserts[spl_object_hash($document)] = $document;
174 497
    }
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 497
    public function executeInserts(array $options = array())
225
    {
226 497
        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 497
        $inserts = array();
231 497
        foreach ($this->queuedInserts as $oid => $document) {
232 497
            $data = $this->pb->prepareInsertData($document);
233
234
            // Set the initial version for each insert
235 496 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 496
            $inserts[$oid] = $data;
249
        }
250
251 496
        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 496
                $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 496
        foreach ($this->queuedInserts as $document) {
265 496
            $this->handleCollections($document, $options);
266
        }
267
268 496
        $this->queuedInserts = array();
269 496
    }
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 164
    public function loadCollection(PersistentCollectionInterface $collection)
612
    {
613 164
        $mapping = $collection->getMapping();
614 164
        switch ($mapping['association']) {
615 164
            case ClassMetadata::EMBED_MANY:
616 114
                $this->loadEmbedManyCollection($collection);
617 114
                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 164
    }
632
633 114
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
634
    {
635 114
        $embeddedDocuments = $collection->getMongoData();
636 114
        $mapping = $collection->getMapping();
637 114
        $owner = $collection->getOwner();
638 114
        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 85
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
640 85
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
641 85
                $embeddedMetadata = $this->dm->getClassMetadata($className);
642 85
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
643
644 85
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
645
646 85
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
647 85
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
648 21
                    ? $data[$embeddedMetadata->identifier]
649 85
                    : null;
650
651 85
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
652 85
                if (CollectionHelper::isHash($mapping['strategy'])) {
653 25
                    $collection->set($key, $embeddedDocumentObject);
654
                } else {
655 85
                    $collection->add($embeddedDocumentObject);
656
                }
657
            }
658
        }
659 114
    }
660
661 53
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
662
    {
663 53
        $hints = $collection->getHints();
664 53
        $mapping = $collection->getMapping();
665 53
        $groupedIds = array();
666
667 53
        $sorted = isset($mapping['sort']) && $mapping['sort'];
668
669 53
        foreach ($collection->getMongoData() as $key => $reference) {
670 48
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
671 5
                $className = $mapping['targetDocument'];
672 5
                $mongoId = $reference;
673
            } else {
674 44
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
675 44
                $mongoId = $reference['$id'];
676
            }
677 48
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
678
679
            // create a reference to the class and id
680 48
            $reference = $this->dm->getReference($className, $id);
681
682
            // no custom sort so add the references right now in the order they are embedded
683 48
            if ( ! $sorted) {
684 47
                if (CollectionHelper::isHash($mapping['strategy'])) {
685 2
                    $collection->set($key, $reference);
686
                } else {
687 45
                    $collection->add($reference);
688
                }
689
            }
690
691
            // only query for the referenced object if it is not already initialized or the collection is sorted
692 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...
693 48
                $groupedIds[$className][] = $mongoId;
694
            }
695
        }
696 53
        foreach ($groupedIds as $className => $ids) {
697 35
            $class = $this->dm->getClassMetadata($className);
698 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
699 35
            $criteria = $this->cm->merge(
700 35
                array('_id' => array('$in' => array_values($ids))),
701 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
702 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
703
            );
704 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
705 35
            $cursor = $mongoCollection->find($criteria);
706 35
            if (isset($mapping['sort'])) {
707 35
                $cursor->sort($mapping['sort']);
708
            }
709 35
            if (isset($mapping['limit'])) {
710
                $cursor->limit($mapping['limit']);
711
            }
712 35
            if (isset($mapping['skip'])) {
713
                $cursor->skip($mapping['skip']);
714
            }
715 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
716
                $cursor->slaveOkay(true);
717
            }
718 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...
719
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
720
            }
721 35
            $documents = $cursor->toArray(false);
722 35
            foreach ($documents as $documentData) {
723 34
                $document = $this->uow->getById($documentData['_id'], $class);
724 34
                $data = $this->hydratorFactory->hydrate($document, $documentData);
725 34
                $this->uow->setOriginalDocumentData($document, $data);
726 34
                $document->__isInitialized__ = true;
727 34
                if ($sorted) {
728 35
                    $collection->add($document);
729
                }
730
            }
731
        }
732 53
    }
733
734 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
735
    {
736 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
737 14
        $documents = $query->execute()->toArray(false);
738 14
        foreach ($documents as $key => $document) {
739 13
            $collection->add($document);
740
        }
741 14
    }
742
743
    /**
744
     * @param PersistentCollectionInterface $collection
745
     *
746
     * @return Query
747
     */
748 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
749
    {
750 16
        $hints = $collection->getHints();
751 16
        $mapping = $collection->getMapping();
752 16
        $owner = $collection->getOwner();
753 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
754 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
755 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
756 16
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
757 16
        $criteria = $this->cm->merge(
758 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
759 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
760 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
761
        );
762 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
763 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
764 16
            ->setQueryArray($criteria);
765
766 16
        if (isset($mapping['sort'])) {
767 16
            $qb->sort($mapping['sort']);
768
        }
769 16
        if (isset($mapping['limit'])) {
770 1
            $qb->limit($mapping['limit']);
771
        }
772 16
        if (isset($mapping['skip'])) {
773
            $qb->skip($mapping['skip']);
774
        }
775 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
776
            $qb->slaveOkay(true);
777
        }
778 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...
779
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
780
        }
781
782 16
        return $qb->getQuery();
783
    }
784
785 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
786
    {
787 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
788 3
        $mapping = $collection->getMapping();
789 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...
790 3
        foreach ($documents as $key => $obj) {
791 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
792 1
                $collection->set($key, $obj);
793
            } else {
794 3
                $collection->add($obj);
795
            }
796
        }
797 3
    }
798
799
    /**
800
     * @param PersistentCollectionInterface $collection
801
     *
802
     * @return CursorInterface
803
     */
804 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
805
    {
806 3
        $hints = $collection->getHints();
807 3
        $mapping = $collection->getMapping();
808 3
        $repositoryMethod = $mapping['repositoryMethod'];
809 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
810 3
            ->$repositoryMethod($collection->getOwner());
811
812 3
        if ( ! $cursor instanceof CursorInterface) {
813
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
814
        }
815
816 3
        if (isset($mapping['sort'])) {
817 3
            $cursor->sort($mapping['sort']);
818
        }
819 3
        if (isset($mapping['limit'])) {
820
            $cursor->limit($mapping['limit']);
821
        }
822 3
        if (isset($mapping['skip'])) {
823
            $cursor->skip($mapping['skip']);
824
        }
825 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
826
            $cursor->slaveOkay(true);
827
        }
828 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...
829
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
830
        }
831
832 3
        return $cursor;
833
    }
834
835
    /**
836
     * Prepare a sort or projection array by converting keys, which are PHP
837
     * property names, to MongoDB field names.
838
     *
839
     * @param array $fields
840
     * @return array
841
     */
842 139
    public function prepareSortOrProjection(array $fields)
843
    {
844 139
        $preparedFields = array();
845
846 139
        foreach ($fields as $key => $value) {
847 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
848
        }
849
850 139
        return $preparedFields;
851
    }
852
853
    /**
854
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
855
     *
856
     * @param string $fieldName
857
     * @return string
858
     */
859 85
    public function prepareFieldName($fieldName)
860
    {
861 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
862
863 85
        return $fieldName;
864
    }
865
866
    /**
867
     * Adds discriminator criteria to an already-prepared query.
868
     *
869
     * This method should be used once for query criteria and not be used for
870
     * nested expressions. It should be called before
871
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
872
     *
873
     * @param array $preparedQuery
874
     * @return array
875
     */
876 492
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
877
    {
878
        /* If the class has a discriminator field, which is not already in the
879
         * criteria, inject it now. The field/values need no preparation.
880
         */
881 492
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
882 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
883 21
            if (count($discriminatorValues) === 1) {
884 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
885
            } else {
886 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
887
            }
888
        }
889
890 492
        return $preparedQuery;
891
    }
892
893
    /**
894
     * Adds filter criteria to an already-prepared query.
895
     *
896
     * This method should be used once for query criteria and not be used for
897
     * nested expressions. It should be called after
898
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
899
     *
900
     * @param array $preparedQuery
901
     * @return array
902
     */
903 493
    public function addFilterToPreparedQuery(array $preparedQuery)
904
    {
905
        /* If filter criteria exists for this class, prepare it and merge
906
         * over the existing query.
907
         *
908
         * @todo Consider recursive merging in case the filter criteria and
909
         * prepared query both contain top-level $and/$or operators.
910
         */
911 493
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
912 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
913
        }
914
915 493
        return $preparedQuery;
916
    }
917
918
    /**
919
     * Prepares the query criteria or new document object.
920
     *
921
     * PHP field names and types will be converted to those used by MongoDB.
922
     *
923
     * @param array $query
924
     * @return array
925
     */
926 526
    public function prepareQueryOrNewObj(array $query)
927
    {
928 526
        $preparedQuery = array();
929
930 526
        foreach ($query as $key => $value) {
931
            // Recursively prepare logical query clauses
932 488
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
933 20
                foreach ($value as $k2 => $v2) {
934 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
935
                }
936 20
                continue;
937
            }
938
939 488
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
940 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
941 20
                continue;
942
            }
943
944 488
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
945
946 488
            $preparedQuery[$key] = is_array($value)
947 122
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
948 488
                : Type::convertPHPToDatabaseValue($value);
949
        }
950
951 526
        return $preparedQuery;
952
    }
953
954
    /**
955
     * Prepares a query value and converts the PHP value to the database value
956
     * if it is an identifier.
957
     *
958
     * It also handles converting $fieldName to the database name if they are different.
959
     *
960
     * @param string $fieldName
961
     * @param mixed $value
962
     * @param ClassMetadata $class        Defaults to $this->class
963
     * @param boolean $prepareValue Whether or not to prepare the value
964
     * @return array        Prepared field name and value
965
     */
966 519
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
967
    {
968 519
        $class = isset($class) ? $class : $this->class;
969
970
        // @todo Consider inlining calls to ClassMetadataInfo methods
971
972
        // Process all non-identifier fields by translating field names
973 519
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
974 240
            $mapping = $class->fieldMappings[$fieldName];
975 240
            $fieldName = $mapping['name'];
976
977 240
            if ( ! $prepareValue) {
978 62
                return array($fieldName, $value);
979
            }
980
981
            // Prepare mapped, embedded objects
982 198
            if ( ! empty($mapping['embedded']) && is_object($value) &&
983 198
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
984 3
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
985
            }
986
987 196
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
988
                try {
989 5
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
990 1
                } catch (MappingException $e) {
991
                    // do nothing in case passed object is not mapped document
992
                }
993
            }
994
995
            // No further preparation unless we're dealing with a simple reference
996
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
997 192
            $arrayValue = (array) $value;
998 192
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
999 117
                return array($fieldName, $value);
1000
            }
1001
1002
            // Additional preparation for one or more simple reference values
1003 103
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1004
1005 103
            if ( ! is_array($value)) {
1006 99
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1007
            }
1008
1009
            // Objects without operators or with DBRef fields can be converted immediately
1010 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...
1011 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1012
            }
1013
1014 6
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1015
        }
1016
1017
        // Process identifier fields
1018 391
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1019 323
            $fieldName = '_id';
1020
1021 323
            if ( ! $prepareValue) {
1022 16
                return array($fieldName, $value);
1023
            }
1024
1025 309
            if ( ! is_array($value)) {
1026 287
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1027
            }
1028
1029
            // Objects without operators or with DBRef fields can be converted immediately
1030 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...
1031 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1032
            }
1033
1034 50
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1035
        }
1036
1037
        // No processing for unmapped, non-identifier, non-dotted field names
1038 101
        if (strpos($fieldName, '.') === false) {
1039 44
            return array($fieldName, $value);
1040
        }
1041
1042
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1043
         *
1044
         * We can limit parsing here, since at most three segments are
1045
         * significant: "fieldName.objectProperty" with an optional index or key
1046
         * for collections stored as either BSON arrays or objects.
1047
         */
1048 63
        $e = explode('.', $fieldName, 4);
1049
1050
        // No further processing for unmapped fields
1051 63
        if ( ! isset($class->fieldMappings[$e[0]])) {
1052 4
            return array($fieldName, $value);
1053
        }
1054
1055 60
        $mapping = $class->fieldMappings[$e[0]];
1056 60
        $e[0] = $mapping['name'];
1057
1058
        // Hash and raw fields will not be prepared beyond the field name
1059 60
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1060 1
            $fieldName = implode('.', $e);
1061
1062 1
            return array($fieldName, $value);
1063
        }
1064
1065 59
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1066 59
                && isset($e[2])) {
1067 1
            $objectProperty = $e[2];
1068 1
            $objectPropertyPrefix = $e[1] . '.';
1069 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1070 58
        } elseif ($e[1] != '$') {
1071 57
            $fieldName = $e[0] . '.' . $e[1];
1072 57
            $objectProperty = $e[1];
1073 57
            $objectPropertyPrefix = '';
1074 57
            $nextObjectProperty = implode('.', array_slice($e, 2));
1075 1
        } elseif (isset($e[2])) {
1076 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1077 1
            $objectProperty = $e[2];
1078 1
            $objectPropertyPrefix = $e[1] . '.';
1079 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1080
        } else {
1081 1
            $fieldName = $e[0] . '.' . $e[1];
1082
1083 1
            return array($fieldName, $value);
1084
        }
1085
1086
        // No further processing for fields without a targetDocument mapping
1087 59
        if ( ! isset($mapping['targetDocument'])) {
1088 3
            if ($nextObjectProperty) {
1089
                $fieldName .= '.'.$nextObjectProperty;
1090
            }
1091
1092 3
            return array($fieldName, $value);
1093
        }
1094
1095 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1096
1097
        // No further processing for unmapped targetDocument fields
1098 56
        if ( ! $targetClass->hasField($objectProperty)) {
1099 24
            if ($nextObjectProperty) {
1100
                $fieldName .= '.'.$nextObjectProperty;
1101
            }
1102
1103 24
            return array($fieldName, $value);
1104
        }
1105
1106 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1107 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1108
1109
        // Prepare DBRef identifiers or the mapped field's property path
1110 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1111 13
            ? $e[0] . '.$id'
1112 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1113
1114
        // Process targetDocument identifier fields
1115 35
        if ($objectPropertyIsId) {
1116 14
            if ( ! $prepareValue) {
1117 1
                return array($fieldName, $value);
1118
            }
1119
1120 13
            if ( ! is_array($value)) {
1121 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1122
            }
1123
1124
            // Objects without operators or with DBRef fields can be converted immediately
1125 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...
1126 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1127
            }
1128
1129 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1130
        }
1131
1132
        /* The property path may include a third field segment, excluding the
1133
         * collection item pointer. If present, this next object property must
1134
         * be processed recursively.
1135
         */
1136 21
        if ($nextObjectProperty) {
1137
            // Respect the targetDocument's class metadata when recursing
1138 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1139 8
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1140 14
                : null;
1141
1142 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1143
1144 14
            $fieldName .= '.' . $key;
1145
        }
1146
1147 21
        return array($fieldName, $value);
1148
    }
1149
1150
    /**
1151
     * Prepares a query expression.
1152
     *
1153
     * @param array|object  $expression
1154
     * @param ClassMetadata $class
1155
     * @return array
1156
     */
1157 68
    private function prepareQueryExpression($expression, $class)
1158
    {
1159 68
        foreach ($expression as $k => $v) {
1160
            // Ignore query operators whose arguments need no type conversion
1161 68
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1162 12
                continue;
1163
            }
1164
1165
            // Process query operators whose argument arrays need type conversion
1166 68
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1167 66
                foreach ($v as $k2 => $v2) {
1168 66
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1169
                }
1170 66
                continue;
1171
            }
1172
1173
            // Recursively process expressions within a $not operator
1174 14
            if ($k === '$not' && is_array($v)) {
1175 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1176 11
                continue;
1177
            }
1178
1179 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1180
        }
1181
1182 68
        return $expression;
1183
    }
1184
1185
    /**
1186
     * Checks whether the value has DBRef fields.
1187
     *
1188
     * This method doesn't check if the the value is a complete DBRef object,
1189
     * although it should return true for a DBRef. Rather, we're checking that
1190
     * the value has one or more fields for a DBref. In practice, this could be
1191
     * $elemMatch criteria for matching a DBRef.
1192
     *
1193
     * @param mixed $value
1194
     * @return boolean
1195
     */
1196 69
    private function hasDBRefFields($value)
1197
    {
1198 69
        if ( ! is_array($value) && ! is_object($value)) {
1199
            return false;
1200
        }
1201
1202 69
        if (is_object($value)) {
1203
            $value = get_object_vars($value);
1204
        }
1205
1206 69
        foreach ($value as $key => $_) {
1207 69
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1208 69
                return true;
1209
            }
1210
        }
1211
1212 68
        return false;
1213
    }
1214
1215
    /**
1216
     * Checks whether the value has query operators.
1217
     *
1218
     * @param mixed $value
1219
     * @return boolean
1220
     */
1221 73
    private function hasQueryOperators($value)
1222
    {
1223 73
        if ( ! is_array($value) && ! is_object($value)) {
1224
            return false;
1225
        }
1226
1227 73
        if (is_object($value)) {
1228
            $value = get_object_vars($value);
1229
        }
1230
1231 73
        foreach ($value as $key => $_) {
1232 73
            if (isset($key[0]) && $key[0] === '$') {
1233 73
                return true;
1234
            }
1235
        }
1236
1237 9
        return false;
1238
    }
1239
1240
    /**
1241
     * Gets the array of discriminator values for the given ClassMetadata
1242
     *
1243
     * @param ClassMetadata $metadata
1244
     * @return array
1245
     */
1246 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1247
    {
1248 21
        $discriminatorValues = array($metadata->discriminatorValue);
1249 21
        foreach ($metadata->subClasses as $className) {
1250 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1251 8
                $discriminatorValues[] = $key;
1252
            }
1253
        }
1254
1255
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1256 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...
1257 2
            $discriminatorValues[] = null;
1258
        }
1259
1260 21
        return $discriminatorValues;
1261
    }
1262
1263 560
    private function handleCollections($document, $options)
1264
    {
1265
        // Collection deletions (deletions of complete collections)
1266 560
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1267 103
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1268 103
                $this->cp->delete($coll, $options);
1269
            }
1270
        }
1271
        // Collection updates (deleteRows, updateRows, insertRows)
1272 560
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1273 103
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1274 103
                $this->cp->update($coll, $options);
1275
            }
1276
        }
1277
        // Take new snapshots from visited collections
1278 560
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1279 239
            $coll->takeSnapshot();
1280
        }
1281 560
    }
1282
}
1283