Completed
Pull Request — master (#1263)
by Andreas
14:36
created

DocumentPersister   F

Complexity

Total Complexity 220

Size/Duplication

Total Lines 1240
Duplicated Lines 4.11 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 1.54%

Importance

Changes 12
Bugs 4 Features 1
Metric Value
wmc 220
c 12
b 4
f 1
lcom 1
cbo 18
dl 51
loc 1240
ccs 9
cts 584
cp 0.0154
rs 0.6314

39 Methods

Rating   Name   Duplication   Size   Complexity  
A getInserts() 0 4 1
A isQueuedForInsert() 0 4 1
A getUpserts() 0 4 1
A isQueuedForUpsert() 0 4 1
A getClassMetadata() 0 4 1
A __construct() 0 19 2
A addInsert() 0 4 1
A addUpsert() 0 4 1
C executeInserts() 12 46 9
C executeUpserts() 12 33 7
B executeUpsert() 0 40 6
C update() 3 49 10
B delete() 3 15 5
A refresh() 0 6 1
C load() 0 28 8
B loadAll() 0 23 4
A wrapCursor() 0 4 1
A exists() 0 5 1
A lock() 0 8 1
A unlock() 0 8 1
A createDocument() 0 14 3
B loadCollection() 0 21 6
C loadEmbedManyCollection() 0 29 7
F loadReferenceManyCollectionOwningSide() 3 72 19
A loadReferenceManyCollectionInverseSide() 0 8 2
F createReferenceManyInverseSideQuery() 3 36 10
A loadReferenceManyWithRepositoryMethod() 0 13 3
C createReferenceManyWithRepositoryMethodCursor() 3 30 7
A prepareSortOrProjection() 0 10 2
A prepareFieldName() 0 6 1
A addDiscriminatorToPreparedQuery() 0 16 4
A addFilterToPreparedQuery() 0 14 2
D prepareQueryOrNewObj() 0 27 9
F prepareQueryElement() 9 183 48
C prepareQueryExpression() 0 27 8
B hasDBRefFields() 0 18 8
B hasQueryOperators() 0 18 7
B getClassDiscriminatorValues() 3 16 5
B handleCollections() 0 19 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocumentPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentPersister, and based on these observations, apply Extract Interface, too.

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 601
    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 601
        $this->pb = $pb;
138 601
        $this->dm = $dm;
139 601
        $this->evm = $evm;
140 601
        $this->cm = $cm ?: new CriteriaMerger();
141 601
        $this->uow = $uow;
142 601
        $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 601
        $this->class = $class;
144 601
        $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
        $this->cp = $this->uow->getCollectionPersister();
146
    }
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
    public function addInsert($document)
172
    {
173
        $this->queuedInserts[spl_object_hash($document)] = $document;
174
    }
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
    public function addUpsert($document)
200
    {
201
        $this->queuedUpserts[spl_object_hash($document)] = $document;
202
    }
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
    public function executeInserts(array $options = array())
225
    {
226
        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
        $inserts = array();
231
        foreach ($this->queuedInserts as $oid => $document) {
232
            $data = $this->pb->prepareInsertData($document);
233
234
            // Set the initial version for each insert
235 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
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
237
                if ($versionMapping['type'] === 'int') {
238
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
239
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
240
                } elseif ($versionMapping['type'] === 'date') {
241
                    $nextVersionDateTime = new \DateTime();
242
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
243
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
244
                }
245
                $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
            $inserts[$oid] = $data;
249
        }
250
251
        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
                $this->collection->batchInsert($inserts, $options);
254
            } catch (\MongoException $e) {
255
                $this->queuedInserts = array();
256
                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
        foreach ($this->queuedInserts as $document) {
265
            $this->handleCollections($document, $options);
266
        }
267
268
        $this->queuedInserts = array();
269
    }
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
    public function executeUpserts(array $options = array())
