Completed
Pull Request — master (#1395)
by Andreas
08:36
created

DocumentPersister::executeUpsert()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 40
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087
Metric Value
dl 0
loc 40
ccs 15
cts 16
cp 0.9375
rs 8.439
cc 6
eloc 16
nc 12
nop 2
crap 6.0087
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\Common\Persistence\Mapping\MappingException;
24
use Doctrine\MongoDB\CursorInterface;
25
use Doctrine\ODM\MongoDB\Cursor;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\LockException;
30
use Doctrine\ODM\MongoDB\LockMode;
31
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
32
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
33
use Doctrine\ODM\MongoDB\Proxy\Proxy;
34
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
35
use Doctrine\ODM\MongoDB\Query\Query;
36
use Doctrine\ODM\MongoDB\Types\Type;
37
use Doctrine\ODM\MongoDB\UnitOfWork;
38
39
/**
40
 * The DocumentPersister is responsible for persisting documents.
41
 *
42
 * @since       1.0
43
 */
44
class DocumentPersister
45
{
46
    /**
47
     * The PersistenceBuilder instance.
48
     *
49
     * @var PersistenceBuilder
50
     */
51
    private $pb;
52
53
    /**
54
     * The DocumentManager instance.
55
     *
56
     * @var DocumentManager
57
     */
58
    private $dm;
59
60
    /**
61
     * The EventManager instance
62
     *
63
     * @var EventManager
64
     */
65
    private $evm;
66
67
    /**
68
     * The UnitOfWork instance.
69
     *
70
     * @var UnitOfWork
71
     */
72
    private $uow;
73
74
    /**
75
     * The ClassMetadata instance for the document type being persisted.
76
     *
77
     * @var ClassMetadata
78
     */
79
    private $class;
80
81
    /**
82
     * The MongoCollection instance for this document.
83
     *
84
     * @var \MongoCollection
85
     */
86
    private $collection;
87
88
    /**
89
     * Array of queued inserts for the persister to insert.
90
     *
91
     * @var array
92
     */
93
    private $queuedInserts = array();
94
95
    /**
96
     * Array of queued inserts for the persister to insert.
97
     *
98
     * @var array
99
     */
100
    private $queuedUpserts = array();
101
102
    /**
103
     * The CriteriaMerger instance.
104
     *
105
     * @var CriteriaMerger
106
     */
107
    private $cm;
108
109
    /**
110
     * The CollectionPersister instance.
111
     *
112
     * @var CollectionPersister
113
     */
114
    private $cp;
115
116
    /**
117
     * Initializes this instance.
118
     *
119
     * @param PersistenceBuilder $pb
120
     * @param DocumentManager $dm
121
     * @param EventManager $evm
122
     * @param UnitOfWork $uow
123
     * @param HydratorFactory $hydratorFactory
124
     * @param ClassMetadata $class
125
     * @param CriteriaMerger $cm
126
     */
127 691
    public function __construct(
128
        PersistenceBuilder $pb,
129
        DocumentManager $dm,
130
        EventManager $evm,
131
        UnitOfWork $uow,
132
        HydratorFactory $hydratorFactory,
133
        ClassMetadata $class,
134
        CriteriaMerger $cm = null
135
    ) {
136 691
        $this->pb = $pb;
137 691
        $this->dm = $dm;
138 691
        $this->evm = $evm;
139 691
        $this->cm = $cm ?: new CriteriaMerger();
140 691
        $this->uow = $uow;
141 691
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

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

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

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

Loading history...
144 691
        $this->cp = $this->uow->getCollectionPersister();
145 691
    }
146
147
    /**
148
     * @return array
149
     */
150
    public function getInserts()
151
    {
152
        return $this->queuedInserts;
153
    }
154
155
    /**
156
     * @param object $document
157
     * @return bool
158
     */
159
    public function isQueuedForInsert($document)
160
    {
161
        return isset($this->queuedInserts[spl_object_hash($document)]);
162
    }
163
164
    /**
165
     * Adds a document to the queued insertions.
166
     * The document remains queued until {@link executeInserts} is invoked.
167
     *
168
     * @param object $document The document to queue for insertion.
169
     */
170 492
    public function addInsert($document)
171
    {
172 492
        $this->queuedInserts[spl_object_hash($document)] = $document;
173 492
    }
174
175
    /**
176
     * @return array
177
     */
178
    public function getUpserts()
179
    {
180
        return $this->queuedUpserts;
181
    }
182
183
    /**
184
     * @param object $document
185
     * @return boolean
186
     */
187
    public function isQueuedForUpsert($document)
188
    {
189
        return isset($this->queuedUpserts[spl_object_hash($document)]);
190
    }
191
192
    /**
193
     * Adds a document to the queued upserts.
194
     * The document remains queued until {@link executeUpserts} is invoked.
195
     *
196
     * @param object $document The document to queue for insertion.
197
     */
198 76
    public function addUpsert($document)
199
    {
200 76
        $this->queuedUpserts[spl_object_hash($document)] = $document;
201 76
    }
202
203
    /**
204
     * Gets the ClassMetadata instance of the document class this persister is used for.
205
     *
206
     * @return ClassMetadata
207
     */
208
    public function getClassMetadata()
209
    {
210
        return $this->class;
211
    }
212
213
    /**
214
     * Executes all queued document insertions.
215
     *
216
     * Queued documents without an ID will inserted in a batch and queued
217
     * documents with an ID will be upserted individually.
218
     *
219
     * If no inserts are queued, invoking this method is a NOOP.
220
     *
221
     * @param array $options Options for batchInsert() and update() driver methods
222
     */
223 492
    public function executeInserts(array $options = array())
224
    {
225 492
        if ( ! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
226
            return;
227
        }
228
229 492
        $inserts = array();
230 492
        foreach ($this->queuedInserts as $oid => $document) {
231 492
            $data = $this->pb->prepareInsertData($document);
232
233
            // Set the initial version for each insert
234 491 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
235 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
236 39
                if ($versionMapping['type'] === 'int') {
237 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
238 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
239 2
                } elseif ($versionMapping['type'] === 'date') {
240 2
                    $nextVersionDateTime = new \DateTime();
241 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
242 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
243
                }
244 39
                $data[$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
245
            }
246
247 491
            $inserts[$oid] = $data;
248
        }
249
250 491
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
251
            try {
252 491
                $this->collection->batchInsert($inserts, $options);
253 7
            } catch (\MongoException $e) {
254 7
                $this->queuedInserts = array();
255 7
                throw $e;
256
            }
257
        }
258
259
        /* All collections except for ones using addToSet have already been
260
         * saved. We have left these to be handled separately to avoid checking
261
         * collection for uniqueness on PHP side.
262
         */
263 491
        foreach ($this->queuedInserts as $document) {
264 491
            $this->handleCollections($document, $options);
265
        }
266
267 491
        $this->queuedInserts = array();
268 491
    }
269
270
    /**
271
     * Executes all queued document upserts.
272
     *
273
     * Queued documents with an ID are upserted individually.
274
     *
275
     * If no upserts are queued, invoking this method is a NOOP.
276
     *
277
     * @param array $options Options for batchInsert() and update() driver methods
278
     */
279 76
    public function executeUpserts(array $options = array())
280
    {
281 76
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
290 3
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
291 3
                if ($versionMapping['type'] === 'int') {
292 2
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
293 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
294 1
                } elseif ($versionMapping['type'] === 'date') {
295 1
                    $nextVersionDateTime = new \DateTime();
296 1
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
297 1
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
298
                }
299 3
                $data['$set'][$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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

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

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

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

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

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

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

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

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

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

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

    return array();
}

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

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

Loading history...
476 363
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
477 363
        $criteria = $this->addFilterToPreparedQuery($criteria);
478
479 363
        $cursor = $this->collection->find($criteria);
480
481 363
        if (null !== $sort) {
482 101
            $cursor->sort($this->prepareSortOrProjection($sort));
483
        }
484
485 363
        $result = $cursor->getSingleResult();
486
487 363
        if ($this->class->isLockable) {
488 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
489 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
490 1
                throw LockException::lockFailed($result);
491
            }
492
        }
493
494 362
        return $this->createDocument($result, $document, $hints);
495
    }