281
    {
282
        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
        foreach ($this->queuedUpserts as $oid => $document) {
287
            $data = $this->pb->prepareUpsertData($document);
288
289
            // Set the initial version for each upsert
290 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
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
292
                if ($versionMapping['type'] === 'int') {
293
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
294
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
295
                } elseif ($versionMapping['type'] === 'date') {
296
                    $nextVersionDateTime = new \DateTime();
297
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
298
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
299
                }
300
                $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
                $this->executeUpsert($data, $options);
305
                $this->handleCollections($document, $options);
306
                unset($this->queuedUpserts[$oid]);
307
            } catch (\MongoException $e) {
308
                unset($this->queuedUpserts[$oid]);
309
                throw $e;
310
            }
311
        }
312
    }
313
314
    /**
315
     * Executes a single upsert in {@link executeInserts}
316
     *
317
     * @param array $data
318
     * @param array $options
319
     */
320
    private function executeUpsert(array $data, array $options)
321
    {
322
        $options['upsert'] = true;
323
        $criteria = array('_id' => $data['$set']['_id']);
324
        unset($data['$set']['_id']);
325
326
        // Do not send an empty $set modifier
327
        if (empty($data['$set'])) {
328
            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
        if (empty($data)) {
345
            $retry = true;
346
            $data = array('$set' => array('_id' => $criteria['_id']));
347
        }
348
349
        try {
350
            $this->collection->update($criteria, $data, $options);
351
            return;
352
        } catch (\MongoCursorException $e) {
353
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
354
                throw $e;
355
            }
356
        }
357
358
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
359
    }
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
    public function update($document, array $options = array())
369
    {
370
        $id = $this->uow->getDocumentIdentifier($document);
371
        $update = $this->pb->prepareUpdateData($document);
372
373
        $id = $this->class->getDatabaseIdentifierValue($id);
374
        $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
        if ($this->class->isVersioned) {
380
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
381
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
382
            if ($versionMapping['type'] === 'int') {
383
                $nextVersion = $currentVersion + 1;
384
                $update['$inc'][$versionMapping['name']] = 1;
385
                $query[$versionMapping['name']] = $currentVersion;
386
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
387
            } elseif ($versionMapping['type'] === 'date') {
388
                $nextVersion = new \DateTime();
389
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
390
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
391
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
392
            }
393
        }
394
395
        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
            if ($this->class->isLockable) {
399
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
400
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
401
                if ($isLocked) {
402
                    $update['$unset'] = array($lockMapping['name'] => true);
403
                } else {
404
                    $query[$lockMapping['name']] = array('$exists' => false);
405
                }
406
            }
407
408
            $result = $this->collection->update($query, $update, $options);
409
410 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
                throw LockException::lockFailed($document);
412
            }
413
        }
414
415
        $this->handleCollections($document, $options);
416
    }
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
    public function delete($document, array $options = array())
426
    {
427
        $id = $this->uow->getDocumentIdentifier($document);
428
        $query = array('_id' => $this->class->getDatabaseIdentifierValue($id));
429
430
        if ($this->class->isLockable) {
431
            $query[$this->class->lockField] = array('$exists' => false);
432
        }
433
434
        $result = $this->collection->remove($query, $options);
435
436 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
            throw LockException::lockFailed($document);
438
        }
439
    }
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
    public function refresh($id, $document)
448
    {
449
        $data = $this->collection->findOne(array('_id' => $id));
450
        $data = $this->hydratorFactory->hydrate($document, $data);
451
        $this->uow->setOriginalDocumentData($document, $data);
452
    }
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
    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
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
473
            $criteria = array('_id' => $criteria);
474
        }
475
476
        $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
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
478
        $criteria = $this->addFilterToPreparedQuery($criteria);
479
480
        $cursor = $this->collection->find($criteria);
481
482
        if (null !== $sort) {
483
            $cursor->sort($this->prepareSortOrProjection($sort));
484
        }
485
486
        $result = $cursor->getSingleResult();
487
488
        if ($this->class->isLockable) {
489
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
490
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
491
                throw LockException::lockFailed($result);
492
            }
493
        }
494
495
        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
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
508
    {
509
        $criteria = $this->prepareQueryOrNewObj($criteria);
510
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
511
        $criteria = $this->addFilterToPreparedQuery($criteria);
512
513
        $baseCursor = $this->collection->find($criteria);
514
        $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
        if (null !== $sort) {
517
            $cursor->sort($sort);
518
        }
519
520
        if (null !== $limit) {
521
            $cursor->limit($limit);
522
        }
523
524
        if (null !== $skip) {
525
            $cursor->skip($skip);
526
        }
527
528
        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
    private function wrapCursor(CursorInterface $baseCursor)
538
    {
539
        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
    public function exists($document)
549
    {
550
        $id = $this->class->getIdentifierObject($document);
551
        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
    public function lock($document, $lockMode)
561
    {
562
        $id = $this->uow->getDocumentIdentifier($document);
563
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
564
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
565
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
566
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
567
    }
568
569
    /**
570
     * Releases any lock that exists on this document.
571
     *
572
     * @param object $document
573
     */
574
    public function unlock($document)
575
    {
576
        $id = $this->uow->getDocumentIdentifier($document);
577
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
578
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
579
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
580
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
581
    }
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
    private function createDocument($result, $document = null, array $hints = array())
592
    {
593
        if ($result === null) {
594
            return null;
595
        }
596
597
        if ($document !== null) {
598
            $hints[Query::HINT_REFRESH] = true;
599
            $id = $this->class->getPHPIdentifierValue($result['_id']);
600
            $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
        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
    public function loadCollection(PersistentCollectionInterface $collection)
612
    {
613
        $mapping = $collection->getMapping();
614
        switch ($mapping['association']) {
615
            case ClassMetadata::EMBED_MANY:
616
                $this->loadEmbedManyCollection($collection);
617
                break;
618
619
            case ClassMetadata::REFERENCE_MANY:
620
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
621
                    $this->loadReferenceManyWithRepositoryMethod($collection);
622
                } else {
623
                    if ($mapping['isOwningSide']) {
624
                        $this->loadReferenceManyCollectionOwningSide($collection);
625
                    } else {
626
                        $this->loadReferenceManyCollectionInverseSide($collection);
627
                    }
628
                }
629
                break;
630
        }
631
    }
632
633
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
634
    {
635
        $embeddedDocuments = $collection->getMongoData();
636
        $mapping = $collection->getMapping();
637
        $owner = $collection->getOwner();
638
        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
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
640
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
641
                $embeddedMetadata = $this->dm->getClassMetadata($className);
642
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
643
644
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
645
646
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
647
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
648
                    ? $data[$embeddedMetadata->identifier]
649
                    : null;
650
                
651
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
652
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
653
                }
654
                if (CollectionHelper::isHash($mapping['strategy'])) {
655
                    $collection->set($key, $embeddedDocumentObject);
656
                } else {
657
                    $collection->add($embeddedDocumentObject);
658
                }
659
            }
660
        }
661
    }
662
663
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
664
    {
665
        $hints = $collection->getHints();
666
        $mapping = $collection->getMapping();
667
        $groupedIds = array();
668
669
        $sorted = isset($mapping['sort']) && $mapping['sort'];
670
671
        foreach ($collection->getMongoData() as $key => $reference) {
672
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
673
                $className = $mapping['targetDocument'];
674
                $mongoId = $reference;
675
            } else {
676
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
677
                $mongoId = $reference['$id'];
678
            }
679
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
680
681
            // create a reference to the class and id
682
            $reference = $this->dm->getReference($className, $id);
683
684
            // no custom sort so add the references right now in the order they are embedded
685
            if ( ! $sorted) {
686
                if (CollectionHelper::isHash($mapping['strategy'])) {
687
                    $collection->set($key, $reference);
688
                } else {
689
                    $collection->add($reference);
690
                }
691
            }
692
693
            // only query for the referenced object if it is not already initialized or the collection is sorted
694
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
695
                $groupedIds[$className][] = $mongoId;
696
            }
697
        }