496
497
    /**
498
     * Finds documents by a set of criteria.
499
     *
500
     * @param array        $criteria Query criteria
501
     * @param array        $sort     Sort array for Cursor::sort()
502
     * @param integer|null $limit    Limit for Cursor::limit()
503
     * @param integer|null $skip     Skip for Cursor::skip()
504
     * @return Cursor
505
     */
506 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
507
    {
508 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
509 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
510 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
511
512 22
        $baseCursor = $this->collection->find($criteria);
513 22
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
514
515 22
        if (null !== $sort) {
516 3
            $cursor->sort($sort);
517
        }
518
519 22
        if (null !== $limit) {
520 2
            $cursor->limit($limit);
521
        }
522
523 22
        if (null !== $skip) {
524 2
            $cursor->skip($skip);
525
        }
526
527 22
        return $cursor;
528
    }
529
530
    /**
531
     * Wraps the supplied base cursor in the corresponding ODM class.
532
     *
533
     * @param CursorInterface $baseCursor
534
     * @return Cursor
535
     */
536 22
    private function wrapCursor(CursorInterface $baseCursor)
537
    {
538 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
539
    }
540
541
    /**
542
     * Checks whether the given managed document exists in the database.
543
     *
544
     * @param object $document
545
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
546
     */
547 3
    public function exists($document)