698
        foreach ($groupedIds as $className => $ids) {
699
            $class = $this->dm->getClassMetadata($className);
700
            $mongoCollection = $this->dm->getDocumentCollection($className);
701
            $criteria = $this->cm->merge(
702
                array('_id' => array('$in' => array_values($ids))),
703
                $this->dm->getFilterCollection()->getFilterCriteria($class),
704
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
705
            );
706
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
707
            $cursor = $mongoCollection->find($criteria);
708
            if (isset($mapping['sort'])) {
709
                $cursor->sort($mapping['sort']);
710
            }
711
            if (isset($mapping['limit'])) {
712
                $cursor->limit($mapping['limit']);
713
            }
714
            if (isset($mapping['skip'])) {
715
                $cursor->skip($mapping['skip']);
716
            }
717
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
718
                $cursor->slaveOkay(true);
719
            }
720 View Code Duplication
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
721
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
722
            }
723
            $documents = $cursor->toArray(false);
724
            foreach ($documents as $documentData) {
725
                $document = $this->uow->getById($documentData['_id'], $class);
726
                $data = $this->hydratorFactory->hydrate($document, $documentData);
727
                $this->uow->setOriginalDocumentData($document, $data);
728
                $document->__isInitialized__ = true;
729
                if ($sorted) {
730
                    $collection->add($document);
731
                }
732
            }
733
        }
734
    }
735
736
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
737
    {
738
        $query = $this->createReferenceManyInverseSideQuery($collection);
739
        $documents = $query->execute()->toArray(false);
740
        foreach ($documents as $key => $document) {
741
            $collection->add($document);
742
        }
743
    }
744
745
    /**
746
     * @param PersistentCollectionInterface $collection
747
     *
748
     * @return Query
749
     */
750
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
751
    {
752
        $hints = $collection->getHints();
753
        $mapping = $collection->getMapping();
754
        $owner = $collection->getOwner();
755
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
756
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
757
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
758
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
759
        $criteria = $this->cm->merge(
760
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
761
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
762
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
763
        );
764
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
765
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
766
            ->setQueryArray($criteria);
767
768
        if (isset($mapping['sort'])) {
769
            $qb->sort($mapping['sort']);
770
        }
771
        if (isset($mapping['limit'])) {
772
            $qb->limit($mapping['limit']);
773
        }
774
        if (isset($mapping['skip'])) {
775
            $qb->skip($mapping['skip']);
776
        }
777
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
778
            $qb->slaveOkay(true);
779
        }
780 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
781
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
782
        }
783
784
        return $qb->getQuery();
785
    }
786
787
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
788
    {
789
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
790
        $mapping = $collection->getMapping();
791
        $documents = $cursor->toArray(false);
0 ignored issues
show
Unused Code introduced by
The call to CursorInterface::toArray() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
792
        foreach ($documents as $key => $obj) {
793
            if (CollectionHelper::isHash($mapping['strategy'])) {
794
                $collection->set($key, $obj);
795
            } else {
796
                $collection->add($obj);
797
            }
798
        }
799
    }
800
801
    /**
802
     * @param PersistentCollectionInterface $collection
803
     *
804
     * @return CursorInterface
805
     */
806
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
807
    {
808
        $hints = $collection->getHints();
809
        $mapping = $collection->getMapping();
810
        $repositoryMethod = $mapping['repositoryMethod'];
811
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
812
            ->$repositoryMethod($collection->getOwner());
813
814
        if ( ! $cursor instanceof CursorInterface) {
815
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
816
        }
817
818
        if (isset($mapping['sort'])) {
819
            $cursor->sort($mapping['sort']);
820
        }
821
        if (isset($mapping['limit'])) {
822
            $cursor->limit($mapping['limit']);
823
        }
824
        if (isset($mapping['skip'])) {
825
            $cursor->skip($mapping['skip']);
826
        }
827
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
828
            $cursor->slaveOkay(true);
829
        }
830 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
831
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
832
        }
833
834
        return $cursor;
835
    }
836
837
    /**
838
     * Prepare a sort or projection array by converting keys, which are PHP
839
     * property names, to MongoDB field names.
840
     *
841
     * @param array $fields
842
     * @return array
843
     */
844
    public function prepareSortOrProjection(array $fields)
845
    {
846
        $preparedFields = array();
847
848
        foreach ($fields as $key => $value) {
849
            $preparedFields[$this->prepareFieldName($key)] = $value;
850
        }
851
852
        return $preparedFields;
853
    }
854
855
    /**
856
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
857
     *
858
     * @param string $fieldName
859
     * @return string
860
     */
861
    public function prepareFieldName($fieldName)
862
    {
863
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
864
865
        return $fieldName;
866
    }
867
868
    /**
869
     * Adds discriminator criteria to an already-prepared query.
870
     *
871
     * This method should be used once for query criteria and not be used for
872
     * nested expressions. It should be called before
873
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
874
     *
875
     * @param array $preparedQuery
876
     * @return array
877
     */
878
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
879
    {
880
        /* If the class has a discriminator field, which is not already in the
881
         * criteria, inject it now. The field/values need no preparation.
882
         */
883
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
884
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
885
            if (count($discriminatorValues) === 1) {
886
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
887
            } else {
888
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
889
            }
890
        }
891
892
        return $preparedQuery;
893
    }
894
895
    /**
896
     * Adds filter criteria to an already-prepared query.
897
     *
898
     * This method should be used once for query criteria and not be used for
899
     * nested expressions. It should be called after
900
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
901
     *
902
     * @param array $preparedQuery
903
     * @return array
904
     */
905
    public function addFilterToPreparedQuery(array $preparedQuery)
906
    {
907
        /* If filter criteria exists for this class, prepare it and merge
908
         * over the existing query.
909
         *
910
         * @todo Consider recursive merging in case the filter criteria and
911
         * prepared query both contain top-level $and/$or operators.
912
         */
913
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
914
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
915
        }
916
917
        return $preparedQuery;
918
    }
919
920
    /**
921
     * Prepares the query criteria or new document object.
922
     *
923
     * PHP field names and types will be converted to those used by MongoDB.
924
     *
925
     * @param array $query
926
     * @return array
927
     */
928
    public function prepareQueryOrNewObj(array $query)
929
    {
930
        $preparedQuery = array();
931
932
        foreach ($query as $key => $value) {
933
            // Recursively prepare logical query clauses
934
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
935
                foreach ($value as $k2 => $v2) {
936
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
937
                }
938
                continue;
939
            }
940
941
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
942
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
943
                continue;
944
            }
945
946
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
947
948
            $preparedQuery[$key] = is_array($value)
949
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
950
                : Type::convertPHPToDatabaseValue($value);
951
        }
952
953
        return $preparedQuery;
954
    }
955
956
    /**
957
     * Prepares a query value and converts the PHP value to the database value
958
     * if it is an identifier.
959
     *
960
     * It also handles converting $fieldName to the database name if they are different.
961
     *
962
     * @param string $fieldName
963
     * @param mixed $value
964
     * @param ClassMetadata $class        Defaults to $this->class
965
     * @param boolean $prepareValue Whether or not to prepare the value
966
     * @return array        Prepared field name and value
967
     */
968
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
969
    {
970
        $class = isset($class) ? $class : $this->class;
971
972
        // @todo Consider inlining calls to ClassMetadataInfo methods
973
974
        // Process all non-identifier fields by translating field names
975
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
976
            $mapping = $class->fieldMappings[$fieldName];
977
            $fieldName = $mapping['name'];
978
979
            if ( ! $prepareValue) {
980
                return array($fieldName, $value);
981
            }
982
983
            // Prepare mapped, embedded objects
984
            if ( ! empty($mapping['embedded']) && is_object($value) &&
985
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
986
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
987
            }
988
989
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
990
                try {
991
                    return array($fieldName, $this->dm->createDBRef($value, $mapping));
992
                } catch (MappingException $e) {
993
                    // do nothing in case passed object is not mapped document
994
                }
995
            }