548
    {
549 3
        $id = $this->class->getIdentifierObject($document);
550 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
551
    }
552
553
    /**
554
     * Locks document by storing the lock mode on the mapped lock field.
555
     *
556
     * @param object $document
557
     * @param int $lockMode
558
     */
559 5
    public function lock($document, $lockMode)
560
    {
561 5
        $id = $this->uow->getDocumentIdentifier($document);
562 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
563 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
564 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
565 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
566 5
    }
567
568
    /**
569
     * Releases any lock that exists on this document.
570
     *
571
     * @param object $document
572
     */
573 1
    public function unlock($document)
574
    {
575 1
        $id = $this->uow->getDocumentIdentifier($document);
576 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
577 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
578 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
579 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
580 1
    }
581
582
    /**
583
     * Creates or fills a single document object from an query result.
584
     *
585
     * @param object $result The query result.
586
     * @param object $document The document object to fill, if any.
587
     * @param array $hints Hints for document creation.
588
     * @return object The filled and managed document object or NULL, if the query result is empty.
589
     */
590 362
    private function createDocument($result, $document = null, array $hints = array())
591
    {
592 362
        if ($result === null) {
593 115
            return null;
594
        }
595
596 310
        if ($document !== null) {
597 37
            $hints[Query::HINT_REFRESH] = true;
598 37
            $id = $this->class->getPHPIdentifierValue($result['_id']);
599 37
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
600
        }
601
602 310
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
603
    }
604
605
    /**
606
     * Loads a PersistentCollection data. Used in the initialize() method.
607
     *
608
     * @param PersistentCollectionInterface $collection
609
     */
610 163
    public function loadCollection(PersistentCollectionInterface $collection)
611
    {
612 163
        $mapping = $collection->getMapping();
613 163
        switch ($mapping['association']) {
614 163
            case ClassMetadata::EMBED_MANY:
615 114
                $this->loadEmbedManyCollection($collection);
616 114
                break;
617
618 65
            case ClassMetadata::REFERENCE_MANY:
619 65
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
620 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
621
                } else {
622 62
                    if ($mapping['isOwningSide']) {
623 52
                        $this->loadReferenceManyCollectionOwningSide($collection);
624
                    } else {
625 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
626
                    }
627
                }
628 65
                break;
629
        }
630 163
    }
631
632 114
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
633
    {
634 114
        $embeddedDocuments = $collection->getMongoData();
635 114
        $mapping = $collection->getMapping();
636 114
        $owner = $collection->getOwner();
637 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...
638 85
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
639 85
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
640 85
                $embeddedMetadata = $this->dm->getClassMetadata($className);
641 85
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
642
643 85
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
644
645 85
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
646 85
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
647 21
                    ? $data[$embeddedMetadata->identifier]
648 85
                    : null;
649
650 85
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
651 85
                if (CollectionHelper::isHash($mapping['strategy'])) {
652 25
                    $collection->set($key, $embeddedDocumentObject);
653
                } else {
654 85
                    $collection->add($embeddedDocumentObject);
655
                }
656
            }
657
        }
658 114
    }
659
660 52
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
661
    {
662 52
        $hints = $collection->getHints();
663 52
        $mapping = $collection->getMapping();
664 52
        $groupedIds = array();
665
666 52
        $sorted = isset($mapping['sort']) && $mapping['sort'];
667
668 52
        foreach ($collection->getMongoData() as $key => $reference) {
669 47
            if (isset($mapping['simple']) && $mapping['simple']) {
670 4
                $className = $mapping['targetDocument'];
671 4
                $mongoId = $reference;
672
            } else {
673 43
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
674 43
                $mongoId = $reference['$id'];
675
            }
676 47
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
677
678
            // create a reference to the class and id
679 47
            $reference = $this->dm->getReference($className, $id);
680
681
            // no custom sort so add the references right now in the order they are embedded
682 47
            if ( ! $sorted) {
683 46
                if (CollectionHelper::isHash($mapping['strategy'])) {
684 2
                    $collection->set($key, $reference);
685
                } else {
686 44
                    $collection->add($reference);
687
                }
688
            }
689
690
            // only query for the referenced object if it is not already initialized or the collection is sorted
691 47
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

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

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

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

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