996
997
            // No further preparation unless we're dealing with a simple reference
998
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
999
            $arrayValue = (array) $value;
1000
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1001
                return array($fieldName, $value);
1002
            }
1003
1004
            // Additional preparation for one or more simple reference values
1005
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1006
1007
            if ( ! is_array($value)) {
1008
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1009
            }
1010
1011
            // Objects without operators or with DBRef fields can be converted immediately
1012 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
1033
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1034
            }
1035
1036
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1037
        }
1038
1039
        // No processing for unmapped, non-identifier, non-dotted field names
1040
        if (strpos($fieldName, '.') === false) {
1041
            return array($fieldName, $value);
1042
        }
1043
1044
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1045
         *
1046
         * We can limit parsing here, since at most three segments are
1047
         * significant: "fieldName.objectProperty" with an optional index or key
1048
         * for collections stored as either BSON arrays or objects.
1049
         */
1050
        $e = explode('.', $fieldName, 4);
1051
1052
        // No further processing for unmapped fields
1053
        if ( ! isset($class->fieldMappings[$e[0]])) {
1054
            return array($fieldName, $value);
1055
        }
1056
1057
        $mapping = $class->fieldMappings[$e[0]];
1058
        $e[0] = $mapping['name'];
1059
1060
        // Hash and raw fields will not be prepared beyond the field name
1061
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1062
            $fieldName = implode('.', $e);
1063
1064
            return array($fieldName, $value);
1065
        }
1066
1067
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1068
                && isset($e[2])) {
1069
            $objectProperty = $e[2];
1070
            $objectPropertyPrefix = $e[1] . '.';
1071
            $nextObjectProperty = implode('.', array_slice($e, 3));
1072
        } elseif ($e[1] != '$') {
1073
            $fieldName = $e[0] . '.' . $e[1];
1074
            $objectProperty = $e[1];
1075
            $objectPropertyPrefix = '';
1076
            $nextObjectProperty = implode('.', array_slice($e, 2));
1077
        } elseif (isset($e[2])) {
1078
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1079
            $objectProperty = $e[2];
1080
            $objectPropertyPrefix = $e[1] . '.';
1081
            $nextObjectProperty = implode('.', array_slice($e, 3));
1082
        } else {
1083
            $fieldName = $e[0] . '.' . $e[1];
1084
1085
            return array($fieldName, $value);
1086
        }
1087
1088
        // No further processing for fields without a targetDocument mapping
1089
        if ( ! isset($mapping['targetDocument'])) {
1090
            if ($nextObjectProperty) {
1091
                $fieldName .= '.'.$nextObjectProperty;
1092
            }
1093
1094
            return array($fieldName, $value);
1095
        }
1096
1097
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1098
1099
        // No further processing for unmapped targetDocument fields
1100
        if ( ! $targetClass->hasField($objectProperty)) {
1101
            if ($nextObjectProperty) {
1102
                $fieldName .= '.'.$nextObjectProperty;
1103
            }
1104
1105
            return array($fieldName, $value);
1106
        }
1107
1108
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1109
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1110
1111
        // Prepare DBRef identifiers or the mapped field's property path
1112
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1113
            ? $e[0] . '.$id'
1114
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1115
1116
        // Process targetDocument identifier fields
1117
        if ($objectPropertyIsId) {
1118
            if ( ! $prepareValue) {
1119
                return array($fieldName, $value);
1120
            }
1121
1122
            if ( ! is_array($value)) {
1123
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1124
            }
1125
1126
            // Objects without operators or with DBRef fields can be converted immediately
1127 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1128
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1129
            }
1130
1131
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1132
        }
1133
1134
        /* The property path may include a third field segment, excluding the
1135
         * collection item pointer. If present, this next object property must
1136
         * be processed recursively.
1137
         */
1138
        if ($nextObjectProperty) {
1139
            // Respect the targetDocument's class metadata when recursing
1140
            $nextTargetClass = isset($targetMapping['targetDocument'])
1141
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1142
                : null;
1143
1144
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1145
1146
            $fieldName .= '.' . $key;
1147
        }
1148
1149
        return array($fieldName, $value);
1150
    }
1151
1152
    /**
1153
     * Prepares a query expression.
1154
     *
1155
     * @param array|object  $expression
1156
     * @param ClassMetadata $class
1157
     * @return array
1158
     */
1159
    private function prepareQueryExpression($expression, $class)
1160
    {
1161
        foreach ($expression as $k => $v) {
1162
            // Ignore query operators whose arguments need no type conversion
1163
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1164
                continue;
1165
            }
1166
1167
            // Process query operators whose argument arrays need type conversion
1168
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1169
                foreach ($v as $k2 => $v2) {
1170
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1171
                }
1172
                continue;
1173
            }
1174
1175
            // Recursively process expressions within a $not operator
1176
            if ($k === '$not' && is_array($v)) {
1177
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1178
                continue;
1179
            }
1180
1181
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1182
        }
1183
1184
        return $expression;
1185
    }
1186
1187
    /**
1188
     * Checks whether the value has DBRef fields.
1189
     *
1190
     * This method doesn't check if the the value is a complete DBRef object,
1191
     * although it should return true for a DBRef. Rather, we're checking that
1192
     * the value has one or more fields for a DBref. In practice, this could be
1193
     * $elemMatch criteria for matching a DBRef.
1194
     *
1195
     * @param mixed $value
1196
     * @return boolean
1197
     */
1198
    private function hasDBRefFields($value)
1199
    {
1200
        if ( ! is_array($value) && ! is_object($value)) {
1201
            return false;
1202
        }
1203
1204
        if (is_object($value)) {
1205
            $value = get_object_vars($value);
1206
        }
1207
1208
        foreach ($value as $key => $_) {
1209
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1210
                return true;
1211
            }
1212
        }
1213
1214
        return false;
1215
    }
1216
1217
    /**
1218
     * Checks whether the value has query operators.
1219
     *
1220
     * @param mixed $value
1221
     * @return boolean
1222
     */
1223
    private function hasQueryOperators($value)
1224
    {
1225
        if ( ! is_array($value) && ! is_object($value)) {
1226
            return false;
1227
        }
1228
1229
        if (is_object($value)) {
1230
            $value = get_object_vars($value);
1231
        }
1232
1233
        foreach ($value as $key => $_) {
1234
            if (isset($key[0]) && $key[0] === '$') {
1235
                return true;
1236
            }
1237
        }
1238
1239
        return false;
1240
    }
1241
1242
    /**
1243
     * Gets the array of discriminator values for the given ClassMetadata
1244
     *
1245
     * @param ClassMetadata $metadata
1246
     * @return array
1247
     */
1248
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1249
    {
1250
        $discriminatorValues = array($metadata->discriminatorValue);
1251
        foreach ($metadata->subClasses as $className) {
1252
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1253
                $discriminatorValues[] = $key;
1254
            }
1255
        }
1256
1257
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1258 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1259
            $discriminatorValues[] = null;
1260
        }
1261
1262
        return $discriminatorValues;
1263
    }
1264
1265
    private function handleCollections($document, $options)
1266
    {
1267
        // Collection deletions (deletions of complete collections)
1268
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1269
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1270
                $this->cp->delete($coll, $options);
1271
            }
1272
        }
1273
        // Collection updates (deleteRows, updateRows, insertRows)
1274
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1275
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1276
                $this->cp->update($coll, $options);
1277
            }
1278
        }
1279
        // Take new snapshots from visited collections
1280
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1281
            $coll->takeSnapshot();
1282
        }
1283
    }
1284
}
1285